コードブロックで文字化けした話——Unicodeの沼にハマった30分
正規表現のUnicode範囲指定に使った稀少文字がブログ上で豆腐(□)になる現象と、\\uXXXX エスケープ表記による解決法
登場人物
- リナ社長 … 高校生なのに会社経営するやり手ギャル。テックにも強くてAI活用が得意。
- タクヤ … 入社3年目の男性社員。真面目で少しだけコードが書ける。
タクヤ「社長、自分たちの記事のコードブロック、なんか文字化けしてる部分があります」
リナ社長「え、どこ?」
タクヤ「読了時間の計算ロジックを紹介してる部分なんですけど、正規表現の中に□が混じってて」
リナ社長「あー、これ豆腐じゃん。ちゃんと表示されてない文字だ。ちょっと調べよ」
何が起きたのか
問題の正規表現はこれだ。日本語文字を検出するためのもの。
// 修正前のコード
const japanese = (body.match(/[ -〿-ヿ㐀-䶿一-鿿豈-]/g) || []).length;
見た目は普通の正規表現だが、ブラウザで表示すると一部が□(豆腐)になる。
リナ社長「U+3400 とか U+4DBF って文字のコード、見たことなくない?」
タクヤ「…確かに。日常で使う漢字じゃないですね」
原因:CJK Extension A の稀少文字
Unicodeには日本語・中国語・韓国語の文字を収録した範囲がいくつかある。
| Unicode範囲 | 内容 | 代表文字 |
|---|---|---|
| U+3000–U+303F | CJK記号・句読点 | (全角スペース)など |
| U+3040–U+30FF | ひらがな・カタカナ | あ、ア など |
| U+4E00–U+9FFF | CJK統合漢字 | 一、国、日 など |
| U+3400–U+4DBF | CJK Extension A | 㐀、䶿 など |
| U+F900–U+FAFF | CJK互換漢字 | 豈、 など |
問題の文字は CJK Extension A(U+3400–U+4DBF)の境界にある \u3400(U+3400)と 䶿(U+4DBF)だ。
リナ社長「CJK Extension Aって何?」
タクヤ「中国の古典テキストとかに使われる、超マイナーな漢字の集まりらしいです。日常では絶対使わない文字で」
リナ社長「そりゃフォントに入ってないわ」
そう。Noto Sansのような汎用フォントでも、CJK Extension Aの境界文字は収録されていないことがある。結果、レンダリングできずに□(豆腐)として表示される。
解決策:\uXXXX エスケープ表記
文字の代わりに Unicode番号をそのまま書くのが正解。
// 修正後のコード
const japanese = (body.match(/[\u3000-\u303F\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/g) || []).length;
\u3400 は「U+3400 の文字」を意味するJavaScript/TypeScriptの正規表現エスケープ。実際の文字を埋め込む代わりに番号で指定するので、フォントの有無に関係なく表示が崩れない。
タクヤ「書いてるファイルに実際の文字が入ってたのが原因だったんですね」
リナ社長「そう。ソースコードに稀少な文字を直接入れると、エディタやブログで表示できない環境が出てくる。エスケープ表記にしとけば純粋なASCIIだから絶対大丈夫」
今回の教訓
正規表現でUnicode範囲を指定するときは、実際の文字をコピペしない。
| NG | OK |
|---|---|
/[㐀-䶿]/g | /[\u3400-\u4DBF]/g |
/[豈-]/g | /[\uF900-\uFAFF]/g |
リナ社長「というか、コード書くときにCJK Extension Aの境界文字をコピペしようとする状況がそもそも特殊すぎるけどね」
タクヤ「確かに。でも読了時間の計算ロジックはそういうレアなコードなので……」
リナ社長「まあ直ったからよし。こういう細かいところを潰していくのが大事よ」
コメント