なぜRubyでは「'👨‍👩‍👧‍👦'.size」が「7」とカウントされるんですか? 文字コードの深淵をとみたまさひろさんが語るのトップ画像

なぜRubyでは「'👨‍👩‍👧‍👦'.size」が「7」とカウントされるんですか? 文字コードの深淵をとみたまさひろさんが語る

投稿日時:
とみたまさひろのアイコン

文字化け好きなプログラマー

とみたまさひろ

Xアカウントリンク

世の中には、あらゆる技術領域の専門家が存在します。実用的な知識もあれば、極めてニッチな知識もあるでしょう。どんな領域であっても「飽くなき探究心を注げるテーマ」があることは、エンジニアとしての人生を豊かなものにしてくれます。

今回お話を伺ったのは、エンジニアコミュニティで「文字コードや文字化けの専門家」として知られる、とみたまさひろさんです。とみたさんはなぜこの深淵に足を踏み入れ、魅了され続けているのでしょうか。知られざるマニアックな世界について語っていただきました。

「パズルのような面白さがある」文字コードという底なし沼へ

― とみたさんは「文字コード・文字化けに詳しい方」としてエンジニアコミュニティ内で有名です。何をきっかけに、これらの技術領域と深く向き合うようになったのでしょうか?

実は、明確に「この瞬間から興味を持った」と言える転機はありません。ただ、振り返ってみると、キャリアのスタート地点がちょうど文字コードの過渡期だったことが大きいかもしれないです。

私が就職したのは1991年なのですが、当時は自分がプライベートで使っていたパソコンの標準の文字コードがShift_JISでした。ところが、配属されたのがUNIXを扱う部署で、そこでの標準はEUC-JPだったんです。「世の中にはShift_JIS以外の文字コードが存在するんだ」と初めて意識したのは、その時でした。

― その後、どのような場面で文字コードのトラブルと遭遇されていましたか?

たとえば、メールのやり取りです。かつてはメールヘッダーのFromやSubjectには日本語が使えず、本文もISO-2022-JPという形式を使うのがルールでした。

ISO-2022-JPは、エスケープシーケンスを使って「ここからは日本語」「ここからはASCII」と切り替えながら、7ビットのASCII文字の範囲内で日本語を表現する仕組みです。そのため、UNIX上のEUC-JPで読み書きするには変換が必要になるのですが、そこで必ずと言っていいほど文字化けが起きます。

特に厄介だったのが、Windows環境から送られてくる「①②③」のような丸付き数字、いわゆる機種依存文字です。これらは当時のJIS規格には存在しない、いわば「外字」扱いなのですが、Windows側では当たり前に使われています。規格にない文字を、どうにか変換して表示させなければならない。パズルのような状況に直面し、「これは面白い」と感じ始めたのがきっかけだった気がします。

― 現代ではUTF-8が主流になったため、かつてのような文字化けは減ったように感じます。

そうですね。世界中でUTF-8が使われるようになり、文字化けが起きるケースは少なくなりました。しかし、それで文字コードの問題が解決したかというと、むしろ逆です。Unicodeが登場・普及したことで、別の難しさが生まれています。Unicodeには同じ見た目でも異なるコードポイントを持つ文字があったり、正規化のルールがあったりと、規格が複雑です。

かつては「表示できるかどうか」が戦いでしたが、今は「多種多様な文字をいかに正しく、一貫性を持って扱うか」というエンジニアリングが求められるようになっています。その底知れなさや、規格の重厚さこそが、今の私が感じている文字コードの魅力ですね。

「全部同じじゃないですか!?」と思うなかれ。見た目は瓜二つでも、中身はまるで別物

― では、文字コードの面白い仕様について教えてください。

過去に「文字ときどきRuby」という発表をしたことがあるので、これを元に解説していきますね。

フォントが違うだけで同じ文字「直」

まず、二つの「直」という文字を例に考えてみます。見た目が少し違っていますが、これらは単にフォントの違いであって、コンピュータ内部では全く同じコードポイントで扱われています。Rubyで'直'.ord.to_s(16)と書いてみると、どちらの「直」も76f4という同じ番号を返してくれます。

違う文字「令」

次に面白いのが、見た目は同じなのにコードポイントが違うケースです。たとえばこの二つの「令」という字。一方はCJK統合漢字、もう一方はCJK互換漢字という別々のカテゴリに属しており、そのまま文字列比較すると結果がfalseになってしまいます。

rei1 = '令'
rei2 = '令'
rei1.ord.to_s(16)  #=> "4ee4"
rei2.ord.to_s(16)  #=> "f9a8"
rei1 == rei2  #=> false

そこで登場するのが「Unicode正規化」です。Rubyなら String#unicode_normalize を呼ぶと、互換漢字を統合漢字に変換してくれるので、文字列比較の結果がtrueになります。

rei1 == rei2.unicode_normalize  #=> true

異体字「令」

また、異体字セレクタという仕組みもあります。基底文字の後にU+E0100U+E01EFというコードを付け足すと、フォントが対応していればプレーンテキストであっても特定の字体で表示させられます。

これらのコードは、正規化をしても消えないという特性があります。もし検索などの際に邪魔になる場合は、gsubなどを使って削る必要があるんですね。

str.gsub(/[\u{e0100}-\u{e01ef}]/, '')

他には「髙(はしご高)」という字も独特です。この文字はUnicode上では「高」の異体字ではなく、完全に「別の文字」として定義されています。正規化も効かないですし、異体字セレクタでもありません。

しかも、Rubyではエンコーディングの扱いも重要で、Windows-31J(SJIS)には存在しますが、標準のShift_JISには存在しないため、変換しようとするとエラーが出るという落とし穴があります。

'髙'.encode('SJIS') #=> "\x{FBFC}"
'髙'.encode('Shift_JIS') # Shift_JIS と SJIS は異なる
# `encode': U+9AD9 from UTF-8 to Shift_JIS
#  (Encoding::UndefinedConversionError)

似た例として「﨑(たち崎)」という字も独特です。これは「令」と同じく CJK互換漢字に含まれる文字です。しかし、unicode_normalizeをしても「崎」には変換されません。「字体差が大きい」という理由で統合の対象外にされているんです。

― ひええ難しい。一筋縄ではいかない仕様がいくつもありますね。

そうなんです。ちなみに絵文字も面白いですよ。

この文字数は?「日本の国旗」「3人家族」「4人家族」の絵文字

これらの絵文字は一見すると「1文字」のように見えますよね。ですが、文字長(String#size)を調べると、そうではないとわかります。まず、国旗は2文字で構成されています。

'🇯🇵'.size  #=> 2

国旗用文字というものがありまして、その「🇯」と「🇵」をつなげて書くと「🇯🇵」になります。だから、sizeが2になるわけです。

3人家族の絵文字は、コードポイントU+1F46Aの1文字です。

'👪'.size  #=> 1

ところが子どもが一人増えて4人家族になるとコードポイント7文字で構成されます。

'👨‍👩‍👧‍👦'.size  #=> 7

理由はこちらの資料の通りです。

4人家族は7文字

辞書順を実現する、驚くほど緻密な「重み付け」の仕組み

― 文字の並び順(Collation)にも、独特な仕様はありますか?

これも、以前「文字列の並び順 / Unicode Collation」という発表をしたことがあるので、その内容をベースに解説します。

データベースなどで文字をソートすると、一見するとシンプルに文字コード(コードポイント)順に並んでいるように見えますよね。でも、実はその裏側では複雑な処理が行われています。

たとえば「aaa」「abc」「AAA」「ABC」という4つの文字列をソートする場合、単純な文字コード順だと大文字小文字の順に並んでしまいます。つまり「AAA、ABC、aaa、abc」になってしまうはずです。

これを人間にとって自然な「辞書順」にするために、Unicodeではweightという仕組みを使っているんです。だから、PostgreSQLで以下のようなクエリを書いた場合、結果はこうなります。

SELECT name, name::bytea byte FROM t ORDER BY name;

name と byte を並べたテーブル。aaa=\x616161、AAA=\x414141、abc=\x616263、ABC=\x414243 と、小文字(61〜)と大文字(41〜)が交互に並んでいる様子

― 文字コードの値だけで決まっているわけではないのですね。

そうなんです。Unicodeの規格(UCA: Unicode Collation Algorithm)では、文字ごとにweightという値を持っています。そして、weightは3つの値から構成されています。

DUCET における a〜C の weight 一覧。a と A は 2380、b と B は 239A、c と C は 23B4 と1番目の値が共通で、3番目の値のみ小文字 0002・大文字 0008 と異なる

ソートする際は、まず文字列を構成する文字の「1番目のweight」をすべて連結し、その後に区切り文字を置いて「2番目のweight」をすべて連結するといったように、文字列のソートキーを生成して比較します。

abc の各文字の weight を、1番目の値・2番目の値・3番目の値ごとにまとめて 0000 で連結し、ソートキー「2380 239A 23B4 0000 0020 0020 0020 0000 0002 0002 0002」を求める図

こうすることで、辞書順のような人間にとって自然な並びを実現しています。

aaa, AAA, abc, ABC の4つの文字列のソートキーを並べた表。先頭3つの値で aaa/AAA と abc/ABC が分かれ、末尾3つの値(0002 か 0008)で小文字と大文字が分かれるため、aaa, AAA, abc, ABC の順になる

― 日本語ならではの、並びの面白さはありますか?

非常に凝っているなと感じるのは、元号の扱いです。「明治」「大正」「昭和」「平成」「令和」には、2文字を1文字分に凝縮した「合字(ごうじ)」があるのですが、Unicodeの標準では2文字の「令和」と1文字の合字の「㋿」が、ソート結果でほぼ同じ位置に現れるようweightが調整されています。一方、日本語コレーションでは合字は元号の順に並びます。

unicode と ja-x-icu の並び順を比較したターミナル出力。ja-x-icu では元号の合字が明治・大正・昭和・平成・令和の元号順にまとまって並ぶ。MySQL は Unicode 9 相当のためこうならないとの注記あり

また、日本語のCollationは長音記号(「ー」)の扱いも独特です。ICUでは、直前の文字の母音によってソート順が変わります。「かー」の場合は「か」のすぐ後に来ますが、「くー」の場合は「くい」と「くう」の間に入るといった具合に、順序を制御しています。

これはJIS(日本産業規格)などが定める国語辞書のルールを忠実に再現しているからです。文字コードの仕様でここまでやりきる情熱は、まさに「やりすぎ」と言いたくなるほどのこだわりですよね。

unicode と ja-x-icu の並び順の比較。ja-x-icu では長音記号が前の文字の母音に従って並び、「か」の後では「あ」の位置、「き」の後では「い」の位置、「く」の後では「う」の位置に来る

大事なのは、その技術を純粋に楽しむこと。「役に立つかどうか」じゃない

― 文字コードのようなニッチな専門領域が、ご自身のキャリアにとってプラスになったと感じることはありますか?

正直なところ、直接的に「得をした」と感じる場面はそれほどありません(笑)。ただ、こうして楽しみながら発信を続けているおかげか、時々人前で話す機会をいただいたり、執筆のお話をいただいたりすることにつながっています。私が共著者として参加している『MySQL徹底入門』(翔泳社)でも、文字コードに関する章を丸々一章分任せていただいたりしました。

実用的かどうかはさておき、何か一つ「これだけは誰にも負けない」という得意分野を持っていることは、エンジニアとしての信頼や差別化には寄与しているのかもしれません。

― この記事を読んで「文字コードを深く学んでみたい」と思った方は、まず何から手をつけるのが良いでしょうか?

決定版と言える本があります。『プログラマのための文字コード技術入門』(技術評論社)です。これは本当によくできていて、その名の通り丸ごと一冊、文字コードの話だけをしています。まずはこれを一読しておけば、大体のことは網羅できるはずです。

もし、そこからさらに深淵を覗きたくなったら……あとはもう、Unicodeの規格書を頑張って読むしかないですね(笑)。かなり骨が折れる作業だとは思いますが、やはり一次資料にあたるのが一番確実です。

― 読者に向けて、文字コードを学ぶことの意義についてメッセージをお願いします。

「仕事に役立てたいから学ぼう」と思うと、文字コードの世界はあまりに複雑で、少し辛くなってしまうかもしれません。それよりも、ふとした瞬間の「なぜこうなっているんだろう?」という疑問を起点に、その裏側の仕組みを調べるような、パズル的な面白さを大切にしてほしいです。

そうして得た知識が、たまに仕事で発生したトラブルを鮮やかに解決して、「ちょっといい顔」ができる。そのくらいの距離感が、一番楽しいんじゃないかと思います。

あとは、規格書を読み込んでいくと「ものすごく賢い人が、うんと考えてこの仕様をつくったんだな」という設計の意図が見えてくる瞬間があります。そうした歴史や思想に触れることも、この領域の醍醐味ですね。

取材・執筆:中薗昴

※本記事のスライド内で使用している絵文字は、しかまつ氏が制作・配布されているものです。配布元:note

50
50