RubyでのUnicode正規化
最近、Rubyの文字列メソッドのほとんどを特定のUnicode文字でテストして、予期しない動作が発生するかどうかを確認する記事を公開しました。それらの多くはそうしました。
この記事に対する批判の1つは、テストに正規化されていない文字列を使用していたことです。率直に言って、私はUnicodeの正規化について少し曖昧でした。多くのRubyistがいると思います。
正規化を使用すると、テストで予期しない動作をした多くのUnicode文字列を取得して、Rubyの文字列メソッドで適切に機能する文字列に変換できます。ただし:
- 変換は必ずしも完全ではありません。一部のUnicodeシーケンスでは、常にRubyの文字列メソッドが誤動作します。
- これは手動で行う必要があります。デフォルトでは、Ruby、Rails、DBのいずれも自動的に正規化されません。
この記事では、RubyでのUnicode正規化について簡単に紹介します。うまくいけば、それはあなた自身の探検のための出発点をあなたに与えるでしょう。
String#unicode_normalize
メソッドはRuby2.2で導入されました。 Rubyで記述されているため、Cを活用するutf8_procやunicodegemのような正規化ライブラリほど高速ではありません。
正規化が必要な理由は、Unicodeには文字を書く方法が複数あるためです。文字"Å"
コードポイント"\u00c5"
として表すことができます または、文字「A」とアクセントの組み合わせとして:"A\u030A"
。
正規化は、ある形式を別の形式に変換します:
"A\u030A".unicode_normalize #=> 'Å' (same as "\u00C5")
もちろん、Unicodeを正規化する方法は1つだけではありません。それは単純すぎるでしょう!正規化には、「正規化フォーム」と呼ばれる4つの方法があります。それらは、不可解な頭字語を使用して名前が付けられています:NFD、NFC、NFKD、およびNFKC。
String#unicode_normalize
デフォルトではNFCを使用しますが、次のような他の形式を使用するように指示できます:
"a\u0300".unicode_normalize(:nfkc) #=> 'à' (same as "\u00E0")
しかし、これは実際にはどういう意味ですか? 4つの正規化フォームは実際に何をしますか?見てみましょう。
正規化操作には次の2種類があります。
- 構成: マルチコードポイント文字をシングルコードポイントに変換します。例:
"a\u0300"
"\u00E0"
になります 、どちらも文字à
をエンコードする方法です 。 - 分解: 構成の反対。単一のコードポイント文字を複数のコードポイントに変換します。例:
"\u00E0"
"a\u0300"
になります 。
合成と分解は、それぞれ2つの方法で実行できます。
- 正規: グリフを保持します。例:
"2⁵"
"2⁵"
のまま 一部のシステムでは上付き文字5文字がサポートされていない場合があります。 - 互換性: グリフを互換文字に置き換えることができます。例:
"2⁵"
"2 5"
に変換されます 。
2つの操作と2つのオプションをさまざまな方法で組み合わせて、4つの「正規化フォーム」を作成します。以下の表に、入力と出力の説明と例とともに、それらすべてをリストしました。
名前 | 説明 | 入力 | 出力 |
---|---|---|---|
NFD | 正規の分解 | Å"\u00c5" | Å"A\u030A" |
NFC | 標準的な分解とそれに続く標準的な構成 | Å"A\u030A" | Å"\u00c5" |
NFKD | 互換性の分解 | ẛ̣"\u1e9b\u0323" | ṩ"\u0073\u0323\u0307" |
NFKC | 互換性の分解とそれに続く正規の構成 | ẛ̣"\u1e9b\u0323" | ṩ"\u1e69" |
この表を数分間見ると、頭字語が意味をなすものであることに気付くかもしれません。
- 「NF」は「正規化形式」の略です。
- 「D」は「分解」の略です
- 「C」は「構成」の略です
- 「K」は「kompatibility」の略です:)
その他の例とより完全な技術的説明については、Unicode Standard Annex#15を確認してください。
使用する必要のある正規化フォームは、目前のタスクによって異なります。以下の私の推奨事項は、Unicode正規化FAQに基づいています。
文字列の互換性のためにNFCを使用する
Rubyの文字列メソッドをほとんどのUnicodeでうまく機能させることが目標である場合は、NFCを使用することをお勧めします。 String#unicode_normalize
のデフォルトであるのには理由があります 。
- 可能な場合は、マルチコードポイント文字を単一のコードポイントに構成します。マルチコードポイント文字は、Stringメソッドに関するほとんどの問題の原因です。
- グリフは変更されないため、エンドユーザーは入力したテキストの変更に気付くことはありません。
とはいえ、すべてのマルチコードポイント文字を単一のコードポイントに構成できるわけではありません。そのような場合、RubyのStringメソッドの動作は悪くなります:
s = "\u01B5\u0327\u0308" # => "Ƶ̧̈", an un-composable character
s.unicode_normalize(:nfc).size # => 3, even though there's only one character
セキュリティとDBの互換性のためにNFKCを使用する
ユーザー名などのセキュリティ関連のテキストを使用している場合、または主にデータベースでテキストを適切に再生することに関心がある場合は、NFKCがおそらく適切な選択です。
- 問題が発生する可能性のある文字を互換性のある文字に変換します。
- 次に、すべての文字を単一のコードポイントに構成します。
これがセキュリティに役立つ理由を理解するために、ユーザー名が「HenryIV」のユーザーがいると想像してください。悪意のある攻撃者が、新しいユーザー名「HenryⅣ」を登録して、このユーザーになりすまそうとする可能性があります。
私は知っています、彼らは同じように見えます。それがポイントです。しかし、実際には2つの異なる文字列です。前者はASCII文字"IV"
を使用します 後者はローマ数字4にUnicode文字を使用します:"Ⅳ"
。
一意性を検証する前にNFKCを使用して文字列を正規化することにより、この種の問題を防ぐことができます。この場合、NFKCはUnicode "\u2163"
を変換します アスキー文字「IV」に。
a = "Henry\u2163"
b = "HenryIV"
a.unicode_normalize(:nfc) == b.unicode_normalize(:nfc) # => false, because NFC preserves glyphs
a.unicode_normalize(:nfkc) == b.unicode_normalize(:nfkc) # => true, because NFKC evaluates both to the ascii "IV"
これを詳しく調べたので、Unicodeの正規化がRubyおよびRailsコミュニティで大きなトピックではないことに少し驚いています。 Railsによって行われることを期待するかもしれませんが、私が知る限り、そうではありません。また、ユーザーから提供されたデータを正規化しないということは、Rubyの文字列メソッドの多くが信頼できないことを意味します。
親愛なる読者の誰かが私が知らないことを知っているなら、ツイッター@StarrHorne経由で連絡するか、[email protected]に電子メールを送ってください。 Unicodeは大きなトピックであり、私はそれについてすべてを知っているわけではないことをすでに証明しました。 :)
-
Rubyでの挿入ソートを理解する
注:これは、Rubyを使用したさまざまなソートアルゴリズムの実装を検討するシリーズのパート4です。パート1ではバブルソート、パート2では選択ソート、パート3ではマージソートについて説明しました。 データを並べ替えるためのさまざまな方法を引き続き検討するため、挿入並べ替えに目を向けます。挿入ソートが好きな理由はたくさんあります!まず、挿入ソートは安定です。 、これは、等しいキーを持つ要素の相対的な順序を変更しないことを意味します。 インプレースアルゴリズムでもあります 、は、並べ替えられた要素を格納するための新しい配列を作成しないことを意味します。最後に、挿入ソートは、すぐにわかるように、実
-
Ruby2.6の9つの新機能
Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,