Shift_JISとCP932の違い — 波ダッシュ・0x5C・機種依存文字

Shift_JISとCP932は別の対応表です。Windows上のツールが「Shift_JIS」として書き出したファイルは、ほぼ確実にCP932(Windows-31Jとも呼ばれます)です。差が小さいので普通のテストは通ってしまい、それでいて差が具体的なので本番データを壊します。

結論だけ先に書くと、取り込み時に「CP932」と明示した変換器でデコードし、UTF-8で保持し、検証は以下に挙げる「壊れ得る文字」で往復させることです。この記事の数値はすべてPython 3.9、Node 24、macOS同梱のiconvで実行した結果です。

デコード結果が食い違うのはちょうど6組

2バイトの全組み合わせを、JIS X 0208の表を実装したPythonのshift_jisコーデックと、Microsoftの表を実装したcp932コーデックの両方でデコードすると、両方で有効かつ結果が違うバイト列は6組でした。

実務で当たるのは0x8160の波ダッシュです。日本語は範囲や概算に「〜」を使い、話数やレベル帯、価格帯に出てきます。0x817Cのマイナス記号と全角ハイフンマイナスはさらに厄介で、目ではほぼ区別できないのに文字列比較では常に別物です。

2つの表は入力方向でも正反対の壊れ方をします。CP932は黙って置き換え、往復しただけで文字の同一性が変わるのに例外はどこにも出ません。厳密なShift_JISは拒否するので、Windows由来のテキストを書き出すビルド工程はその場で落ちます。落ちる方がまだ安全で、黙って置き換わる方はプレイヤーに届きます。

下流で出る症状は、変換境界をまたぐと文字列の完全一致が成立しなくなることです。同じ範囲記号に見える2つが比較で不一致になり、翻訳メモリがヒットせず、重複検出が1つの文字列を2つとして扱います。表示は正しいのに挙動がおかしいときは、文字化けの原因と直し方に並ぶ他の失敗パターンも確認してください。

# デコード: 同じ2バイトが別の文字になる
バイト列  厳密なshift_jis          cp932(Microsoft)
8160     U+301C 〜 波ダッシュ       U+FF5E ~ 全角チルダ
8161     U+2016 ‖ 双柱            U+2225 ∥ 平行記号
817C     U+2212 − マイナス記号     U+FF0D - 全角ハイフンマイナス
8191     U+00A2 ¢ セント記号       U+FFE0 ¢ 全角セント記号
8192     U+00A3 £ ポンド記号       U+FFE1 £ 全角ポンド記号
81CA     U+00AC ¬ 否定記号         U+FFE2 ¬ 全角否定記号

# エンコード: 各コーデックが受け付けるもの
U+301C   shift_jis -> 8160        cp932 -> 8160(戻すとU+FF5E)
U+FF5E   shift_jis -> エラー       cp932 -> 8160
U+00A5   shift_jis -> 5C          cp932 -> エラー

CP932にしかない457字と、2か所に存在する396字

CP932はJIS X 0208が未定義のままにしたコードポイントを2つのブロックで埋めています。先頭バイト0x87の行にあるNEC特殊文字と、先頭バイト0xED・0xEEにあるIBM拡張文字です。外字領域を除くと、cp932ではデコードできて厳密なshift_jisではエラーになるバイト列が845組あり、異なる文字としては457字です。

これが「機種依存文字」です。丸囲み数字はランキング画面や番号付きの台詞に出ますし、「髙」「﨑」はスタッフロールに普通に並ぶ姓の漢字です。厳密なShift_JISのエンコーダはこれらを1字も書けないので、台本本体は変換できるのにスタッフロールの書き出しだけ失敗します。

同じ領域でより厄介なのが重複です。396字が2つ以上の異なるバイト列から到達できます(394字が2通り、¬と∵が3通り)。先頭バイト0xFA〜0xFCにある「NEC選定IBM拡張文字」が、既に別の場所にある文字を再び収録しているからです。

Pythonのcp932エンコーダは「髙」に0xEEE0を選び、macOS同梱のiconvにCP932を指定すると0xFBFCを選びました。どちらも正しいCP932で、表示も同一です。しかしバイト列は一致しないので、チェックサム、バイト単位の差分、コンテンツハッシュは、存在しない差分を報告します。CP932のファイルはUnicodeにデコードしてから比較してください。

文字                          shift_jis  cp932  shift_jisx0213
① U+2460 丸囲み数字の1         エラー      8740   8740
㈱ U+3231 株式会社の略号        エラー      878A   878A
№ U+2116 ナンバー記号          エラー      8782   8782
℡ U+2121 電話記号              エラー      8784   8784
Ⅰ U+2160 ローマ数字の1         エラー      8754   8754
髙 U+9AD9 はしご高             エラー      EEE0   エラー
﨑 U+FA11 たつさき             エラー      ED95   9892
纊 U+7E8A IBM拡張の先頭漢字    エラー      ED40   EDB5

0x5C問題 — 「表示」が「侮ヲ」に化ける仕組みと、化けない場合

CP932は1バイト文字と2バイト文字が混在するので、あるバイトの意味は直前のバイトに依存します。全探索で確かめると、0xA1〜0xDFは1バイトの半角カタカナでした。

2バイト目の範囲が0x40から始まるので、どのASCII文字が2バイト文字の内側に潜り込めるかは完全に確定します。生のCP932バイト列をカンマ・タブ・改行で分割するのは安全です。一方、パイプで分割する、0x5Cをエスケープの開始と見なす、これらは安全ではありません。printf形式の%sは安全です(パーセント記号は0x40未満)。波括弧形式は危険で、0x7Bと0x7Dはそれぞれ40字の2バイト目に現れます。英字とアンダースコアも2バイト目になり得るため(数字は不可)、snake_caseのキーのバイト単位走査も危険です。

2バイト目が0x5Cになる割り当て済みの文字は42字あり、どれも日常語です。ソ 0x835C、十 0x8F5C、申 0x905C、能 0x945C、表 0x955C、予 0x975C、構 0x8D5C、圭 0x8C5C、貼 0x935C、それに水平バー(U+2015)の0x815C。C言語風のエスケープ処理に通した結果は末尾の表のとおりです。

1行だけ無変化で返っているのが、このバグがテストをすり抜ける理由です。文字列の末尾にある「表」は0x5Cの次のバイトが存在しないので、同じ文字が位置によって壊れたり壊れなかったりします(末尾に孤立したバックスラッシュを残す実装に限ります)。

円記号も同じバイトの話です。Pythonの厳密なshift_jisはU+00A5を1バイトの0x5Cにエンコードしますが、0x5CをデコードするとU+005Cが返るので、この対応づけは意図的に一方通行です。cp932はU+00A5のエンコードを拒否します。1つのバイトに2つの字形があり、日本語ロケールのフォントで描けば円記号、それ以外ではバックスラッシュです。表示側はWindowsのシステムロケールと日本語の文字化けにまとめてあります。

2バイト目は必ず0x40〜0x7Eか0x80〜0xFCなので:

2バイト目になり得る — バイト単位の走査は危険:
    @  A-Z  [  \  ]  ^  _  `  a-z  {  |  }  ~

2バイト目になり得ない — バイト単位の走査は安全:
    TAB  CR  LF  空白  !  "  #  $  %  &  '  (  )  *  +  ,  -  .  /
    0-9  :  ;  <  =  >  ?

バックスラッシュ+1文字をその1文字に置き換える処理を実CP932バイト列に適用:
表示      955C8EA6          ->  侮ヲ
ソート     835C815B8367      ->  メ[ト
十分      8F5C95AA          ->  助ェ
能力      945C97CD          ->  迫ヘ
構造      8D5C91A2          ->  国「
データ表    8366815B835E955C  ->  データ表   (変化なし)

どの名前がどの表を指すのか — iconv・Node・Python・Excel

落とし穴は、「Shift_JIS」という文字列を受け取ってMicrosoftの表を返す道具があることです。0x8160をデコードさせれば、どの表かは1コマンドで判定できます。U+301CならJISの表、U+FF5EならMicrosoftの表です。

  • macOSのiconv: SHIFT_JIS、SJIS、MS_KANJI、SHIFT_JISX0213はすべてU+301Cを返し、CP932とWINDOWS-31JはU+FF5Eを返しました。名前に反して、ここでのMS_KANJIはJISの表です。
  • SHIFT_JISX0213はCP932の上位集合ではなく、代用にもなりません。髙は拒否し、収録している文字も位置が違うことがあります。纊は0xEDB5、﨑は0x9892で、CP932の0xED40・0xED95とは別です。
  • Node: TextDecoderにshift_jisというラベルを渡すとU+FF5Eを返し、①・㈱・髙・﨑もデコードできます。WHATWGのEncoding Standardがこのラベルの中身をMicrosoftの表として定義しているためです。cp932というラベルはRangeErrorになります。TextEncoderはUTF-8しか出力しないので、この環境はCP932を読めても書けません。
  • Java: Shift_JISとwindows-31jは別のcharsetです。必須のcharsetと任意のcharsetの区分はバージョンで変わってきたので、推測せず使用中のJDKの公式ドキュメントの対応エンコーディング一覧で確認してください。
  • 日本語WindowsのExcel: 単純なカンマ区切りCSVの保存はシステムのANSIコードページ、つまり日本語環境ではCP932で書き出されます。近年のバージョンはUTF-8のCSVを別項目として持ちます。保存形式の項目名はリリースによって違うので、公式ドキュメントで確認してください。

安全な変換手順と、やってはいけない確認

どの道具も間違ってはいません。重なり合う名前で別の表を文書化しているだけです。変換は境界で一方向に行います。

  • 取り込むファイルは、CP932またはWindows-31Jと明示した変換器でデコードする。厳密なShift_JISが読めるものすべてに加えて457字の拡張文字も読めるので、寛容な側が正解です。
  • すぐUTF-8に正規化し、CP932のバイト列を後段に持ち込まない。BOM(EF BB BF)を付けるのは、日本語Windowsの表計算ソフトがダブルクリックで開く必要があるときだけ。パーサーが読むファイルには付けない。付けると先頭の列名にU+FEFFがくっついた状態で届きます。
  • CP932が本当に必要な相手には、エンコードした直後にデコードし、元の文字列と1文字ずつ比較して、違うインデックスを報告させる。「表示ソート十分〜①㈱髙 100%」で実行すると、差は1か所だけ、インデックス7でU+301CがU+FF5Eになりました。
  • 思い込まず検出する。まずUTF-8としてデコードを試す。日本語のCP932テキストが正しいUTF-8になることはほぼありません。ただしこれは強い証拠であって証明ではなく(CP932とUTF-8の両方として読める2バイト列が990組あります)、短いフィールドでは表記されたエンコーディング名を判定材料として併用してください。
  • 例外が出なかったこと、バイト数が変わらなかったことを検証と見なさない。CP932の黙った置き換えは例外を出さず、長さも変えません。

すべてに共通する型

日本語に限らない教訓です。表記されたエンコーディング名は意図を述べているだけで、上位集合の差分こそ低頻度の破損が潜む場所です。信頼できる確認は、壊れ得る文字で実際に走らせた往復だけです。同じ考え方は二重エンコードされたUTF-8の直し方にも当てはまり、症状の一覧は文字化けパターン早見表にあります。これから日本語を追加するなら、最初からUTF-8で始めればこの種の問題はまるごと消えます。残りの作業は英語から日本語へのゲームローカライズで扱っています。

関連記事