文字コードと改行コード
この部の 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 // 2JavaScript の length が「見た目の文字数」と合わないのは、
文字列を UTF-16 として扱っているからです(見つけにくいバグ)。
[...'😀'].length // 1 ← コードポイント単位で分割される
Array.from('👨👩👧').length // 5 ← 結合文字は、まだ分かれるバイト数 UTF-8 で何バイトか
コードポイント数 Unicode の番号がいくつか
書記素クラスタ数 人間が見て何文字か ← 本当に欲しいのはたいていこれ
「名前は20文字まで」という仕様は、どれで数えるかを決めないと実装できません。
□ DB のカラム長 → バイト数のことが多い
□ 画面の表示幅 → 全角・半角でも変わる
□ 利用者の感覚 → 書記素クラスタ
仕様として明記してください。 決めないと、後で必ず揉めます。
正規化 — 見た目が同じでもバイトが違う
「が」= U+304C (1つの文字)
「が」= U+304B + U+3099 (か + 濁点)
どちらも画面では同じに見えますが、バイト列は違います。
NFC 合成する(「が」を1文字にまとめる) ← 一般にこちらを使う
NFD 分解する(「か」+「濁点」に分ける)
□ 検索でヒットしない
□ 重複チェックをすり抜ける
□ ログイン ID が一致しない
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 |
|---|---|---|
| LF | 0A | Unix / Linux / macOS(現在) |
| CRLF | 0D 0A | Windows |
| CR | 0D | 古い Mac(〜Mac OS 9) |
タイプライターの時代、改行には2つの動作が必要でした。
CR(Carriage Return) 印字位置を行の左端に戻す
LF(Line Feed) 紙を1行送る
初期のコンピュータは、この制御をそのまま引き継ぎました。 Windows はこの慣習を維持し、Unix は「改行は1文字でよい」として LF だけにしました。
どちらが正しいという話ではなく、単に選択が分かれたまま今に至っています。
実務で起きる問題
□ シェルスクリプトが CRLF で保存され、実行すると謎のエラーが出る
→ `#!/bin/bash^M` が「そんなコマンドは無い」になる
□ git の差分が全行になる(実際は改行コードだけが変わっている)
□ 設定ファイルの末尾に見えない ^M が入り、値が一致しない
# .gitattributes を置くのが最も確実(リポジトリ全体に効く)
* text=auto eol=lf
*.sh text eol=lf
*.bat text eol=crlf
*.png binary□ .gitattributes をリポジトリに置く ← 推奨
□ エディタ(.editorconfig)でも揃える
□ core.autocrlf は人ごとの設定なので、これだけに頼らない
「自分の環境では動く」の原因として、見つけにくいバグに挙げたのと同じ構図です。
OS による違いのまとめ
| 項目 | Windows | macOS | Linux |
|---|---|---|---|
| 改行 | CRLF | LF | LF |
| パス区切り | \ | / | / |
| ファイル名の大小 | 区別しない | 既定では区別しない | 区別する |
| ファイル名の正規化 | NFC 寄り | NFD 寄り | そのまま |
| 既定の文字コード | 環境により Shift_JIS のことも | UTF-8 | UTF-8 |
この表の差が、そのまま「自分の環境では動く」問題になります(開発環境を作る・見つけにくいバグ)。
CSV の落とし穴
実務で最も頻繁に文字コードで揉めるのが CSV です。
□ Excel は、環境によって CSV を Shift_JIS として開こうとする
□ UTF-8 で出力すると、Excel で開いた時に文字化けする
□ BOM 付き UTF-8 にすると Excel は正しく開くが、他のツールが BOM を読んでしまう
□ 業務でExcelに渡すなら、BOM 付き UTF-8 か Shift_JIS を検討する
□ システム間連携なら、BOM なし UTF-8 で統一する
□ **どちらなのかを、仕様として決めて明記する**
ファイルの先頭に付く「これは 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` というエラーが出ました。原因は?
実務の落とし穴まとめ
lengthを文字数として使う — 絵文字・結合文字でずれる- 正規化を揃えない — 見た目が同じでも一致しない。macOS は NFD
- MySQL の
utf8— 3バイトまで。utf8mb4を使う - Shift_JIS でプログラムを書く — 0x5C 問題
- 波ダッシュと全角チルダ — データ移行で混在する
- 改行コードを決めていない — git の差分が全行になる、スクリプトが動かない
- BOM を意識しない — JSON や設定ファイルのパースが謎に失敗する
- CSV の文字コードを仕様に書かない — Excel で開くたびに文字化けする
- 途中で何度も変換する — どこで化けたか追えなくなる
まとめ
- 文字コードが複数あるのは、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 Consortium | https://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 がプログラムをどう扱っているかを見ていきます。