RubyにおけるUnicode正規化の基本と4つの正規化形式の使い分け
先日、Rubyの文字列メソッドの多くを特定のUnicode文字でテストし、予期しない動作をするかどうかを検証する記事を公開しました。その結果、実に多くのメソッドが予想外の挙動を示しました。
この記事への批判のひとつが「正規化されていない文字列を使ってテストしている」というものでした。正直に言えば、当時の私はUnicode正規化についてあまり深く理解していませんでした。おそらく、同様に曖昧なままのRubyistは少なくないでしょう。
正規化を活用すれば、私のテストで予期しない挙動を見せたUnicode文字列の多くを、Rubyの文字列メソッドと問題なく協調動作する文字列へと変換できます。ただし、以下の点には注意が必要です。
- 変換が常に完璧とは限りません。一部のUnicodeシーケンスは、どんな正規化を行ってもRubyの文字列メソッドで誤動作します。
- 正規化は手動で行う必要があります。RubyもRailsもデータベースも、デフォルトでは自動的に正規化してくれません。
本記事では、RubyにおけるUnicode正規化の入門的な解説を行います。皆さんがさらに深く探求するための出発点となれば幸いです。
文字列を正規化してみよう
String#unicode_normalizeメソッドはRuby 2.2で導入されました。純粋なRubyで実装されているため、C言語を利用したutf8_procやunicodeなどのgemに比べると処理速度は劣ります。
正規化が必要な理由は、Unicodeでは同じ文字を複数の方法で表現できるからです。例えば文字「Å」は、コードポイント"\u00c5"として表すこともできますし、「A」とアクセント記号を組み合わせた"A\u030A"として表すこともできます。

正規化は、一方の形式をもう一方へ変換します。
"A\u030A".unicode_normalize #=> 'Å' ("\u00C5"と同じ)
もちろん、Unicodeの正規化方法はひとつだけではありません。それでは簡単すぎますからね。正規化には4つの方式があり、「正規化形式(normalization forms)」と呼ばれています。それぞれNFD、NFC、NFKD、NFKCという暗号的な略称で名付けられています。
String#unicode_normalizeはデフォルトでNFCを使用しますが、次のように他の形式を指定することもできます。
"a\u0300".unicode_normalize(:nfkc) #=> 'à' ("\u00E0"と同じ)
しかし、これは実際には何を意味するのでしょうか。4つの正規化形式はそれぞれ何を行っているのか、詳しく見ていきましょう。
正規化形式
正規化の操作には2種類あります。
- 合成(Composition): 複数コードポイントの文字を単一のコードポイントに変換します。例えば、
"a\u0300"は"\u00E0"になります。どちらも文字「à」をエンコードする方法です。 - 分解(Decomposition): 合成の逆です。単一コードポイントの文字を複数のコードポイントに変換します。例えば、
"\u00E0"は"a\u0300"になります。
そして、合成・分解はそれぞれ2つの方法で行うことができます。
- 正準等価性(Canonical): 字形(グリフ)を保持します。例えば、
"2⁵"は"2⁵"のまま変わりません。システムによっては上付きの「5」をサポートしていない場合もありますが、字形は維持されます。 - 互換等価性(Compatibility): 字形を互換文字に置き換えることがあります。例えば、
"2⁵"は"25"に変換されます。
これら2つの操作と2つのオプションを組み合わせることで、4つの「正規化形式」が生まれます。以下の表に、名称、説明、入出力の例をまとめました。
| 名称 | 説明 | 入力 | 出力 |
|---|---|---|---|
| NFD | 正準分解 | Å "\u00c5" |
Å "A\u030A" |
| NFC | 正準分解の後に正準合成 | Å "A\u030A" |
Å "\u00c5" |
| NFKD | 互換分解 | ẛ̣ "\u1e9b\u0323" |
ṩ "\u0073\u0323\u0307" |
| NFKC | 互換分解の後に正準合成 | ẛ̣ "\u1e9b\u0323" |
ṩ "\u1e69" |
この表をじっくり眺めると、略称にも一定の規則があることに気づくかもしれません。
- 「NF」は「normalization form(正規化形式)」の略です。
- 「D」は「decomposition(分解)」の略です。
- 「C」は「composition(合成)」の略です。
- 「K」は「kompatibility(互換性)」の略です :)
より多くの例や詳細な技術的解説については、Unicode Standard Annex #15を参照してください。
正規化形式の選び方
どの正規化形式を使うべきかは、目的によって異なります。以下の推奨事項は、Unicode Normalization FAQに基づいています。
文字列メソッドとの互換性にはNFCを使う
Rubyの文字列メソッドをUnicodeの大部分とうまく協調させたいのであれば、おそらくNFCを選ぶのが最適です。String#unicode_normalizeのデフォルトがNFCになっているのには理由があります。
- 可能な限り、複数コードポイントの文字を単一コードポイントに合成します。複数コードポイントの文字こそが、Stringメソッドの問題の主な原因だからです。
- 字形を変更しないため、ユーザーが入力したテキストに見た目上の変化は生じません。
とはいえ、すべての複数コードポイント文字が単一コードポイントに合成できるわけではありません。そうした場合、RubyのStringメソッドは期待通りに動作しません。
s = "\u01B5\u0327\u0308" # => "Ƶ̧̈"、合成できない文字
s.unicode_normalize(:nfc).size # => 3、1文字しかないのに3と返る
セキュリティとDB互換性にはNFKCを使う
ユーザー名のようなセキュリティに関わるテキストを扱う場合や、テキストをデータベースとうまく協調させることが主な関心事である場合は、NFKCが良い選択となるでしょう。
- 問題を引き起こしうる文字を、対応する互換文字に変換します。
- その後、すべての文字を単一コードポイントに合成します。
なぜこれがセキュリティ上有用なのか、具体例で考えてみましょう。「HenryIV」というユーザー名のユーザーがいるとします。悪意ある攻撃者は、新しいユーザー名「HenryⅣ」を登録することで、このユーザーになりすまそうとするかもしれません。
そうです、両者は見た目がそっくりなのです。それこそが狙いです。しかし実際には、これらはまったく別の文字列です。前者はASCII文字の"IV"を使用していますが、後者はローマ数字の4を表すUnicode文字"Ⅳ"を使用しています。
この種の攻撃を防ぐには、一意性のバリデーションを行う前にNFKCで文字列を正規化します。この場合、NFKCはUnicodeの"\u2163"をASCII文字の「IV」に変換してくれます。
a = "Henry\u2163"
b = "HenryIV"
a.unicode_normalize(:nfc) == b.unicode_normalize(:nfc) # => false(NFCは字形を保持するため)
a.unicode_normalize(:nfkc) == b.unicode_normalize(:nfkc) # => true(NFKCは両方ともASCIIの"IV"に評価されるため)
おわりに
改めて調べてみて、Unicode正規化がRubyやRailsコミュニティで大きな話題になっていないことに少し驚きました。Railsが自動的に処理してくれるものだと期待するかもしれませんが、私の調べた限りではそうなっていません。そして、ユーザーから受け取ったデータを正規化しないままにしておくと、Rubyの文字列メソッドの多くが信頼できなくなってしまいます。
読者の皆さんの中で、私の知らないことをご存じの方がいらっしゃいましたら、Twitter(@StarrHorne)やメール(starr@honeybadger.io)でお気軽にご連絡ください。Unicodeは非常に奥深いトピックであり、私自身、そのすべてを知らないことはすでに証明済みです。
-
Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説
※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す
-
Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説
Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端