プログラマのための IT 教科書

文字コードと改行コード

この部の 2 / 12 章 ・ 全体で 23 / 76 章 ・ 読了目安 40 分

この章を読むとできるようになること
  • 文字化けの原因を層で切り分けられる
  • 正規化と改行コードをチームで統一できる
  • CSV と BOM の扱いを仕様として決められる

ビットとバイトで「文字化けは、保存した時と読んだ時でルールが違うから起きる」と書きました。 この章では、その中身を詳しく見ます。

なぜこんなに面倒なのか。歴史を知ると、全部つながります。

なぜ文字コードは複数あるのか

1963年、ASCII が生まれました。 英数字と記号だけの128種類、1文字1バイト。 当時のコンピュータにとって、これで十分でした。

しかし世界には英語以外の言語があります。 各国は残りの領域(128〜255)や、複数バイトの独自方式で自国の文字を表しました。

日本:   JIS / Shift_JIS / EUC-JP / ISO-2022-JP  ← 日本だけで4種類
中国:   GB2312, Big5
韓国:   EUC-KR
欧州:   ISO-8859-1 〜 16

それぞれ互換性がありません。 同じバイト列が、方式によって別の文字になります。 これが文字化けの正体です。

「世界中の文字に、1つずつ番号を振ろう」—— この発想で1980年代末に始まったのが Unicode です。

つまり、文字コードの歴史は「バラバラだったものを統一する過程」 であり、 今はまだその移行期の後半にいます。だから古い方式に出会い続けます。

Unicode — 番号と、バイトへの変換は別

ここが最も誤解されるところです。

Unicode         「あ」に U+3042 という番号を振る(コードポイント)
符号化方式      その番号を、実際に何バイトでどう表すか

Unicode は「番号を決めただけ」 で、バイト列にする方法は別に複数あります。

符号化方式特徴
UTF-8可変長(1〜4バイト)。ASCII と完全互換。Web の事実上の標準
UTF-16主に2バイト(一部4バイト)。Windows / Java / JavaScript の内部表現
UTF-32常に4バイト。扱いは楽だがサイズが4倍

UTF-8 が勝った理由

「A」  ASCII: 41        UTF-8: 41        ← 同じ
「あ」 ASCII: 表せない   UTF-8: E3 81 82

既存の ASCII のファイルが、そのまま UTF-8 として正しく読めます。 英語圏のシステムを一切変えずに、多言語対応ができました。

さらに、バイトの並びを見るだけで文字の境界が分かる設計になっており、 途中から読んでも復帰できます。

サロゲートペア

UTF-16 では、基本的な文字は2バイトで表せますが、 絵文字など一部の文字は2バイトに収まりません。 そこで、2つの値を組にして1文字を表します。これがサロゲートペアです。

'あ'.length      // 1
'𩸽'.length      // 2   ← 1文字なのに2
'😀'.length      // 2

JavaScript の length が「見た目の文字数」と合わないのは、 文字列を UTF-16 として扱っているからです(見つけにくいバグ)。

[...'😀'].length              // 1  ← コードポイント単位で分割される
Array.from('👨‍👩‍👧').length     // 5  ← 結合文字は、まだ分かれる
「文字数」は3通りある
バイト数          UTF-8 で何バイトか
コードポイント数  Unicode の番号がいくつか
書記素クラスタ数  人間が見て何文字か       ← 本当に欲しいのはたいていこれ

「名前は20文字まで」という仕様は、どれで数えるかを決めないと実装できません。

□ DB のカラム長      → バイト数のことが多い
□ 画面の表示幅       → 全角・半角でも変わる
□ 利用者の感覚       → 書記素クラスタ

仕様として明記してください。 決めないと、後で必ず揉めます。

正規化 — 見た目が同じでもバイトが違う

「が」= U+304C                (1つの文字)
「が」= U+304B + U+3099        (か + 濁点)

どちらも画面では同じに見えますが、バイト列は違います。

NFC   合成する(「が」を1文字にまとめる)  ← 一般にこちらを使う
NFD   分解する(「か」+「濁点」に分ける)
□ 検索でヒットしない
□ 重複チェックをすり抜ける
□ ログイン ID が一致しない
macOS のファイル名は NFD

macOS のファイルシステムは、ファイル名を分解形(NFD に近い形)で保存します。

□ macOS で作った「が.txt」を Linux に持っていくと、名前が違う扱いになる
□ zip で固めて Windows で開くと、濁点が分離して見えることがある
□ git でファイル名が変わったように見える

入口で NFC に正規化するのが実務での定石です。

str.normalize('NFC')

日本語で特に注意すること

Shift_JIS の 0x5C 問題

日本のシステムで最も有名な罠です。

ASCII では 0x5C = \(バックスラッシュ)
日本語環境では 0x5C = ¥(円記号)として表示されてきた

さらに悪いことに、Shift_JIS では一部の漢字の2バイト目が 0x5C になります。

「表」「能」「十」「予」「暴」…(俗に「ダメ文字」と呼ばれる)
□ プログラムが 0x5C をエスケープ文字と解釈し、次の文字を食べてしまう
□ SQL やシェルに渡した時に構文が壊れる

この問題があるため、Shift_JIS でプログラムを書くのは避けます。 今は UTF-8 が標準ですが、古いシステムとの連携では今も出会います。

機種依存文字・環境依存文字

①②③  ㍿  ㈱  Ⅲ  ~(波ダッシュ)

Unicode には入っていますが、変換の過程で化けることがあります。

有名なのが 波ダッシュ問題です。 ~(U+301C 波ダッシュ)と 〜(U+FF5E 全角チルダ)は別の文字で、 Windows と他の環境で扱いが分かれてきました。

□ 住所や氏名のデータ移行で、この2つが混在する
□ 検索が一致しない

半角カナ

アイウ  ← 半角カナ

古いシステム(銀行の全銀フォーマットなど)では今も現役です。

□ 半角と全角の変換規則を決める
□ 濁点の扱い(ガ は2文字)に注意する

MySQL の utf8 は UTF-8 ではない

ビットとバイトでも触れましたが、重要なので繰り返します。

MySQL の utf8      → 最大3バイト。絵文字(4バイト)が入らない
MySQL の utf8mb4   → 本来の UTF-8

必ず utf8mb4 を使ってください。 「絵文字を入れたらエラー」の原因は、ほぼこれです。

改行コード

もう1つの、OS 間で揉める問題です。

表記バイト使う OS
LF0AUnix / Linux / macOS(現在)
CRLF0D 0AWindows
CR0D古い Mac(〜Mac OS 9)
なぜ2文字なのか(CRLF の由来)

タイプライターの時代、改行には2つの動作が必要でした。

CR(Carriage Return)  印字位置を行の左端に戻す
LF(Line Feed)        紙を1行送る

初期のコンピュータは、この制御をそのまま引き継ぎました。 Windows はこの慣習を維持し、Unix は「改行は1文字でよい」として LF だけにしました。

どちらが正しいという話ではなく、単に選択が分かれたまま今に至っています。

実務で起きる問題

□ シェルスクリプトが CRLF で保存され、実行すると謎のエラーが出る
   → `#!/bin/bash^M` が「そんなコマンドは無い」になる
□ git の差分が全行になる(実際は改行コードだけが変わっている)
□ 設定ファイルの末尾に見えない ^M が入り、値が一致しない
Windows で開発するチームがあるなら、最初に決める
# .gitattributes を置くのが最も確実(リポジトリ全体に効く)
* text=auto eol=lf
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
□ .gitattributes をリポジトリに置く      ← 推奨
□ エディタ(.editorconfig)でも揃える
□ core.autocrlf は人ごとの設定なので、これだけに頼らない

「自分の環境では動く」の原因として、見つけにくいバグに挙げたのと同じ構図です。

OS による違いのまとめ

項目WindowsmacOSLinux
改行CRLFLFLF
パス区切り\//
ファイル名の大小区別しない既定では区別しない区別する
ファイル名の正規化NFC 寄りNFD 寄りそのまま
既定の文字コード環境により Shift_JIS のこともUTF-8UTF-8

この表の差が、そのまま「自分の環境では動く」問題になります(開発環境を作る・見つけにくいバグ)。

CSV の落とし穴

実務で最も頻繁に文字コードで揉めるのが CSV です。

□ Excel は、環境によって CSV を Shift_JIS として開こうとする
□ UTF-8 で出力すると、Excel で開いた時に文字化けする
□ BOM 付き UTF-8 にすると Excel は正しく開くが、他のツールが BOM を読んでしまう
□ 業務でExcelに渡すなら、BOM 付き UTF-8 か Shift_JIS を検討する
□ システム間連携なら、BOM なし UTF-8 で統一する
□ **どちらなのかを、仕様として決めて明記する**
BOM(Byte Order Mark)

ファイルの先頭に付く「これは UTF-8 です」という印(EF BB BF)です。

□ Excel は BOM があると UTF-8 として開く      ← 便利
□ プログラムは BOM を「見えない1文字」として読んでしまうことがある
   → JSON のパースに失敗する
   → 設定ファイルの最初のキーが一致しない
   → シェルスクリプトが動かない

「原因不明のパースエラー」で、先頭に見えない3バイトがある—— 実際によくあります。head -c 3 file | xxd で確認できます。

実務での原則

1. **入口で UTF-8 に統一する**(外部から受け取ったら、すぐ変換)
2. **内部は UTF-8 のまま扱う**(途中で変換しない)
3. **出口で相手の要求に合わせる**(Excel なら BOM 付き、など)
4. **正規化(NFC)も入口で揃える**
5. どの箇所でどの符号化を使うかを、**仕様として書く**

そして、文字化けを見つけた時はビットとバイトのとおり、 「化けている場所」ではなく「化け始めた場所」を探します。

□ DB に正しく入っているか(直接見る)
□ API のレスポンスヘッダの charset は何か
□ HTML の meta charset は何か
□ ファイルの実際のバイト列はどうなっているか(xxd / hexdump)

Linux 上でシェルスクリプトを実行したら `/bin/bash^M: bad interpreter` というエラーが出ました。原因は?

実務の落とし穴まとめ

  1. length を文字数として使う — 絵文字・結合文字でずれる
  2. 正規化を揃えない — 見た目が同じでも一致しない。macOS は NFD
  3. MySQL の utf8 — 3バイトまで。utf8mb4 を使う
  4. Shift_JIS でプログラムを書く — 0x5C 問題
  5. 波ダッシュと全角チルダ — データ移行で混在する
  6. 改行コードを決めていない — git の差分が全行になる、スクリプトが動かない
  7. BOM を意識しない — JSON や設定ファイルのパースが謎に失敗する
  8. CSV の文字コードを仕様に書かない — Excel で開くたびに文字化けする
  9. 途中で何度も変換する — どこで化けたか追えなくなる

まとめ

  • 文字コードが複数あるのは、ASCII では足りず、各国が独自に拡張した歴史のため
  • Unicode は番号を決めただけ。バイトにする方法(UTF-8 / UTF-16)は別
  • UTF-8 は ASCII と完全互換だから普及した
  • 「文字数」はバイト / コードポイント / 書記素の3通り。仕様で決める
  • 正規化(NFC)を入口で揃える。macOS は NFD で保存する
  • 日本語特有の罠:Shift_JIS の 0x5C、波ダッシュ、半角カナ、utf8mb4
  • 改行は LF(Unix)と CRLF(Windows)。.gitattributes で統一する
  • BOM は Excel には便利、プログラムには害になることがある
  • 原則は 入口で UTF-8 に統一、内部は変換しない、出口で合わせる

公式ドキュメント・参考資料

対象リンク
Unicode Consortiumhttps://home.unicode.org/
Unicode 正規化(UAX #15)https://unicode.org/reports/tr15/
MDN: 文字エンコーディングhttps://developer.mozilla.org/ja/docs/Glossary/Character_encoding
Git の改行コード設定https://git-scm.com/docs/gitattributes
MySQL の文字セットhttps://dev.mysql.com/doc/refman/8.0/ja/charset.html

次の章では、これらを実際に動かしている側—— プロセスとメモリ、そして OS がプログラムをどう扱っているかを見ていきます。

読み終わったら記録しておくと、目次で進み具合が分かります。