あ、この文字、なんで入ってるの? (1)
この記事は、FEConf2024で発表された<あれ、この文字、なんで入ってるの? (副題: 知っておくとたまに役立つハングルUnicode完全マスター)>の内容をまとめたものです。発表内容を2回に分けてお届けします。第1回ではUnicodeの概要と、その中でのハングルの扱いについて解説します。第2回では、置換文字()が表示される原因と、その解決策について探ります。本文に挿入されている画像の出典は、すべて同名の発表資料であり、個別の出典表記は省略しています。
「あれ、この文字、なんで入ってるの? (副題: 知っておくとたまに役立つハングルUnicode完全マスター)」
チェ・ハンジェ、Denier CTO
こんにちは。「あれ、この文字、なんで入ってるの?」というテーマで発表させていただくチェ・ハンジェです。以前はNAVER OfficeでWordやSlideなどの製品開発を担当し、現在は歯科医師向けコミュニティを運営するDenierでCTOとして、医療従事者のためのプラットフォーム開発に携わっています。
今回は、皆さんがいつか遭遇するかもしれないこの問題をスムーズに解決し、技術面接で関連する質問を受けた際に的確に答えられるよう、内容を構成しました。ぜひ参考にしてください。
この記事で扱う内容は以下の通りです。
- Unicodeにおけるハングル音節の仕組み
- EUC-KR/CP949の文字エンコーディングとその限界
- 身近なUTF-8とUCS-2
- 「組み合わせ文字」と「完成形文字」の理解
- ハングルの文字化けトラブルシューティングとUTF文字の解読事例
Unicode
Unicode地獄
今回の発表は、下の画像から始まります。一緒に見てみましょう。以前一緒に働いていたマネージャーから、こんなSlackメッセージが届きました。
「テキストエディタで長文を作成していると、おかしなクエスチョンマークの文字が何度も出てくるんです。」
「この文字を消すために修正して再アップロードすると、また別の場所に現れるんです。まるでモグラ叩きみたいです!」
このメッセージを送ってきたマネージャーは、この現象を「Unicode地獄」と表現しました。上の画像の右下に写っているのが、実際にそのマネージャーが体験した文字化けしたテキストです。では、この置換文字()がなぜ登場するのか、その問題を解決するための旅に出かけましょう。
Unicodeとは?
まず、Unicodeについて見ていきましょう。UnicodeはISO 10646という規格です。言い換えれば、Unicodeとは世界中のすべての文字を表現するための一貫した規格であり、同時にこれらの文字を管理する機関でもあります。このUnicodeには、いくつかの大きなブロックに分かれています。
最初の領域は、基本多言語面(BMP: Basic Multilingual Plane)と呼ばれます。このプレーンでは、U+0000からU+FFFFまでのコードポイントが使用されます。これはUnicodeで一般的に用いられる表記法で、U+
に続く16進数が特定の文字を指すコードポイントを表します。数字だけではどの規格のコードか分かりにくいため、慣習的にU+
というプレフィックスが付けられています。
2番目のSMP(Supplementary Multilingual Plane)は補助多言語面と呼ばれ、普段私たちが使う絵文字などがここに含まれます。最後の3番目の領域は補助漢字面(Supplementary Ideographic Plane)と呼ばれ、主に漢字系の文字が割り当てられています。
上の図は、最初の領域である基本多言語面です。図のように、基本多言語面には非常に多くの文字が含まれています。一つのマスには256文字が割り当てられています。00から02の行にはローマ字があり、青色で示されたヨーロッパ言語の文字などを経て、34番の行からはCJK統合漢字が見られます。CJKはChinese, Japanese, Koreanの略で、日中韓で共通して使われる漢字のブロックです。私たちが使うハングルは東アジア文字領域にあり、図では赤色で示されています。この赤色で示された東アジア文字には、11番、30番、31番の行や、A0からD7までの範囲が含まれます。
このBMP領域はU+0000からU+FFFFまで、合計4つの16進数で表現されるため2バイトの表現範囲を持ち、合計65,536文字が含まれています。Unicodeでは、世界中のほとんどの文字がこの約65,000文字の範囲に含まれると説明しています。
Unicodeにおけるハングル
では、私たちが表現するハングルは、正確にどこに含まれているのでしょうか?先ほど述べたように、東アジア文字セットに含まれており、その中でもACで始まる部分から含まれています。下の図は、Unicode内にあるハングルの一覧の一部です。
「가(カ)」から「힣(ヒッ)」までを表現するU+AC00〜U+D7A3のコード範囲に、私たちが使う現代ハングルのすべてが割り当てられています。ハングル文字を判定する正規表現で [가-힣]
と書くことがありますが、まさにその範囲を指しています。
では、この範囲には何文字のハングルが含まれているのでしょうか?計算してみましょう。初声19個、中声21個、そして終声27個に「終声なし」の1個を加えた28個をすべて掛け合わせると、19 × 21 × 28 = 11,172文字となります。この11,172文字をすべて表現できるかどうかが、そのエンコーディングが完全なハングルをサポートしているかを判断する基準になります。
そして、ここに含まれていないハングルもあります。下の図のように、「ㆍ(アレア)」を含む昔のハングルや、初声がない文字、中声がない文字などはUnicodeに含まれていません。
ハングルのソート順
ハングルのソート順についても見ていきましょう。「가나다라마바사」から「아자차카타파하」までのソート順が定められたのは、そう昔のことではありません。二重子音や複合母音をどの位置に含めるかという議論を経て、ハングルの電子化が進められていた1988年、当時の文教部(現在の教育部)の告示によって公式に確定されました。この時に定められた順序が、現在のUnicodeにも反映されています。
さらに調べてみると、この順序に関する提案は何度かあり、80年代以前には二重子音が後ろに来るなど、異なる順序を持つ辞書が時々ありました。このように様々な議論を経て定められた順序が、KS X 1026-1という規格に記録されています。
ここで少し、北朝鮮のハングルはどうか見てみましょう。北朝鮮で使われるソート順は、韓国のものとは少し異なります。下の図のように、二重子音が後ろに来て、子音「ㅇ」も一番最後に配置されています。そのため、北朝鮮の技術者が開発でハングルを扱う際には、Unicodeの標準のソート順とは異なる独自の実装をする必要があります。
1999年に北朝鮮が下の図のように修正を提案しましたが、Unicode委員会はこれを受け入れませんでした。なぜなら、既に88年に韓国がハングルのソート順を登録していたためです。
では、Unicodeにおいてハングルはどれくらいの割合を占めているのでしょうか?先ほど説明したBMP領域の65,536文字のうち11,172文字、実に6分の1を占めています。そのため、Unicodeコンソーシアム内部でも、これほど膨大なハングル文字をすべて収録すべきかという議論があったそうです。その結果、議論の末に一部の文字は収録が見送られました。
そのため、Unicode 1.0仕様には今より少ないハングルが記録されていました。1996年に出たUnicode 2.0仕様になって初めて、11,172文字すべてのハングルが記録されました。
組み合わせ型Unicode、Hangul Jamo
ハングルにはもう一つ、異なる表現方法があります。それが「組み合わせ文字」方式のUnicode、すなわちHangul Jamoです。この方式では、初声・中声・終声のパーツを組み合わせて一つの文字を表現します。
チョッカックッUnicode
「チョソンの『初』、チュンソンの『中』、チョンソンの『終』を取ってチョッカックッ」とも呼ばれるこの方式を使えば、例えば私の名前の「한」という文字は、「ㅎ(h)」「ㅏ(a)」「ㄴ(n)」というパーツの組み合わせで表現されます。この方法により、現代ハングル11,172文字はもちろん、古ハングルの文字も理論上は表現可能です。(もちろん、古ハングルを正しく表示するには、対応するフォントが別途必要です。)
下の図は、組み合わせ型テキストの例外的なケースを示しています。「t」という変数に「한재는 발표중!(ハンジェは発表中!)」という値を保存しました。この変数にsubstringを使って2文字だけを取り出すと、私たちは「한재(ハンジェ)」という文字が出てくることを期待するでしょう。しかし、実際には「하(ハ)」という文字が表示されました。
JavaScriptの文字列は、Unicodeの組み合わせ文字の構成に従い、内部的に初声、中声、終声のパーツが連続して並んでいます。そのため、substring
で先頭から2つの「文字(構成要素)」を取り出すと、意図した「한」ではなく、初声「ㅎ」と中声「ㅏ」が組み合わさった「하」が表示されてしまったのです。
身の回りのチョッカックッUnicode
このような組み合わせ型テキストであるチョッカックッUnicodeは、私たちの身の回りでも時々見かけられます。例えば、Macで作成されたファイルがその一例です。
Macで作成したハングルファイル名をWindowsで開くと、文字がバラバラに分解されて表示されることがあります。図のファイル名は、続けて読むと「개발자_매뉴얼(開発者マニュアル)」です。これは、macOSがファイル名にNFD(組み合わせ文字)形式のUnicodeを使用するのに対し、WindowsはNFC(完成形文字)形式を基本とするためです。互換性のない形式のため、Windowsでは1つの文字として認識されず、パーツごとに表示されてしまうのです。
では、ブラウザではチョッカックッUnicodeはうまくサポートされているのでしょうか?幸いなことに、ほとんどのモダンブラウザではうまくサポートされています。しかし、これが原因で問題が生じることもあります。ブラウザ上では正しく表示されていたハングルが、Windows OSにダウンロードして確認すると文字化けしているケースです。
チョッカックッUnicodeの入力:三伐式キーボード
チョッカックッUnicodeの特性を見ると、図のように初声に来る文字と終声に来る文字が同じであっても、実際には異なる文字として扱われます。
もちろん、私たちが普段使っているキーボード(二伐式)では、初声と終声を明確に区別して入力することはできません。しかし、それらを区別して入力できるキーボードがあります。それが三伐式(セボルシク)キーボードです。下の図のように、三伐式キーボードは初声、中声、終声のキーが分かれています。
時々、周りで三伐式キーボードが良いと言う人を見かけることがあります。そういった方々は、より速いタイピングのために三伐式キーボードを使っているそうです。
私たちが一般的に使う二伐式(ドゥボルシク)キーボードでは、しばしば次のようなタイプミスが起こります。「옷이 없어요(服がありません)」と打とうとして、急ぐあまり「옷이 ㅇ벗어요」と入力してしまうケースです。二伐式キーボードは初声と終声のキーが共用のため、入力のタイミングによってどちらの音として扱われるかが決まります。その結果、このような意図しない文字が入力されてしまうのです。
私たちの目には同じ文字に見えても、三伐式キーボードでは初声と終声を異なる文字として扱います。そのために生まれるユニークな利点でありテクニックもあります。それが「三伐式モアチギ(集め打ち)」です。「않」という文字を入力する際、三伐式キーボードは初声と終声の違いを認識しているため、「ㅏ」→「ㄴ」→「ㅎ」→「ㅇ」の順で入力しようが、最後の音を先に入力しようが、問題なく文字を表現できるのです。
結合方法:NFC / NFD
もちろん、ここまで説明してきたチョッカックッは理解しにくく、どのように結合して使うべきかはさらに難しいのが実情です。こうした困難を解消するために、Unicodeが紹介している結合方法があります。それがNFC(Normalization Form Canonical Composition、正規化形式C)とNFD(Normalization Form Canonical Decomposition、正規化形式D)です。
「Canonical(正準)」の辞書的な意味は「標準的な規範」です。Unicodeは、この概念に基づいて正準結合(Composition)と正準分解(Decomposition)を定義し、「このルールに従って文字を正規化しなさい」とガイドしています。
上の例の「(ㄱ)」は、正準分解を通じて「(」「ㄱ」「)」の3つの文字に分解できます。これは逆に、正準結合を通じて「(ㄱ)」という一文字に結合できます。同様に、「한」という文字も正準結合と正準分解を通じて分解と結合が可能です。
この規則はJavaScriptのString.prototype.normalize
に定義されているので、リンクを確認してみると良いでしょう。
ハングル互換字母(Hangul Compatibility Jamo)
最後に見ていくのは、ハングル互換字母です。この記事で登場する3番目のハングルです。
ハングル互換字母は、主に二伐式キーボードでの入力を想定した文字セットです。Hangul Jamo(組み合わせ文字)との決定的な違いは、初声と終声の区別がない点です。皆さんが二伐式キーボードで「ㄱ」を入力したとき、それはまさにこの「ハングル互換字母」の一つが入力されていることになります。
ここまで、Unicode、そしてUnicodeにおけるハングルについて見てきました。
これまでに見てきた内容を基に、次の記事では冒頭で述べた置換文字()がなぜ登場するのか、そしてこの問題を解決する方法について見ていきましょう。
Discussion