"ページ"は"ページ"じゃない? Unicodeの正規化形式で大混乱した話
はじめに
Pythonを使って業務効率化を図っていた時、とある現象に遭遇しました。
筆者が行っていた業務効率化は、以下の画像加工(リサイズ及びリネーム)処理になります。
- 環境:
Apple M1,macOS 15.5 Sequoia,Python 3.13.1
- Adobe Acrobat でpdfファイルを開いて、画像の書き出し(jpg)を実行
- 筆者自作のPythonシステムで(書き出された)各画像のリサイズ(
pillow)及び、ファイル名の一部置換(リネーム)処理を実施
上記リネーム処理時に、ファイル名にあるページという文字列が適切に認識されず、全然うまくリネームできない現象に遭遇したのです。
結論
結論を先に書くと、今回の原因はUnicodeの正規化形式にありました。
「へ + ゜ + ー + シ + ゛」のように、文字と半濁点・濁点が分離された構造になる
筆者が遭遇した、全然うまくリネームできない現象とは具体的には以下となります。
`ページ`という文字列が以下のように扱われる
「`へ`+`゜` + `ー` + `シ`+`゛`」
当初、なぜ置換できないのか全く理解できませんでしたが、pdfファイルから抽出された「ページ」文字列をコピペして以下のように貼り付けると機能したのです!
new_filepath = normalized_path.replace("ページ", "page")
os.rename(jpgfile, new_filepath)
return new_filepath
そして、その後手打ちでページと入力し直して実行すると元通り動かなくなりました。
「え?ページとページって何か違うの?」と、大混乱の状況に陥った筆者。
何気なくコピペした方のページという文字列に対して操作すると「へ + ゜ + ー + シ + ゛」という文字と半濁点・濁点が分離された構造になっていることに気づきました……。
これは「Unicodeの正規化形式」の影響によるそうです。
Unicodeって?
Unicodeは、世界中の文字をコンピューター上で表現するための規約です。
蛇足ですが、UTF-8/UTF-16/UTF-32などは、Unicodeのコードポイントを、実際にバイト列として記録・送信するためのエンコーディング方式(符号化方式)となります。
コードポイント(文字の定義)とは、各文字に一意な番号(例:U+3042 = あ)を割り当てたものを指します。
このUnicodeという規約の中に「正規化ルール」という 「見た目は同じでも構成が違う文字」を統一的に扱うための変換規則 があり、これが筆者の大混乱を引き起こしていました。
Unicodeの正規化形式について
-
NFC(Normalization Form Canonical Composition)
- Windowsや多くのLinuxディストリビューションではNFC形式が一般的
- 正準等価性による正規化
- 文字を結合して最小の形式にする
- 例:
é→é(単一の合成文字)
-
NFD(Normalization Form Canonical Decomposition)
- macOSのファイルシステムではNFD形式が使用される
- 正準等価性による分解
- 文字を基底文字と結合文字に分解
- 例:
é→e+´(基底文字 + 結合文字)
-
NFKC(Normalization Form Compatibility Composition)
- 互換等価性による正規化と結合
- 見た目が似ている文字を共通の形式に変換して結合
- 例:ページ → ページ(互換文字を正規化して単一の合成文字に)
-
NFKD(Normalization Form Compatibility Decomposition)
- 互換等価性による分解
- 互換文字を正規化して基底文字と結合文字に分解
- 例:ページ → へ + ゜ + ー + シ + ゛
以下のように正規化処理を挟むことで通常通り文字入力しても期待した処理を行ってくれるようになりました。
+ import unicodedata
+ normalized_path = unicodedata.normalize("NFKC", jpgfile)
new_filepath = normalized_path.replace("ページ", "page")
os.rename(jpgfile, new_filepath)
return new_filepath
さいごに
今回の件は、OSごとに正規化形式が異なるのを知らなかった筆者の基礎知識の浅さに起因します。
有識者からすると「なんだそんなことか」と思われたかもしれませんが、筆者の自戒記事が同一文字列なのに機能しない、処理が進まない、といったトラブルに遭遇した方々のお役に立てれば幸いです。
ここまで読んでいただき、ありがとうございました。
Discussion