📖

あなたの知らない UTF-8 の世界

2022/12/14に公開
bo.py
if "ボボボーボ・ボーボボ" == "ボボボーボ・ボーボボ":
    print("same")
else:
    print("different")

これの実行結果はどうなるでしょう?

はじめに

この記事は、公立はこだて未来大学 Advent Calendar 2022 part3 の14日目の記事です。
https://adventar.org/calendars/7655

近年のコンピュータは、Unicode を用いて多種多様な文字を表示することができます。この Unicode にはおもしろ機能などが様々あるので、ちょっと遊んでみようと思います。

アハ体験

bo.py
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)
互換等価性によって分解した後に結合

これらを用いることにより、人間の認知に近い形での文字列比較ができます。これを使って正規化をしましょう。

今回は、ボ(通常のボ)ホ+濁点を見分けられるようにして、ボ(通常のボ)の方に統一したいため、NFCNFKCで正規化すればよさそうです。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さん
環境構築なしでらくらくプログラミング、やりたくないですか?
脚注
  1. ざっくりいうと、見た目が同じもの ↩︎

  2. ざっくりいうと、表す意味が同じもの ↩︎

  3. Pythonであれば、neologdnなどを使うと良いと思います ↩︎

Discussion