【Power Query】CSVの文字コードを置換文字で判定する

CSVの文字コードを置換文字で判定する Power Queryのコード例 Power Query

CSVを取り込むと、文字化けすることがあります。ファイルがShift_JISなのかUTF-8なのか、分からないときです。

以前の記事「【Power Query】CSVの文字化けをEncoding指定で防ぐ方法」では、先頭のBOMの有無で判定する方法を紹介しました。ただし、BOMのないUTF-8を、Shift_JISと誤判定する限界がありました。

今回は、この限界を置換文字(�)で補えるかを試しました。結論を先に書くと、補えたのは一部です。Shift_JIS側では置換文字が出ず、想定していた「両方を数えて比べる」方法は成り立ちませんでした。

想定読者

  • 取込元ごとに文字コードが違うCSVを、1つのクエリで取り込みたい人
  • BOMなしのUTF-8のCSVが混ざることがある人
  • 文字コードの自動判定で、何が判定できて何ができないかを知りたい人

結論

  • UTF-8として読めないバイトは、置換文字(U+FFFD)になる
  • Shift_JIS(932)で読むと、置換文字は出ない
  • 判定に使えるのは「UTF-8で読んで、置換文字が出るか」だけ
  • 英数字だけのCSVと、半角カナのShift_JISは、この方法では区別できない

検証環境は、Excel 16.0(Windows、日本語環境、コードページ932)です。他のバージョン・Mac版は未検証です。

置換文字と、最初に考えた方法

バイト列を、ある文字コードとして読めなかったとき、その位置は「�」(U+FFFD)に置き換わります。これが置換文字です。

最初は、次の方法を考えました。

  1. UTF-8とShift_JISの両方で読む
  2. 置換文字の数を数える
  3. 少ない方を採る

Shift_JIS側は数えられなかった

実際に試すと、Shift_JIS(932)で読んだときの置換文字は、どのファイルでも0個でした。UTF-8で作ったCSVを932で読んでも、0個です(3種類のファイルで確認)。

そこで、不正なバイトを含む短いバイト列を、両方の文字コードで読み比べました。

バイト列(10進)UTF-8(65001)で読むShift_JIS(932)で読む
65 255 66A、置換文字、BA、私用領域の文字(U+F8F3)、B
65 129 32 66A、置換文字、スペース、BA、中黒(・)、B
65 129(末尾)A、置換文字A、中黒(・)

Shift_JISで読むと、不正なバイトは置換文字ではなく、中黒(U+30FB)や私用領域の文字に変わりました。2行目では、後ろのスペースまで1文字に吸収されています。ほかに、128は制御文字(U+0080)、160・253・255は私用領域の文字になりました。13パターンで確認しています。

それなら、932で読んだときの中黒を数えればよいかもしれません。そこで、5種類のファイルを932で読み、中黒の数を数えました。

  • 正しいShift_JISのファイル:1個(もともと中黒を1つ含むため)
  • BOMありのUTF-8:4個、BOMなしのUTF-8:3個
  • 英数字だけ、半角カナのShift_JIS:0個

中黒は、正しい文字としても現れます。件数だけでは、中黒を含む正しいShift_JISと区別できません。半角カナのShift_JISは0個で、この方法でも判定できませんでした。サンプルは5ファイルだけで、境目になる件数は決められません。判定には使わないことにしました。

判定のコード

使えるのは、UTF-8側の置換文字だけです。UTF-8として読めたらUTF-8、読めなければShift_JISとみなします。

let
    Count = (t as text, ch as text) as number =>
        Text.Length(t) - Text.Length(Text.Replace(t, ch, "")),
    Detect = (buf as binary) as record =>
        let
            t8 = Text.FromBinary(buf, 65001),
            bad = Count(t8, "#(fffd)"),
            kind =
                if bad > 0 then "NotUtf8"
                else if Text.Length(t8) = Binary.Length(buf) then "AsciiOnly"
                else "ValidUtf8"
        in
            [Utf8Bad = bad, Kind = kind, Encoding = if bad > 0 then 932 else 65001],
    Buf = Binary.Buffer(File.Contents("C:\data\sample.csv")),
    Info = Detect(Buf),
    Result = Csv.Document(Buf, [Delimiter = ",", Encoding = Info[Encoding], QuoteStyle = QuoteStyle.None])
in
    Result

Kindは、判定の確かさを示す目印です。NotUtf8はShift_JISと判断できます。ValidUtf8は、UTF-8として読めたという意味で、Shift_JISでないとは言い切れません。

関数の仕様は、Microsoft LearnのText.FromBinaryで確認できます。

5種類のファイルで試した結果

同じ内容のCSVを、5種類作って判定しました。ReadOkは、判定した文字コードで読んだとき、見出しが正しく読めたかどうかです。

5種類のCSVに判定関数を適用した結果の表。Shift_JISだけNotUtf8で932、BOMありとなしのUTF-8は65001、半角カナのShift_JISは65001と判定されReadOkがFALSE
再現用の最小コードの実行結果
ファイルBOMだけで判定置換文字(UTF-8側)で判定
Shift_JIS正しく読めた正しく読めた
BOMありUTF-8正しく読めた正しく読めた
BOMなしUTF-8文字化け正しく読めた
英数字だけ正しく読めた正しく読めた(どちらで読んでも同じ)
半角カナのShift_JIS正しく読めた文字化け

BOMだけの判定は、同じ種類のファイルで別に確認した結果です。

BOMなしのUTF-8は、置換文字を使うと正しく読めました。以前の記事で挙げた限界は、この点で補えています。

一方で、半角カナのShift_JISは、置換文字を使うと文字化けしました。バイト列がUTF-8として読めてしまい、置換文字が出ないためです。BOMだけの判定では、このファイルは正しく読めました。

よくある失敗

1. 両方の置換文字を数えて比べる

Shift_JIS側が常に0個のため、比較になりません。数えるのはUTF-8側だけです。

2. 同数のときに、黙って決める

両方0個のときに、先頭の候補を採ると、誤っていても気づけません。今回の半角カナがその例です。

3. BOMだけで決める

BOMなしのUTF-8を、Shift_JISと決めてしまいます。置換文字を併用すると防げます。

4. 判定の確かさを確認しない

ValidUtf8のファイルは、半角カナのShift_JISが混ざっていても区別できません。取込後に、文字を目で確認する運用が安全です。

応用例

Detectはファイル単位で呼び出す関数です。フォルダ内の複数のCSVに適用して、Kindの一覧を見る使い方ができます(この使い方は未検証です)。

Shift_JISで表せない文字の扱いは、「【VBA】全角氏名のバイト数チェックはLenBとStrConvで行う」で整理しています。

同じ年月のCSVが版を変えて溜まっているときに、最新の版だけを読む方法は、【Power Query】年月ごとにCSVの最新版だけを採用するで扱っています。

BOM付きUTF-8のCSVを書き出す側は、【VBA】BOM付きUTF-8のCSV書き出しと、URLパスの検知で扱っています。

まとめ

  • UTF-8側の置換文字だけが、判定に使える
  • Shift_JIS側は、置換文字が出ないため数えられない
  • BOMなしUTF-8は補えたが、半角カナのShift_JISは判定できない
  • 判定の確かさを示す目印(Kind)を残し、取込後に確認する

未検証の項目は、EUC-JP・UTF-16・他のロケール・Mac版・大きなファイルでの速度です。

コメント

タイトルとURLをコピーしました