Edited at

RustでUTF-8とShift_JISが混在した日本語のデコード

一日遅れました。


はじめに

複数の文字コードが混じった文字列を扱う事が極稀にあり、その文字列をUTF-8に統一して扱えるようにするライブラリ、およびコマンドを作成しました。

成果物は自作コマンド集の一部となってますのでこちらからどうぞ。

自作コマンド集 - GitHub

本記事対象のコマンド - GitHub


UTF-8とShift_JIS混在文字列のデコード方法

文字列をUTF-8に統一するには、UTF-8部分はそのままに、Shift_JIS部分をUTF-8としてデコードする必要があります。

例えば、Shift_JISの"大将"の後にUTF-8の"大将"が続く文字列がある場合、バイト列は"91 e5 8f ab e5 a4 a7 e5 b0 86"となります、ここからShift_JIS大将部分"91 e5 8f ab"をUTF-8にデコードすればいいのですが、この部分を抜き出すのが問題となってきます。

このバイト列のUTF-8、Shift_JISそれぞれで有効な文字コードをチェックすると、それぞれで文字コードが被る範囲がある事が分かります。

UTF-8の有効範囲

91 [e5 8f ab e5 a4 a7 e5 b0 86]
Shift_JISの有効範囲
[91 e5 8f ab e5 a4 a7 e5 b0] 86

このままではShift_JISとUTF-8の境目が不明ですが、幸いにもShift_JISのマルチバイト文字はすべてUTF-8の有効文字コードかのチェックに失敗します。

そのため、デコード方法を改めた結果次のようなシンプルな方法に落ち着きました。


  1. UTF-8の有効文字コード部分を取り出す。

  2. 1の失敗後に2バイト以上あるなら、その2バイトをShift_JISのマルチバイト文字としてUTF-8デコード

  3. バイト列の終わりまで1、2を繰り返す


実装

上記をRustのコードに落とし込んでみます、デコードライブラリとしてhsivonen/encoding_rsを利用しています。

このコードは冒頭の自作コマンドの中からUTF-8デコードをしている関数の抜粋です。

// mycommands/_origrun/src/decodingreaer.rs

fn decode_utf8_sjis(src: &Vec<u8>, dst: &mut Vec<u8>) -> io::Result<usize> {
let mut utf8_decoder = UTF_8.new_decoder();

let mut buf: Box<[u8]> = unsafe {
// NOTE: *4は適当、とりあえずのOutputFull回避
let size = src.len() * 4;
let mut v = Vec::with_capacity(size);
v.set_len(size);
v.into_boxed_slice()
};

let mut src_cur = 0usize;
let src_len = src.len();
while src_cur < src_len {
// UTF-8でデコード
let (result, nread, nwrite) =
utf8_decoder.decode_to_utf8_without_replacement(&src[src_cur..],
&mut buf[..],
false);

let bad_bytes: usize = if let DecoderResult::Malformed(bad_bytes, _consumed_bytes) = result {
bad_bytes as usize
} else {
0
};

if nread > 0 && nread > bad_bytes {
// Success
for s in buf[..nwrite].iter() {
dst.push(*s);
}
src_cur += nread - bad_bytes;

match result {
DecoderResult::InputEmpty => {
break;
}
DecoderResult::OutputFull => {
eprintln!("buf size is short");
assert!(false); // bufが足りない
}
DecoderResult::Malformed(_, _) => {
continue; // no-op
}
}
} else if src_cur + 2 <= src_len {
// Shift_JISの全角文字としてリトライ
let (result, nread, nwrite) =
SHIFT_JIS.new_decoder().decode_to_utf8_without_replacement(&src[src_cur..src_cur+2],
&mut buf[..],
true);

let bad_bytes: usize = if let DecoderResult::Malformed(bad_bytes, _consumed_bytes) = result {
bad_bytes as usize
} else {
0
};

if nread > 0 && nread > bad_bytes {
// Success
for s in buf[..nwrite].iter() {
dst.push(*s);
}
src_cur += nread - bad_bytes;
} else {
// 失敗、不明バイト
dst.push(0x3f); // "?"
src_cur += 1;
}
} else {
// 不明バイト
dst.push(0x3f); // "?"
src_cur += 1;
}
}

// Process EOF
{
let (result, _nread, nwrite, _had_errors) =
utf8_decoder.decode_to_utf8(b"",
&mut buf[..],
true);

for s in buf[..nwrite].iter() {
dst.push(*s);
}

assert!(result == CoderResult::InputEmpty);
}

Ok(dst.len())
}

ここで少し解説を

decode_to_utf8_without_replacement関数は渡されたバイト列をUTF-8にデコードします、Shift_JISのデコーダーから呼び出した場合はShift_JISからUTF-8へのデコード処理として機能し、UTF-8のデコーダーから呼び出した場合はUTF-8文字コードチェック処理に利用できます。

        // UTF-8チェック

let (result, nread, nwrite) =
utf8_decoder.decode_to_utf8_without_replacement(&src[src_cur..],
&mut buf[..],
false);

let bad_bytes: usize = if let DecoderResult::Malformed(bad_bytes, _consumed_bytes) = result {
bad_bytes as usize
} else {
0
};

...

nreadにはデコード時に読み込んだバイト数、resultから取得出来るbad_bytesはデコード失敗時のバイト数で、nread - bad_bytesで正常にデコードした元のバイト列が取得できます、nwriteにはデコード後のバイト数、bufにはデコード後のバイト列が入ります。

ここで失敗すればShift_JISのマルチバイト文字にさしかかったと判断出来るのでデコーダーを切り替えます、その際はUTF-8用のバイト列がShift_JISとしてデコードされると困るので、2バイト分だけのデコードにとどめます。

        ... UTF-8デコードの場合 ...

} else if src_cur + 2 <= src_len {
// Shift_JISの全角文字としてリトライ
let (result, nread, nwrite) =
SHIFT_JIS.new_decoder().decode_to_utf8_without_replacement(&src[src_cur..src_cur+2],
&mut buf[..],
true);

let bad_bytes: usize = if let DecoderResult::Malformed(bad_bytes, _consumed_bytes) = result {
bad_bytes as usize
} else {
0
};

...
}


まとめ


  • Shift_JISとUTF-8が混じった文字列をUTF-8に統一出来るコマンドを作成した

  • この方法はShift_JISとUTF-8だから出来る方法なので、EUC-JPも含めようと思ったらまったく別の方法をとる必要があり、かなり難易度が上がる

  • 本記事では行単位のデコードだけど、先頭から順次デコードする方法なのでストリームデコーダにも利用出来る、ただしEUC-JPも対応する場合はその限りではない。


参考