トラブルシュートの中で、これUnicodeの正規化関連のトラブルではないか、と思える事象がありました。 最終的には違ったようなのですが、改めてUnicodeの正規化について理解を深めたく、調べてみました。
Unicodeにおける結合文字列
前提として、「ほげ」という文字列をUnicodeで表記しようとするとき、U+307B
(ほ)とU+3052
(げ)の2つのコードポイント列での表現が可能です。
また、別解として、U+307B
(ほ)とU+3051
(け)、U+3099
(濁点)の組み合わせでも表現できます。
>>> print("\u307B\u3052") ほげ >>> print("\u307B\u3051\u3099") ほげ
2つの文字符号を合わせて1つの文字を表すのは、いわゆる結合文字列(Combining Character Sequence)という概念で説明されます。
上記で言えば、U+3099
はCombining Characterと呼ばれる合成用文字で、それがU+3051
(け)と結合されて結合文字列である「げ」を表します。
Combining Character Sequenceの課題
課題になるのは、なんと言っても文字列としての等価性判定でしょう。
"\u307B\u3052"
と"\u307B\u3051\u3099"
が共に「ほげ」を表すにもかかわらず、コードポイント列は明らかに異なります。
根本的な問題は、等しく扱ってほしい文字列に対して複数のコードポイント列が割り当て可能なことなので、これを1つに統一する方法があればよい。 それを行うのが、Unicodeにおける「正規化」(Unicode Normalization)です。
Unicodeの正規化
Unicodeの正規化方法には4種類が存在します。
Form | 説明 |
---|---|
Normalization Form D (NFD) | 正準等価によって分解する正規化方法 |
Normalization Form C (NFC) | 正準等価によって分解された後、正準同意等価によって合成する正規化方法 |
Normalization Form KD (NFKD) | 互換等価によって分解する正規化方法 |
Normalization Form KC (NFKC) | 互換等価によって分解された後、正準等価によって合成する正規化方法 |
これを理解するためには「正準等価」「互換等価」をまず理解しなければなりません。
正準等価性と互換等価性
Unicode標準では、文字の「等しさ」について2つの基準を定めています。 Canonical Equivalence(正準等価性)とCompatibility Equivalence(互換等価性)です。
正準等価な文字とは、「機能」と「表示」の双方で等価と考えられる文字です。 以下はUnicode Standard Annex #15 UNICODE NORMALIZATION FORMSからの引用ですが、(コードポイント列が異なったとしても)Exampleは正準等価です。
一方で互換等価性とは、「表示」は異なり得るという、正準等価よりもやや弱い等価性です。 百聞は一見にしかずということで、再度Annexから引用します。
Width VariantsやSquared Charactersの例は、日本でも馴染み深いでしょう。
合成と分解
正規化方法のところで使った「合成」は、結合文字列を合成済み文字に変換することを指します。例えば、U+3051
(け)、U+3099
(濁点)をU+3052
(げ)に変換するのが合成です。
この逆が分解です。ここで記載した「合成」と「分解」は、正準等価性に基づいているため、正準等価による合成と分解といえるでしょう。
どう実装するのか
これら正規化のアルゴリズムは、プログラミング言語あるいはそのライブラリで実装されていることがほとんどです。
例えばJavaであればNormalizerクラス、 JavaScriptであればString.prototype.normalize()を利用することになるでしょう。 それぞれ、4つの正規化方法のうちで何を使うかをパラメータで指定できるようになっています。
OSごとの扱いは
Windowsに関しては、IMEからは基本的にNFCの文字列が生成されるという記載があります。
Windows, Microsoft applications, and the .NET Framework generally generate characters in form C using normal input methods. For most purposes on Windows, form C is the preferred form.
Using Unicode Normalization to Represent Strings - Win32 apps | Microsoft Docs
また、ファイルシステム側ではUnicode正規化は行わないという話1もあります。 Windowsの世界ではNFCが使われる「傾向」が強いでしょう。
There is no need to perform any Unicode normalization on path and file name strings for use by the Windows file I/O API functions because the file system treats path and file names as an opaque sequence of WCHARs. Any normalization that your application requires should be performed with this in mind, external of any calls to related Windows file I/O API functions.
一方でMacの場合、APFSの場合はファイルシステムレベルでUnicode正規化は行われないようです。
ただ、上位レイヤでは多くの場合NFDが使われるので、Mac文化圏はNFDという認識です。
S3のオブジェクトキーでの扱いは
S3のオブジェクトキーはUnicode文字列がUTF-8エンコードされたものという仕様になっています。
The object key name is a sequence of Unicode characters with UTF-8 encoding of up to 1,024 bytes long.
では、ここでUnicode正規化は行われるのでしょうか。 この問いに対しては先人がすでに確認をしてくれており、共にS3側では何もしていない結論になっています。