Unicode
Unicode
概要(サマリー)
Unicodeとは、世界中の文字に対して「世界共通の番号」を1つずつ割り当てた、文字集合の国際規格である。
コンピュータは文字をそのまま扱えず、内部では数値として保存している。かつては国や言語ごとに別々の対応表が使われていたため、日本語の文書を海外のパソコンで開くと読めない、という事故が日常的に起きていた。そこで「世界中の文字を1つの巨大な番号簿にまとめてしまおう」という発想で作られたのがUnicodeである。
イメージとしては、全世界の文字を集めた巨大な辞書に、通し番号を振ったものだと考えるとよい。「あ」は12354番、「A」は65番、絵文字の「笑顔」も別の番号、というように、文字ごとに固有の番号が決まっている。
詳細解説
Unicodeとは何か(文字の共通番号簿)
Unicodeが割り当てる番号のことをコードポイントと呼ぶ。表記するときは U+ に続けて16進数で書くのが慣例である。
A→U+0041あ→U+3042漢→U+6F22
この番号は世界共通で、どの国のどのソフトで見ても変わらない。Unicodeが決めているのは、あくまで「どの文字に何番を与えるか」という対応関係だけである。
UnicodeとUTF-8の違い(ここが最重要)
初心者が最もつまずくのがこの区別である。Unicodeは「番号簿」であり、UTF-8は「その番号をどうやってバイト列にするかの方式」である。
- Unicode(文字集合): 「あ」は
U+3042という番号だと決める - UTF-8(符号化方式): その
U+3042をE3 81 82という3バイトで表現する - UTF-16(符号化方式): 同じ
U+3042を30 42という2バイトで表現する
つまり同じUnicodeの文字でも、符号化方式が違えば保存されるバイト列は変わる。住所(Unicode)は同じでも、それを封筒に書くときの書式(UTF-8 / UTF-16)が複数ある、と考えると分かりやすい。
「文字コードはUTF-8です」という言い方は現場で通じるが、厳密には「文字集合はUnicode、符号化方式はUTF-8」ということになる。詳しくは文字コードの項目も合わせて読むとよい。
コードポイントをコードで確認する
主要な言語には、文字とコードポイントを相互変換する機能が用意されている。
JavaScriptの場合は次のように書ける。
// 文字からコードポイントを取得する
console.log("あ".codePointAt(0).toString(16)); // "3042"
// コードポイントから文字を作る
console.log(String.fromCodePoint(0x3042)); // "あ"
Pythonでは ord() と chr() を使う。
print(hex(ord("あ"))) # '0x3042'
print(chr(0x3042)) # 'あ'
どちらも「文字 ↔ 番号」を行き来しているだけで、やっていることは同じである。
絵文字とサロゲートペア
Unicodeは当初、文字は約6万5千個(2バイト)に収まると想定していた。しかし漢字の異体字や絵文字が増え、それでは足りなくなった。
そのため、U+FFFF を超える文字は、UTF-16では2つの単位を組み合わせて表現する。これをサロゲートペアと呼ぶ。絵文字や一部の漢字がこれに該当する。
この仕組みが、次のような直感に反する挙動を生む。
console.log("あ".length); // 1
console.log("𠮟".length); // 2 ← 1文字なのに2と数えられる
JavaScriptの length はUTF-16の単位数を数えるため、サロゲートペアの文字は「2文字」と判定される。文字数を正しく数えたい場合は、次のように分割する必要がある。
console.log([..."𠮟"].length); // 1
見た目が同じでも別の文字であることがある
Unicodeには、見た目がほぼ同じでもコードポイントが異なる文字が存在する。
- 全角の
A(U+FF21)と半角のA(U+0041) - 「が」を1文字で表す
U+304Cと、「か」+濁点の2文字で表すU+304B U+3099
後者のように、同じ見た目を複数の表現で書けてしまう問題に対処するのが正規化(Normalization)である。検索やログイン処理で「見た目は同じなのに一致しない」というトラブルが起きたときは、これを疑うとよい。
const a = "が"; // U+304C
const b = "か" + "゙"; // U+304B U+3099
console.log(a === b); // false
console.log(a.normalize("NFC") === b.normalize("NFC")); // true
Unicodeでも文字化けは起きる
「Unicodeを使えば文字化けしない」と思われがちだが、そうではない。Unicodeはあくまで番号の決め方であり、保存したときの符号化方式と、読み込むときに想定した符号化方式が食い違えば文字化けする。
UTF-8で保存したファイルをShift_JISだと思って開けば、当然読めない文字列になる。これはUnicodeの欠陥ではなく、送り手と受け手の取り決めがずれているだけである。
すでに壊れてしまったテキストは、元の符号化方式を推定して読み直すことで復元できる場合がある。
AIコーディングとの関係
AIに文字列処理を書かせるとき、Unicode特有の落とし穴は明示的に伝えないと踏み抜かれやすい。特に文字数カウント、切り詰め、バリデーションは要注意である。
たとえば次のように条件を具体化するとよい。
入力された文字列を20文字で切り詰める関数を書いてください。
絵文字やサロゲートペアを含む場合でも、文字が途中で壊れないようにしてください。
文字数のカウントは書記素単位で行ってください。
「20文字で切って」とだけ指示すると、AIは slice(0, 20) のような実装を返しがちで、絵文字がちょうど境界に来ると文字が壊れる。
また、AIが生成したコードで日本語が化ける場合、原因はコードのロジックではなくファイルの保存時の符号化方式であることが多い。その場合は「このスクリプトはUTF-8で読み書きするようにしてください」と指定し、実際のファイルの符号化方式も確認する。
生成されたコードを受け取ったら、日本語・絵文字・全角記号を含むテストデータで必ず動作確認する。半角英数字だけのテストでは、この種の不具合は表面化しない。
よくある勘違い
UnicodeとUTF-8は同じもの?
違う。Unicodeは「どの文字に何番を割り当てるか」という文字集合の規格であり、UTF-8は「その番号を実際に何バイトで表すか」という符号化方式である。UTF-8のほかにUTF-16やUTF-32もあり、いずれも同じUnicodeを別の方法でバイト列にしている。
Unicodeにすれば文字化けは起きない?
起きる。文字化けは「保存時の符号化方式」と「読み込み時に想定した符号化方式」の食い違いで発生するため、Unicodeを使っていても指定を誤れば化ける。重要なのは、送り手と受け手で符号化方式を一致させることである。
1文字は必ず1つの数値で表される?
そうとは限らない。絵文字のようにサロゲートペアで表される文字や、「か」+濁点のように複数のコードポイントを組み合わせて1つの見た目を作る文字がある。国旗の絵文字のように、さらに複数の文字を連結して1つに見せているものもある。
見た目が同じなら同じ文字?
違う。全角の A と半角の A、合成済みの「が」と分解された「が」は、見た目が近くてもコードポイントが異なる。文字列比較や検索で不一致が起きる原因になるため、必要に応じて正規化してから比較する。
まとめ
- Unicodeは、世界中の文字に共通の番号(コードポイント)を割り当てた文字集合の国際規格である
- Unicodeは「番号簿」、UTF-8やUTF-16は「その番号をバイト列にする方式」であり、役割が異なる
- 絵文字などはサロゲートペアで表されるため、プログラム上の文字数が見た目と一致しないことがある
- 見た目が同じでもコードポイントが異なる文字があり、比較の前に正規化が必要になる場合がある
- Unicodeを使っていても、符号化方式の指定を誤れば文字化けは起きる
情報ソース
より詳しくAIに聞いてみよう
- UnicodeとUTF-8の違いを、初心者にもわかるように具体例つきで説明してください。
- JavaScriptで絵文字を含む文字列を安全に指定文字数で切り詰める方法を教えてください。
- Unicodeの正規化(NFC・NFD)の違いと、どちらを使うべきかの判断基準を教えてください。
- 見た目が同じ文字なのに文字列比較が一致しない場合、何を疑えばよいか教えてください。
- Webフォームで入力された文字列を保存する際、文字コードまわりで注意すべき点を整理してください。