文字コード再入門 ─ Unicodeでのサロゲートペア、結合文字、正規化、書記素クラスタを理解しよう!

文字コードには、どのような種類があり、それぞれどのような意味を持つのか、といった、文字コードの基本的な概念、従来の文字コードを紹介し、現在のUnicodeの構成を概説し、プログラミングにおいて注意すべき箇所をいくつか取り上げます。

文字コード再入門 ─ Unicodeでのサロゲートペア、結合文字、正規化、書記素クラスタを理解しよう!

ソフトウェア開発に携わる方の多くは、何らかの形で文字コードに触れることがあるでしょう。文字や記号をコンピュータ上でデータとして扱うには、文字コードの知識が必要不可欠です。

本稿では、書籍『プログラマのための文字コード技術入門』の著者である矢野啓介さんが、知っておきたい基礎知識を分かりやすく解説します。

文字コードとは?

コンピュータで文字を扱うには、平仮名の「あ」やアルファベットの「A」といった文字ごとに一意な符号化表現(バイト列)を割り当てます。この割り当ての規則を文字コードと呼びます。ISOやJIS等の規格では、符号化文字集合(CCS、Coded Character Set)という用語が使われます。

歴史的な経緯から、英数字100文字程度の小さくシンプルなものから、世界中の文字を収めた大規模かつ複雑なものまで、さまざまな文字コードが世界各地で作られてきました。

Unicode以前の文字コード

ここでは、現在主流になっているUnicodeが普及する以前の主要な文字コードを、ごく簡単に紹介します。

最もシンプルな文字コードは、1文字を1バイトで表す1バイトコードです。国や地域ごとにバリエーションがあります。

ASCII
米国のANSI規格。0x21~0x7Eの範囲に英数字を収録する7ビットの1バイトコード
JIS X 0201
日本規格。ASCII類似の7ビットの1バイトコードであり、それに加えて8ビット(0xA1~0xFE)の範囲に片仮名のコードを用いることができる
ISO/IEC 8859-1
ASCIIに加えて、アクセント記号付きアルファベット等を収録した西欧諸言語向けの8ビットの1バイトコード。通称、Latin-1

また、東アジアでは、1文字を2バイトで表す2バイトコードも実用化されてきました。

JIS X 0208
日本の漢字、平仮名、片仮名等を収録。1978年初版。第1・第2水準漢字を含む6,879文字。コンピュータの日本語処理の実現に貢献
JIS X 0213
JIS X 0208に足りない文字を追加した拡張版。2000年初版。第3・第4水準漢字を含む、現代日本で使用実態の認められた11,233文字
GB 2312
中国規格。JIS X 0208と類似の構造で、簡体字の漢字を収録
KS X 1001
韓国規格。JIS X 0208と類似の構造で、ハングルと漢字を収録

上記の符号化文字集合を組み合わせて使う運用方式として以下があります。これらを文字符号化方式(CES、Character Encoding Scheme)と呼ぶことがあります。

EUC-JP
ASCIIとJIS X 0208を組み合わせたコード。7ビット部分(0x7F以下)はASCIIそのもので、8ビット部分(0xA1~0xFE)に2バイトコードを配置。Unixの日本語化で採用。中国や韓国でも、同様の構成でGB規格やKS規格を用いるEUC-CNやEUC-KRが採用された
ISO-2022-JP
ASCIIとJIS X 0208をエスケープシーケンスで切り替える7ビットのコード。インターネットメールで長年使用された
Shift_JIS
JIS X 0208を計算式によって変形した上で、JIS X 0201の隙間に詰め込んだコード。PCや携帯電話等で広く利用された

これら3つに対する拡張版が、JIS X 0213でそれぞれEUC-JIS-2004、ISO-2022-JP-2004、Shift_JIS-2004として用意されています。

このように多数の文字コードが世界各地で策定され使用されてきましたが、これでは国や地域ごとに文字コードを使い分ける必要があります。そこで、ひとつの統一的なコードで世界中の文字を扱えるように、Unicodeが開発されました。

Unicodeとその主な符号化形式

Unicodeは、ユニコードコンソーシアム(Unicode Consortium)という非営利法人によって公開されています。初版は1991年リリース。最新は13版(2020年3月10日)で、絵文字や記号も含む世界各言語の計143,859文字が収録されています。

1 The Unicode Standard

当初のUnicodeは、16ビット固定長のコードとして開発されました。現在では、整数値で表現される符号位置を、UTF-8・UTF-16・UTF-32といった符号化方式によってバイト列へと符号化する形態を取っています。符号位置とは、文字コード表のマス目の位置と思えばよいでしょう。

Unicodeには、16ビットすなわち65,536の符号位置を持つ面(plane)が17面あり、合計100万あまりの符号位置を持ちます。符号位置は「U+4E00」のように接頭辞「U+」を付けた4~6桁の16進数で表記します。

17面あるうち、最初の面00が基本多言語面(BMP、Basic Multilingual Plane)であり、日常的に用いる文字の大半がここに収められています。

Unicodeの主な符号化方式の概略を以下に記します。

UTF-16

UTF-16は、16ビットを1単位として用いる符号化方式です。BMPの文字については、符号位置の整数を16ビットで表したビット組合せがそのままUTF-16の値になります。

ただし、16ビットに収まらないBMP以外の面の文字は、サロゲートペアという仕組みを用いて表現する必要があります。BMPの中のD800からDBFFまでが上位サロゲート、DC00からDFFFまでが下位サロゲートとして用意されており、上位サロゲートと下位サロゲートの2つの16ビットの組み合わせによって、BMP外の1つの符号位置を表します。

また、16ビットの単位を8ビットのバイト列に直列化する際に、上位・下位どちらの8ビットを先にするかというバイト順の問題が生じます。上位を先にするビッグエンディアンはUTF-16BE、下位を先にするリトルエンディアンはUTF-16LEと呼ばれます。

バイト順を見分けるため、データの先頭にBOM(Byte Order Mark)という特殊な符号位置(U+FEFF)を置く方式が採られることがあります。バイトを逆順にしたU+FFFEには文字を割り当てないことが保証されているため、どちらのバイト順か見分けられます。

UTF-32

UTF-32は、32ビットを1単位として用いる符号化方式です。符号位置の整数を32ビットで表したビット組合せがそのままUTF-32の値になります。

全ての符号位置は32ビット以内で表せるので、UTF-16と異なり、サロゲートは必要ありません。ただし、UTF-16と同様にバイト順の問題はあるため、やはりBOMが用いられることがあります。

UTF-8

上記のUTF-16・32はバイト単位でASCIIと互換ではありませんが、UTF-8はASCII互換の符号化方式です。Unicode符号位置の範囲に応じて、1~4バイトの長さを取る可変長のコードです。

ASCIIと同等の範囲は、1バイトで表されます。アクセント付きアルファベット等は2バイト、漢字や平仮名等は3バイトになります。BMP外の符号位置は4バイトを要します。

UTF-16・32と異なり、バイト順の問題は存在しませんが、UTF-8の印としてファイル先頭にBOM(U+FEFF)が付けられることがあります。EF BB BFという3バイトです。

Webで文字コードを指定する仕組み

Webでは、主にUTF-8が用いられています。HTMLやCSS等の仕組みは特定のコード系に依存するものではありませんが、コード変換のトラブルを避けるため、新規に開発するものはファイルや入出力のコードをUTF-8にそろえておくと何かと便利であり、現在の慣例となっています。

HTML文書で文字コードを指定する

HTMLでは、meta要素によってそのHTML文書の文字コードを指定できます。UTF-8で符号化されたHTML文書では、head要素内に下記のように記します。

<meta charset=”UTF-8”>

なお、これはHTML5で導入された記述方法です。それまでのバージョンでは下記のように記していました。

<meta http-equiv=”Content-Type” content=”text/html;charset=UTF-8”>

HTTPのプロトコルで文字コードを指定する

HTTPのレスポンスヘッダで、サーバから送信されるHTML等のコンテンツの文字コードを指定できます。コンテンツの種類をContent-Typeフィールドで示しますが、これがテキストの場合は、オプションとしてcharsetパラメータを指定し、文字コードを示すことができます。

下記の例では、サーバから送信されるHTML文書(text/html)の文字コードがUTF-8であることを表しています。

Content-Type: text/html; charset=utf-8

パラメータcharsetに与える文字列は、IANA charset registryにて定義されています。シフトJISなら「Shift_JIS」、日本語EUCなら「EUC-JP」などとなります。大文字小文字は問いません。

このパラメータはHTMLコンテンツに限らず、プレーンテキスト(text/plain)等にも適用可能です。

Unicodeによるプログラミング上の注意点

Unicodeには、ASCIIやJIS X 0208とは異なる独特の特徴があり、プログラミング上で注意を要します。ここでは、そのいくつかを取り上げます。

サロゲートペア

JavaやJavaScript、C#のように文字列処理にUTF-16を意識しないといけない言語では、サロゲートペアの存在に注意する必要があります。本節ではJavaを例に取って説明します。

Javaでは文字列データは16ビット単位のchar型の連なりとして表現されます。BMP内の符号位置に対してはcharひとつが対応します。

一方、BMP外の符号位置に対しては、上位・下位のサロゲートがそれぞれひとつのchar値になり、上位下位を組み合わせて初めて1符号位置を表します。

例えば、文字列の最初の1文字を取得したくて、次のようにコーディングしたとします。

char firstCharacter  = str.charAt(0);

変数strの指す文字列の先頭が例えば「𩸽」(U+29E3D)という面02の文字だとすると、この符号位置を指すサロゲートペアD867 DE3Dの上位サロゲートだけを取得することになってしまい、文字として意味のある結果になりません。

そこで、StringクラスのcharAt()というchar単位のメソッドではなく、次のように符号位置単位で処理するメソッドを用います。

int firstCharacter = str.codePointAt(0);

これでサロゲートペアが泣き別れにならず、0x29E3Dという符号位置をint値で取得できます。

結合文字

Unicodeにはアクセント付きのアルファベットや、濁点・半濁点付きの仮名文字、あるいは構造の複雑なインドやタイ等の文字を表現するために、結合文字というものが用意されています。

これを使うと、複数の符号位置で1文字を表せます。例えば、アルファベット「e」に対してアクセント記号「´」を後置することで、アクセント付きの「é」を表現します。

このアクセント記号には、文字位置の前進を伴う通常の記号(U+00B4, ACUTE ACCENT)のほかに、合成用のものが別符号位置(U+0301, COMBINING ACUTE ACCENT)として用意されています。合成用の記号を使うと、その前のアルファベット(基底文字)と合わせて、1文字を表現できます。

したがって、1文字を取得するつもりで1符号位置を取得すると、アクセント記号とそれの付くアルファベットとが泣き別れになってしまうことがあるので注意が必要です。

アルファベットだけでなく、平仮名や片仮名でも濁点・半濁点の表現には結合文字を用いることができるので、日本語処理においても無縁ではありません。

正規化

上で見たように、アクセント付きのアルファベット等は結合文字によって表現できる一方で、アクセント付きのアルファベットそのものを1文字とする符号位置もまた用意されています。

上記の例では、アクセント付きの「é」をひとつの符号位置で表すこともできます(U+00E9, LATIN SMALL LETTER E WITH ACUTE)。すると、同じ「é」という文字に対して複数の符号化表現が対応する重複符号化の問題が発生してしまいます。

このため、Unicodeには符号化表現を一意にそろえる正規化の処理が定義されています。Unicodeの正規化には4種類の方式があります。NFC・NFD・NFKC・NFKDです。NFCは、結合文字を使わない方にそろえる形式。NFDは逆に、結合文字を使う方へそろえる形式です。

NFKCとNFKDは、それぞれNFCとNFDのバリエーションです。おおまかに言うと、互換用やそれに類する符号位置を積極的に変換するものです。例えば、全角英数字を通常の英数字に置換したり、丸付き数字を丸のないただの数字にするなどの処理が行われます。

正規化処理は、Java、Ruby、Python等の標準ライブラリに用意されています。Pythonでの使用例を、pythonコマンドでインタラクティブに試してみます。

>>> import unicodedata
>>> s = "cafe\u0301"                    ← U+0301は合成用アキュートアクセント
>>> s
'café'                                  ← sの内容を表示。eとアクセント記号で1文字のéになる
>>> len(s)
5                                       ← sの長さはアクセント記号が1符号位置を取るため5となる
>>> t = unicodedata.normalize('NFC', s) ← 文字列sをNFC正規化してtに格納
>>> t
'café'                                  ← tの内容を表示。見た目はsと同じ
>>> len(t)
4                                       ← 長さを調べると5から4に変わっている
>>> t[3]
'é'                                     ← 文字列tの末尾が1符号位置でéを表している
>>> unicodedata.name(t[3])              ← tの末尾のUnicode文字名を調べてみる
'LATIN SMALL LETTER E WITH ACUTE'       ← アキュートアクセント付き小文字E

これを使うと、結合文字を使わない方の表現にそろえることができます。

ただし、NFC正規化は、結果の文字列に結合文字が含まれないことを保証するものではありません。合成済みの符号位置が用意されていない文字は依然として結合文字を用いないと表現できないためです。

例えば、アイヌ語の表記に用いられる小書きの「ㇷ゚」という文字があります。JIS X 0213では、1面6区88点にこの字を単独の文字として符号化しています。

一方、Unicodeではこの文字に合成済みの符号位置を与えておらず、基底文字「ㇷ」の直後に合成用半濁点(U+309A)を置いて表現する必要があります。正規化の種類によらず、この文字は結合文字を必要とするわけです。

書記素クラスタで文字数をカウント

上に見たように、人が認識する1文字はUnicodeにおいては複数の符号位置の連なりで表現されることがあります。そこで、「人が認識する1文字」に切り分ける方式が、Unicodeの技術文書「Unicode Standard Annex #29」に定義されています。

この単位を、書記素クラスタ(grapheme cluster)といいます。文字列を書記素クラスタ単位に分割すれば、各々の単位は人から見た1文字に当たります。

いくつかのプログラミング言語では、文字列の書記素クラスタへの切り分けに対応しています。ここではRubyを例に取ります。

RubyのStringクラスにはgrapheme_clustersというメソッドが用意されています。これを呼び出すと、文字列を書記素クラスタに切り分けた結果を配列にして返します。

下記にコード例を示します。例に用いている片仮名の文字列は、先ほども登場したアイヌ語です。

s = "チタタㇷ\u309aにする"      ← U+309Aは合成用半濁点
p s.chars.join("/")             ← sの中身を符号位置ごとに分割した配列を ‘/’ でつなぐ
t = s.grapheme_clusters         ← sを書記素クラスタに分割した配列をtに格納
p t.join("/")                   ← tの要素を ‘/’ でつなぐ

このプログラムを実行すると下記のように出力されます。符号位置単位の分割と書記素クラスタの分割とで結果が異なることが分かります。

"チ/タ/タ/ㇷ/゜/に/す/る"       ← 符号位置単位の分割。合成用半濁点が独立して扱われる
"チ/タ/タ/ㇷ゚/に/す/る"          ← 書記素クラスタに分けると「ㇷ゚」が1文字扱いされる

書記素クラスタに切り分ける処理は、絵文字に対しても有用です。Unicode絵文字は、絵文字それ自体の符号位置の後ろに、スタイルを表す合成用の符号位置を後置して見栄えを示すことが頻繁にあります。

すると、文字列処理のために、符号位置の列の中でどこまでがひとつの絵文字であるかを調べることが必要になります。絵文字を書記素クラスタに切り分けるRubyのコード例を下記に示します。

s = "\u{2600 fe0f}"     ←太陽「☀」U+2600に絵文字スタイルU+FE0Fを付与(Unicodeエスケープ)
puts s.length           ←符号位置の数、すなわち2が出力される
a = s.grapheme_clusters ← 文字列を書記素クラスタに分割した配列を作る
puts a.length           ← その配列の要素数、すなわち1が出力される

変数sに入る絵文字は「 2 」のように、色の付いたいかにも絵文字風の見栄えで表示されます。これは後置されたU+FE0Fの働きによります。

文字列sは、見かけ上は1文字でありながら、符号位置は2あります。String#lengthメソッドでは、符号位置の数すなわち2が返されます。見かけの文字数とは一致しません。一方、sに対してgrapheme_clustersを実行して得られた配列の要素数は、見かけの文字数に一致し、1を返します。

このように、ASCIIやJIS X 0208といった従来の文字コードでは単純だった文字数のカウントという処理が、Unicodeでは書記素クラスタという概念を用いないと意図しない結果になるので注意が必要です。

まとめ

本稿では、文字コードの基本的な概念から始めて従来の主な文字コードを紹介し、現在主に使われているUnicodeの構成を概説しました。Webにおける文字コードの扱いを紹介し、また、プログラミングにおいて注意すべき箇所をいくつか取り上げました。

紙幅の都合上、細部を大胆に単純化したところもあり、また取り扱わなかった話題も多々あります。詳しくは拙著『[改訂新版] プログラマのための文字コード技術入門』(技術評論社刊)をお読みいただければ幸いです。

関連規格

ISOやJISの規格類はJSA Webdeskから購入できます。

  • ISO/IEC 646:1991, Information technology -- ISO 7-bit coded character set for information interchange.
  • ISO/IEC 8859-1:1998, Information technology -- 8-bit single-byte coded graphic character sets -- Part 1: Latin alphabet No. 1.
  • ISO/IEC 10646:2017, Information technology -- Universal Coded Character Set (UCS).
  • JIS X 0201:1997, 7ビット及び8ビットの情報交換用符号化文字集合
  • JIS X 0208:1997, 7ビット及び8ビットの2バイト情報交換用符号化漢字集合
  • JIS X 0213:2000, 7ビット及び8ビットの2バイト情報交換用符号化拡張漢字集合
  • JIS X 0221:2014, 国際符号化文字集合(UCS)

矢野 啓介(やの・けいすけ) yano-keisuke yanok

3
北海道札幌市出身、工学修士(北海道大学、システム情報工学専攻)。株式会社富士通研究所に勤務し、企業向けソフトウェア技術の研究開発に従事するかたわら、ライフワークとして文字の符号化を探求。オープンソースの仮名漢字変換ソフトウェアSKKのJIS第3・第4水準漢字辞書の開発に携わる。ソフトウェア工学分野の研究により、情報処理学会から2017年度山下記念研究賞を受賞。

編集:中薗 昴

若手ハイキャリアのスカウト転職