あなたの知らない UTF-8 の世界
if "ボボボーボ・ボーボボ" == "ボボボーボ・ボーボボ":
    print("same")
else:
    print("different")
これの実行結果はどうなるでしょう?
はじめに
この記事は、公立はこだて未来大学 Advent Calendar 2022 part3 の14日目の記事です。
近年のコンピュータは、Unicode を用いて多種多様な文字を表示することができます。この Unicode にはおもしろ機能などが様々あるので、ちょっと遊んでみようと思います。
アハ体験
if "ボボボーボ・ボーボボ" == "ボボボーボ・ボーボボ":
    print("same")
else:
    print("different")
さて、このPythonのコードの実行結果はどうなると思いますか?未来大生なら、多分どこかにPythonの環境を持っていると思うので (データサイエンス入門の環境など)、試しに実行してみてください。結果はどうなりましたか?
実行結果
different
どうでしょう?予想は当たりましたか?
どうしてこんなことに
ボボボーボ・ボーボボ と ボボボーボ・ボーボボ は、人間から見ると全く同じに見えます。実はフォントの表示も全く同じなため、見分ける手段はありません。しかし、コンピュータから見たら異なる文字列なのです。
文字コードレベルで確認してみましょう。
print("ボボボーボ・ボーボボ".encode('unicode_escape').decode())
print("ボボボーボ・ボーボボ".encode('unicode_escape').decode())
というコードで、文字列がコンピュータからどのように見えているか確認します。
すると、
\u30dc\u30dc\u30dc\u30fc\u30dc\u30fb\u30dc\u30fc\u30dc\u30dc
\u30db\u3099\u30db\u3099\u30db\u3099\u30fc\u30db\u3099\u30fb\u30db\u3099\u30fc\u30db\u3099\u30db\u3099
どうやら中身が全然違うようです。この文字列には、ボとーと・という文字列が含まれているはずです。これらはコンピュータ上では、下のようにあらわされます。
| 文字 | Code | 
|---|---|
| ボ | \u30dc | 
| ー | \u30fc | 
| ・ | \u30fb | 
ここまでで分かったところを解釈して置き換えてみましょう。
ボボボーボ・ボーボボ
\u30db\u3099\u30db\u3099\u30db\u3099ー\u30db\u3099・\u30db\u3099ー\u30db\u3099\u30db\u3099
\u30db \u3099という文字が書き込まれていることが分かりました。これはなんなんでしょうか。それぞれ確認してみましょう。
print("\u30db")
print("\u3099")
ホ
゙
この2つはホと濁点を表してるようです。
ボボボーボ・ボーボボ
ホ(濁点)ホ(濁点)ホ(濁点)ーホ(濁点)・ホ(濁点)ーホ(濁点)ホ(濁点)
という文字列だったわけです。
Unicode には結合文字列なるものがあり、2つの文字で1つの文字を表すという方法があります。濁点がつけられる文字の後に゙をつけると、濁点を付けることができます。同様に半濁点でも可能です。
対策
これらを見分けずに比較したいことがあると思います。そういう時は、正規化をしましょう。正規化については、Unicode® Standard Annex #15 - UNICODE NORMALIZATION FORMS に詳しく書いてあります。まとめると、以下の4種類の正規化があります。
| 名称 | 説明 | 
|---|---|
| NFD (Normalization Form Canonical Decomposition) | 正準等価性[1]よって分解 | 
| NFC (Normalization Form Canonical Composition) | 正準等価性によって分解した後に結合 | 
| NFKD (Normalization Form Compatibility Decomposition) | 互換等価性[2]によって分解 | 
| NFKC (Normalization Form Compatibility Composition) | 互換等価性によって分解した後に結合 | 
これらを用いることにより、人間の認知に近い形での文字列比較ができます。これを使って正規化をしましょう。
今回は、ボ(通常のボ)とホ+濁点を見分けられるようにして、ボ(通常のボ)の方に統一したいため、NFCかNFKCで正規化すればよさそうです。pythonではunicodedataパッケージでこれらの正規化をすることができます。
import unicodedata
b1 = "ボボボーボ・ボーボボ"
b1_n = unicodedata.normalize("NFKC", b1)
b2 = "ボボボーボ・ボーボボ"
b2_n = unicodedata.normalize("NFKC", b2)
if b1 == b2:
    print("same")
else:
    print("different")
if b1_n == b2_n:
    print("same")
else:
    print("different")
print(b2.encode('unicode_escape').decode())
print(b2_n.encode('unicode_escape').decode())
different
same
\u30db\u3099\u30db\u3099\u30db\u3099\u30fc\u30db\u3099\u30fb\u30db\u3099\u30fc\u30db\u3099\u30db\u3099
\u30dc\u30dc\u30dc\u30fc\u30dc\u30fb\u30dc\u30fc\u30dc\u30dc
正しく正規化され、いい感じに比較ができました。
おわりに
Unicode文字列を扱う際には、正規化にも気を付けましょう。特に、自然言語処理をする際には、これらのほかにも、よく似た文字などの正規化をしないと上手く処理が出来なかったりするので注意が必要です[3]。ということで、良い文字列処理ライフを!
のりかえ
| Part | 12/13 | 12/14 | 12/15 | 
|---|---|---|---|
| Part1 | くっしーさん FUNラジの部員数が○○人になった件について | バッシュさん 函館バスの系統番号をさらに改善しようぜ | 吾妻ちとせさん ピクセル×と×像 | 
| Part2 | あきめくさん 春休み海外行かん?? | しっしー(竹フクロウ)さん TCG研究の話とか学んだことを書きます | tomohihi6さん 大学院には行くな! | 
| Part3 | K.K.さん サルでもできる!まらしぃライクなピアノアレンジ講座 | 当記事 あなたの知らないUTF-8の世界 | _n_elさん 環境構築なしでらくらくプログラミング、やりたくないですか? | 


Discussion