Unicode 正規化を文字操作に応用します。
合成文字はデータ数と文字数が一致しないなど Unicode の複雑性として言及されることが多いですが、便利な一面もあります。
Unicode 正規化
Unicode には文字の構成要素をまとめて 1 つのコードで表す NFC と、構成要素を別々にコード化する NFD という正規化方式があります。
※ その他 NFKC と NFKD もありますが、この記事では扱いません。
正規化を処理する API として、Python には unicodedata.normalize
があります。
濁点
「が」という平仮名を例に説明します。NFC ではまとめて 1 つのコードで表しますが、NFD では「か」と「゛」に分解したコードで表します。
>>> import unicodedata
>>> def tohex(s):
... return " ".join([f"{ord(ch):04x}" for ch in s])
...
>>> tohex(unicodedata.normalize("NFC", "が"))
'304c'
>>> tohex(unicodedata.normalize("NFD", "が"))
'304b 3099'
>>> chr(0x304b)
'か'
>>> chr(0x3099)
'゙'
※ "\uXXXX"
という指定方法もあります。今後はこちらを使用します。
>>> "\u304b"
'か'
分解すると len
が文字数とは一致しなくなることに注意が必要です。
>>> len("が")
1
>>> len(unicodedata.normalize("NFD", "が"))
2
unicodedata.name
で Unicode 規格での文字の名前が確認できます。
>>> unicodedata.name("\u304c")
'HIRAGANA LETTER GA'
>>> unicodedata.name("\u304b")
'HIRAGANA LETTER KA'
>>> unicodedata.name("\u3099")
'COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK'
※ 合成に使われる付加記号類には COMBINING で始まる名前が付けられます。結合文字と呼ばれます。
unicodedata.decomposition
でどのように分解されるか確認できます。「か」はこれ以上分解できないため空文字列が返されます。
>>> unicodedata.decomposition("か")
''
>>> unicodedata.decomposition("が")
'304B 3099'
※ 文字コードが文字列で返されるため、先ほど定義した tohex
は不要です。
複数の付加記号
アルファベットには複数の付加記号(アクセント類)が付くことがあり、NFD では記号ごとに分解されます。
>>> tohex(unicodedata.normalize("NFD", "ḉ"))
'0063 0327 0301'
>>> unicodedata.name("ḉ")
'LATIN SMALL LETTER C WITH CEDILLA AND ACUTE'
>>> unicodedata.name("\u0063")
'LATIN SMALL LETTER C'
>>> unicodedata.name("\u0327")
'COMBINING CEDILLA'
>>> unicodedata.name("\u0301")
'COMBINING ACUTE ACCENT'
decomposition
は 2 段階で定義されます。normalize
ではこれを一度に処理します。
>>> unicodedata.decomposition("ḉ")
'00E7 0301'
>>> "\u00e7"
'ç'
>>> unicodedata.decomposition("ç")
'0063 0327'
>>> unicodedata.name("ç")
'LATIN SMALL LETTER C WITH CEDILLA'
この場合、付加記号の順番を変えても NFC での合成に影響はありません。
>>> tohex(unicodedata.normalize("NFC", "\u0063\u0327\u0301"))
'1e09'
>>> tohex(unicodedata.normalize("NFC", "\u0063\u0301\u0327"))
'1e09'
付加記号の並びで異なる文字を表すこともあります。その組み合わせが規格で定義されているかに依存するため、規則性はありません。
>>> tohex(unicodedata.normalize("NFD", "ǖ"))
'0075 0308 0304'
>>> tohex(unicodedata.normalize("NFD", "ṻ"))
'0075 0304 0308'
>>> unicodedata.name("\u0075")
'LATIN SMALL LETTER U'
>>> unicodedata.name("\u0308")
'COMBINING DIAERESIS'
>>> unicodedata.name("\u0304")
'COMBINING MACRON'
ある記号がどのような文字と組み合わせられるかは、Unicode 規格書や Wikipedia で確認できます。
文字操作
通常の利用ではデータ量が抑えられる NFC 正規化が有利です。
文字の操作には NFD が有用です。いくつか例を示します。
濁点を付ける
濁点を表す付加記号を付けるだけです。
>>> def dakuten(ch):
... return ch + "\u3099"
...
>>> dakuten("さ")
'ざ'
>>> dakuten("ツ")
'ヅ'
このままだとデータ量が大きくなってしまいます。
>>> len(dakuten("さ"))
2
>>> tohex(dakuten("さ"))
'3055 3099'
return
の前に NFC
で合成した方が良いでしょう。
>>> def dakuten(ch):
... return unicodedata.normalize("NFC", ch + "\u3099")
...
>>> tohex(dakuten("さ"))
'3056'
濁点を付加すると文字コードが 1 つ増えるだけに見えますが、例外はあります。
>>> tohex("か"), tohex(dakuten("か"))
('304b', '304c')
>>> tohex("は"), tohex(dakuten("は"))
('306f', '3070')
>>> tohex("う"), tohex(dakuten("う"))
('3046', '3094')
一般的に、ある記号を付けたときの文字コードの変化には規則性がありません。正規化をライブラリに任せれば、文字コードの変化を知らなくても処理が書けるので便利です。
濁点を取り除く
NFD で分解して最初の 1 文字だけを残せば、一律に濁点が取り除けます。
>>> def strip(text):
... return "".join([unicodedata.normalize("NFD", ch)[0] for ch in text])
...
>>> strip("がぎぐげご")
'かきくけこ'
転写
ある文字から別の文字に書き換えるのにも利用できます。(転写)
古代ギリシア語は付加記号類が多く、アルファベットに転写するため Unicode 正規化を利用しました。
>>> import greektrans
>>> greektrans.romanize("Πάτερ ἡμῶν ὁ ἐν τοῖς οὐρανοῖς·")
'Páter hēmôn ho en tóès ūranóès;'
基本的には濁点を付ける例と同じことをやっています。当初は NFD で分解せずに NFC 正規化された状態で処理していたため、変換部分はすべてテーブルに依存していました。NFD で分解して扱うことによって、コードで付加記号を明示的に記述できるようになりました。記号の変更にも対応しやすいです。
転写の詳細はこちらを参照してください。