文章に対して自然言語処理による分析や文字列の検索を行う前に,正規化によって全角文字と半角文字の表記ゆれを統一する必要がある場合があります.ここでは,全角アルファベットや全角数字,全角記号を半角文字に変換する方法を説明します.また,正規化の方法は複数あるので,その特徴についても考察します.
まず,unicodedata
ライブラリをプログラムの先頭でインポートします.
import unicodedata
ここでは全角文字と半角文字で表記の揺れが存在する文字列を準備します.なお,わかりやすいように,全角のアルファベット,数字,記号はハイライトしています.カッコや空白についても全角と半角が混在していることにも注意してください.
msg = "近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。"
print(msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
上の文字列を正規化して半角に変換します.結論から述べると,次のように unicodedata.normalize()
関数に 'NFKC'
という正規形を指定するとうまくいきます.
normalized_msg = unicodedata.normalize('NFKC', msg)
print(normalized_msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
上では,unicodedata.normalize()
関数の第1引数に 'NFKC'
を指定しました.これ以外にも 'NFC'
,'NFD'
,'NFKD'
が指定できます.次のセクションではこれらの違いについて考察します.
上と同じ文章を正規化するときの第1引数を変化させてみます.なお,unicodedata.normalize()
の詳細はこちらを参照してください.
まず,'NFC'
を指定します.この場合は表記の揺れが統一されませんでした.
normalized_msg = unicodedata.normalize('NFC', msg)
print(normalized_msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
次に 'NFKC'
です.これはすでに示したとおり,半角に統一されています.
normalized_msg = unicodedata.normalize('NFKC', msg)
print(normalized_msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
'NFD'
では 'NFC'
と同じ結果で,表記の揺れは統一されません.
normalized_msg = unicodedata.normalize('NFD', msg)
print(normalized_msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
'NFKD'
では 'NFKC'
と同じ結果で,半角文字に統一されました.
normalized_msg = unicodedata.normalize('NFKD', msg)
print(normalized_msg)
近年日本で開催されたサミット(主要国会議)は G8 Hokkaido TOYAKO Summit、G7 JAPAN 2016 Ise-Shima Summit と、G7 Hiroshima Summit です。
つまり,上の文字列を半角文字に変換するには 'NFKC'
または 'NFKD'
を指定すれば良いことになります.(すでに説明したとおり,通常は 'NFKC'
でよいはずですが)これらの違いについて次のセクションで更に分析してみます.具体的には「ヴ」などの文字の取り扱いで違いが出てくることになります.またドイツ語のウムラウトなどの取り扱いでも違いが現れます.
ここでは unicodedata.normalize()
関数で指定する4種類の正規形('NFC'
,'NFKC'
,'NFD'
,'NFKD'
)について分析しその結果を考察してみます.なお,これらの詳細は docs.python.org や Wikipedia で確認できます.(私はこのあたりの理論については詳しくありません.)
まず文字列を一文字ごと文字コードに変換して表示する関数を準備します.
# 文字列を1文字ずつ文字コードにして表示する
def print_ord(s):
print(s, ": ", end='')
for c in s:
print(hex(ord(c)), end=' ')
print(' ')
次に,分析したい文字の組み合わせを s1
,s2
にセットして,その文字コードを確認します.分析したい組み合わせのコメントを解除していろいろ試してください.
s1 = '1' # 半角文字
s2 = '1' # 全角文字
# s1 = 'A' # 半角文字
# s2 = 'A' # 全角文字
# s1 = 'ア' # 半角文字
# s2 = 'ア' # 全角文字
# s1 = '(' # 半角カッコ
# s2 = '(' # 全角カッコ
# s1 = ' ' # 半角空白
# s2 = ' ' # 全角空白
# s1 = 'km' # 半角文字
# s2 = '㎞' # 全角文字(特殊文字)
# s1 = 'ヘクタール' # 半角文字
# s2 = '㌶' # 全角文字(特殊文字)
# s1 = 'ヴ' # 「ヴ」という一文字
# s2 = 'ヴ' # 「ウ」と「 ゙」 の合字
# s1 = 'fi'
# s2 = 'fi' # 合字
# s1 = 'u'
# s2 = 'ü' # ドイツ語のウムラウト u
# s1 = 'a'
# s2 = 'å' # 上リング付き a (ノルウェー語,デンマーク語,スウェーデン語で用いられる)
print_ord(s1)
print_ord(s2)
1 : 0x31 1 : 0xff11
正規化の方法に 'NFC'
,'NFKC'
,'NFD'
,'NFKD'
を使って結果を確認します.
normalized_s1 = unicodedata.normalize('NFC', s1)
normalized_s2 = unicodedata.normalize('NFC', s2)
print_ord(normalized_s1)
print_ord(normalized_s2)
1 : 0x31 1 : 0xff11
normalized_s1 = unicodedata.normalize('NFKC', s1)
normalized_s2 = unicodedata.normalize('NFKC', s2)
print_ord(normalized_s1)
print_ord(normalized_s2)
1 : 0x31 1 : 0x31
normalized_s1 = unicodedata.normalize('NFD', s1)
normalized_s2 = unicodedata.normalize('NFD', s2)
print_ord(normalized_s1)
print_ord(normalized_s2)
1 : 0x31 1 : 0xff11
normalized_s1 = unicodedata.normalize('NFKD', s1)
normalized_s2 = unicodedata.normalize('NFKD', s2)
print_ord(normalized_s1)
print_ord(normalized_s2)
1 : 0x31 1 : 0x31
「1」と「1」,「A」と「A」,「ア」と「ア」,「(」と「(」,「半角空白」と「全角空白」について上の5つのサンプルコードを実行した結果をまとめると次のようになりました.この表を見る限り,全角文字と半角文字の表記の揺れを統一するだけであれば,'NFKC'
でも 'NFKD'
でも良さそうです.具体的には,アルファベット,数字,記号は半角に統一され,カナは全角に統一されます.
文字 | 処理なし | 'NFC' | 'NFKC' | 'NFD' | 'NFKD' |
---|---|---|---|---|---|
1 |
0x31 | 0x31 | 0x31 | 0x31 | 0x31 |
1 |
0xff11 | 0xff11 | 0x31 | 0xff11 | 0x31 |
A |
0x41 | 0x41 | 0x41 | 0x41 | 0x41 |
A |
0xff21 | 0xff21 | 0x41 | 0xff21 | 0x41 |
ア |
0xff71 | 0xff71 | 0x30a2 | 0xff71 | 0x30a2 |
ア |
0x30a2 | 0x30a2 | 0x30a2 | 0x30a2 | 0x30a2 |
( |
0x28 | 0x28 | 0x28 | 0x28 | 0x28 |
( |
0xff08 | 0xff08 | 0x28 | 0xff08 | 0x28 |
(半角空白) |
0x20 | 0x20 | 0x20 | 0x20 | 0x20 |
(全角空白) |
0x3000 | 0x3000 | 0x20 | 0x3000 | 0x20 |
次は「km」という2文字と「㎞」という1文字がどのように正規化されるか,「ヘクタール」という5文字と「㌶」という1文字がどのように正規化されるかを分析した結果を表に示します.この結果,'NFKC'
や 'NFKD'
では「km」(2文字)や「ヘクタール」に変換されることがわかります.
文字(列) | 処理なし | 'NFC' | 'NFKC' | 'NFD' | 'NFKD' |
---|---|---|---|---|---|
km |
0x6b 0x6d | 0x6b 0x6d | 0x6b 0x6d | 0x6b 0x6d | 0x6b 0x6d |
㎞ |
0x339e | 0x339e | 0x6b 0x6d | 0x339e | 0x6b 0x6d |
ヘクタール |
0x30d8 0x30af 0x30bf 0x30fc 0x30eb | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb |
㌶ |
0x3336 | 0x3336 | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb | 0x3336 | 0x30d8 0x30af 0x30bf 0x30fc 0x30eb |
次は「ヴ」という一文字と,「ウ」と「 ゙ 」 からなる合字である「ヴ」についてどのように処理されるか分析します.この2つの文字は人間の目には同じに見えますが,一文字「ヴ」の文字コードは「0x30f4」で,合字「ヴ」の文字コードは「0x30a6 0x3099」であることから,内部的には同じではありません(これは検索などで期待した結果にならない可能性があることを意味しています).しかしながら, unicodedata.normalize()
関数を使うとどちらかに統一できることがわかります.具体的には,'NFC'
や 'NFKC'
を使うと,つまりここの説明にある正準等価を使うと,一文字「ヴ」に正規化され,'NFD'
や 'NFKD'
の互換等価を使うと合字「ヴ」に正規化されます.この結果からも日本語文字列を取り扱う場合は 'NFKC'
が最も適していると考えられます.
文字 | 処理なし | 'NFC' | 'NFKC' | 'NFD' | 'NFKD' |
---|---|---|---|---|---|
ヴ |
0x30f4 | 0x30f4 | 0x30f4 | 0x30a6 0x3099 | 0x30a6 0x3099 |
ヴ (合字) |
0x30a6 0x3099 | 0x30f4 | 0x30f4 | 0x30a6 0x3099 | 0x30a6 0x3099 |
次は合字「fi
」が unicodedata.normalize()
関数でどのように処理されるか分析します.なお Mac では option + shift + 5 で「fi
」を入力できます.次の表から 'NFKC'
や 'NFKD'
を指定すると,「f
」と「i
」に分割されることがわかります.なお,合字「fi
」は英語の学術論文PDFファイルの中などでも頻繁に利用されています.見た目は美しくなるのですが,PDFファイル内を「fi
」で検索しても「fi
」がヒットしないという欠点があります.つまり,検索システムを開発するときに検索対象文字列の正規化を適切に行うと,より精度の高い検索システムができることになります.
文字(列) | 処理なし | 'NFC' | 'NFKC' | 'NFD' | 'NFKD' |
---|---|---|---|---|---|
fi |
0x66 0x69 | 0x66 0x69 | 0x66 0x69 | 0x66 0x69 | 0x66 0x69 |
fi |
0xfb01 | 0xfb01 | 0x66 0x69 | 0xfb01 | 0x66 0x69 |
さらに,ドイツ語のウムラウト「ü」,ノルウェー語などの上リング付き「å」が unicodedata.normalize()
関数でどのように処理されるか分析します.なお Mac では option + u のあとに u で「ü」を入力でき,option + a で「å」を入力できます.次の表から 互換等価である 'NFD'
や 'NFKD'
を指定すると「u」や「a」とウムラウト(またはリング)に分割されることがわかります.
文字 | 処理なし | 'NFC' | 'NFKC' | 'NFD' | 'NFKD' |
---|---|---|---|---|---|
u |
0x75 | 0x75 | 0x75 | 0x75 | 0x75 |
ü |
0xfc | 0xfc | 0xfc | 0x75 0x308 | 0x75 0x308 |
a |
0x61 | 0x61 | 0x61 | 0x61 | 0x61 |
å |
0xe5 | 0xe5 | 0xe5 | 0x61 0x30a | 0x61 0x30a |
以上,まとめると日本語の文字列を扱う場合には 'NFKC'
を指定するのが最も良さそうです.欧州のウムラウトなどを扱う場合にはその目的によって 'NFKC'
と 'NFKD'
を使い分ける必要がありそうです.さらに,日本,中国,台湾,韓国で使われる漢字については,同じ形や同じ意味の漢字が言語ごとにそれぞれ Unicode 表の別の場所に定義されている関係で,その取扱は難しそうです.このような問題については,京都のある大学の先生と頻繁に議論していますが,調べてみると根深い問題が潜んでいます.