UTF-8にもいろいろある
前回からの続き。
改行コードの違いも知った。文字コードとロケール、ターミナルの言語環境との関係も知った。これで文字にまつわる悩みとはおさらばできると思ったら、まだダメだった...。
実験環境
体感する道具
- Xcodeをインストール済みであること。
- Homebrewをインストール済みであること。
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- nkfコマンドをインストール済みであること。
$ brew install nkf
NFDとNFC
grep検索で見つからない語句がある
- ターミナルで新規タブを開いて、
- まずは実験用のディレクトリを作って、ファイルを二つ追加してみた。
$ mkdir utf-8-mac $ cd utf-8-mac $ >お読みください.txt $ >読みましょう.txt $ ls 読みましょう.txt お読みください.txt
- Finderで開くとこんな感じで見えているはず。
$ open .
- Finderで選択してコピー、
- 標準テキストなテキストエディットにペースト。
- file_list.txtというファイル名で保存した。
- file_list.txtをcatしても、例の如く、ちゃんと見えない。
$ cat file_list.txt
読みましょう.txt
- 改行コードを変換すれば、正常に表示される。
$ cat file_list.txt | nkf -w -Lu
お読みください.txt
読みましょう.txt
- ここまでは、前回までに理解した事実である。
- 今回は、これをgrep検索してみる。
$ cat file_list.txt | nkf -w -Lu | grep お読みください $ cat file_list.txt | nkf -w -Lu | grep 読みましょう 読みましょう.txt
- すると、「読みましょう」はヒットするのに、「お読みください」はヒットしない...。
grep検索でちゃんと見つかる場合
- 一方、まったく同じ内容をテキストエディットで手入力したファイルも作ってみる。
- input_list.txtというファイル名で保存した。
$ cat input_list.txt
お読みください.txt
読みましょう.txt
$ cat input_list.txt | nkf -w -Lu | grep お読みください お読みください.txt $ cat input_list.txt | nkf -w -Lu | grep 読みましょう 読みましょう.txt
- 今度はちゃんと「お読みください」もヒットした!
違いを見る
- こうゆう時は、文字コードの違いを見てしまうのが手っ取り早い。
- お馴染みのodコマンドで比較してみた。
$ cat file_list.txt | nkf -wLu | od -tx1c 0000000 e3 81 8a e8 aa ad e3 81 bf e3 81 8f e3 81 9f e3 お ** ** 読 ** ** み ** ** く ** ** た ** ** ゙ 0000020 82 99 e3 81 95 e3 81 84 2e 74 78 74 0a e8 aa ad ** ** さ ** ** い ** ** . t x t \n 読 ** ** 0000040 e3 81 bf e3 81 be e3 81 97 e3 82 87 e3 81 86 2e み ** ** ま ** ** し ** ** ょ ** ** う ** ** . 0000060 74 78 74 t x t 0000063
$ cat input_list.txt | nkf -wLu | od -tx1c 0000000 e3 81 8a e8 aa ad e3 81 bf e3 81 8f e3 81 a0 e3 お ** ** 読 ** ** み ** ** く ** ** だ ** ** さ 0000020 81 95 e3 81 84 2e 74 78 74 0a e8 aa ad e3 81 bf ** ** い ** ** . t x t \n 読 ** ** み ** ** 0000040 e3 81 be e3 81 97 e3 82 87 e3 81 86 2e 74 78 74 ま ** ** し ** ** ょ ** ** う ** ** . t x t 0000060
- 注目すべきはオレンジ色の太字の部分。
- Finderからコピーしたファイルの方は「た」+「゛」つまり、結合された2文字。
- この濁点は、「た」に結合する文字幅なしの濁点U+3099である。
- ことえりから入力可能な1文字分の幅を持つ濁点U+309Bではない。
- 一方、手入力したファイルの方は「だ」つまり、単独の1文字。
- このように外見上まったく同じ文字でも、UTF-8には二つの表現方法があるのだ。
「た」+「゛」 | 結合された2文字 | NFD(Normalization Form Canonical Decomposition) | 例:OSXのHFS+ファイルシステムではファイルパスはNFDなUTF-8で統一されている。 |
「だ」 | 単独の1文字 | NFC(Normalization Form Canonical Composition) | 例:ターミナルのUTF-8ではどちらに統一される訳でもなく、入力されたままのUTF-8で処理される。 (キーボードからの入力はNFCなUTF-8で受け取っているようだ) |
どちらかに統一する
- 正しくgrep検索できるようにするためには、NFDかNFCのどちらかに統一して処理すれば良いはず。
- 例えば、grepの引数の「お読みください」をキーボードから入力せず、Finderからコピーした「お読みください」にしてみる。
$ cat file_list.txt | nkf -wLu | grep お読みください
お読みください.txt
- 上記結果のとおり、NFDの「お読みくた゛さい」でgrep検索する分には、ちゃんとヒットするのだ!
NFDとNFCを変換する
$ cat file_list.txt | nkf -wLu --ic=UTF8-MAC | grep お読みください
お読みください.txt
- 最新のnkf((バージョン2.1以降のnkfで確認した。
$ nkf --version
))は、入力側でNFDなUTF-8だよと明示することで、出力側でNFCなUTF-8に変換してくれるのだ。
Network Kanji Filter Version 2.1.0 (2009-11-17)
Copyright (C) 1987, FUJITSU LTD. (I.Ichikawa).
Copyright (C) 1996-2009, The nkf Project.
- 但し、その逆はできないようだ。
$ cat file_list.txt | nkf -wLu | grep `echo お読みください | nkf -wLu --oc=UTF8-MAC`
$ cat file_list.txt | nkf -wLu | iconv -f UTF-8-MAC -t UTF-8 | grep お読みください お読みください.txt $ cat file_list.txt | nkf -wLu | grep `echo お読みください | iconv -f UTF-8 -t UTF-8-MAC` お読みください.txt
NFDとUTF8-MAC
Characters in the ranges U2000-U2FFF, UF900-UFA6A, and U2F800-U2FA1D are not decomposed.
https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpinternational/Articles/FileEncodings.html
- なぜ標準のNFDを使わないかというと、標準のNFDの規定どおりに変換すると、一部で文字化けしてしまうのである。
- 単独の1文字を分解する過程で、別の字形に変化して、元の字形に戻せない文字があるのだ。
- 例:神(示申)→神(ネ申)に変化してしまう。
- 標準のNFDの目的は重複を排除する正規化にあるようだが、それによって字形まで変化してしまうと困る場合もあるのだ。
- そのような困った状況を避けるために、AppleはHFS+の正規化にNFDをそのまま適用するのではなく、字形が変化してしまう一部の文字を除外して正規化する仕組みにした。
- このApple独自のNFD仕様が、nkfやiconvにおいて、便宜的にUTF8-MACと呼ばれているのだ。
但し、このUTF8-MACも完璧ではなく、若干の不具合もある。例えば...
- ターミナルでは神(示申)も神(ネ申)と表示されてしまう。(OSX 10.6にて確認。OSX 10.9では解消されていた)
- 表示は「ネ申」となってしまうが、ことえりの変換で「示申」を指定すれば、示申.txtも作成可能。
$ >示申.txt
- しかし、Finderで示申.txtを作ろうとしても、作れない。
- 名前を確定した時に、ネ申.txtに自動変換されてしまう。
- この文字を含むテキストをiconvで変換しようとしても、エラーが出て変換できない。
$ ls 神.txt 神.txt 冬.txt $ ls | iconv -f utf8-mac -t utf8 神.txt 神.txt iconv: (stdin):5:0: cannot convert
- 上記文字以外にも、0面*2以外のユニコードでは同じ状況に陥る。
- 例えば、U+1D100から始まる楽譜を表現する文字なども、ことごとくエラーになる。
- おっと、nkfなら正常に変換できる。素晴らしい。
$ ls | nkf -wLu --ic=utf8-mac
神.txt
神.txt
冬.txt
この辺りの話は、以下の参考ページが詳しい。非常に興味深い内容である。
BOMについて
- BOMとは、Byte Order Mark(バイトオーダーマーク=バイト列の並び順マーク)のことである。
- そもそもはUTF-16などで、2バイト以上の読み込み順序を、どちらにするかを判別するために必要であった。
- 上位桁から読み込むのか、下位桁から読み込むのか、
ファイルの位置 | データ |
---|---|
1バイト目 | AB |
2バイト目 | CD |
: | : |
- 上記データをABCDと解釈するなら、ビック・エディアン。ファイルの先頭に16進数データのFE FFが付加されている。
- 上記データをCDABと解釈するなら、リトル・エディアン。ファイルの先頭に16進数データのFF FEが付加されている。
- この、FE FFまたはFF FEこそが、BOM(Byte Order Mark)である。
BOMとは、ファイルに記録されたデータの並びを、どちらの順序で解釈すべきかのマークなのだ。
UTF-8のBOM
- ところで、UTF-8のファイルの先頭にもBOMが付加されることがある。
- そのBOMは、16進数データのEF BB BFという並びである。
- しかし本来、UTF-8においては、BOMは不要である。
- 1バイトごとに区切られたデータを順に読み込むことになっているので。
- UTF-8におけるBOMは、このファイルがUTF-8でエンコードされているという目印でしかない。
- 基本的に付加しなくても良いはずなのだけど...
- アプリケーションや環境によっては、BOMがないと正常に表示できない場合もある。
- 逆に、BOMが付加されていることで、正常に表示できない場合もある。
- UTF-8と言えども、必要に応じてBOMを付加したり、削除したりする技を身につけておきたい。
BOMの操作
- ごく普通にhello.txtを作ってみた。
$ echo hello > hello.txt
- このhello.txtにBOMは存在しない。
$ od -tx1c hello.txt
0000000 68 65 6c 6c 6f 0a
h e l l o \n
0000006
- hello.txtにBOMを追加してみる。
- nkfコマンドで簡単に追加できる。
$ nkf -w8 hello.txt > hello_bom.txt $ od -tx1c hello_bom.txt 0000000 ef bb bf 68 65 6c 6c 6f 0a 357 273 277 h e l l o \n 0000011
- ファイルの先頭にEF BB BFが追加された。
- nkfコマンドならBOMを削除するのも簡単。
$ nkf -w hello_bom.txt | od -tx1c
0000000 68 65 6c 6c 6f 0a
h e l l o \n
0000006
- BOM付きのファイルを連結してみる。
$ echo world | nkf -w8 > world_bom.txt $ od -tx1c world_bom.txt 0000000 ef bb bf 77 6f 72 6c 64 0a 357 273 277 w o r l d \n 0000011 $ cat hello_bom.txt world_bom.txt hello world
- catコマンドはhelloとworldしか表示しないけど...
$ cat hello_bom.txt world_bom.txt | od -tx1c
0000000 ef bb bf 68 65 6c 6c 6f 0a ef bb bf 77 6f 72 6c
357 273 277 h e l l o \n 357 273 277 w o r l
0000020 64 0a
d \n
0000022
- そのファイルの中には二つのBOMを含んでいる。
$ cat hello_bom.txt world_bom.txt | nkf -w | od -tx1c
0000000 68 65 6c 6c 6f 0a ef bb bf 77 6f 72 6c 64 0a
h e l l o \n 357 273 277 w o r l d \n
0000017
- かくなる上は、sedコマンドでやってみる。
$ cat hello_bom.txt world_bom.txt | sed $'s/\xef\xbb\xbf//g' | od -tx1c
0000000 68 65 6c 6c 6f 0a 77 6f 72 6c 64 0a
h e l l o \n w o r l d \n
0000014
- これでどうにかBOMをきれいに削除できた。
OSX環境で、日本語を扱う上で覚えておくと幸せになれそうなコマンドは...
コマンド | 意味 |
---|---|
export LANG=ja_JP.UTF-8 | コマンド環境のロケールをUTF-8に設定する |
iconv -f utf8 -t utf8-mac | NFCをMAC仕様のNFDに変換する |
iconv -f utf8-mac -t utf8 | MAC仕様のNFDをNFCに変換する |
nkf -wLu --ic=utf8-mac | BOMなし、改行コードLF、NFCなUTF-8に変換する |
nkf -w8 | BOM付きUTF-8に変換する |
sed $'s/\xef\xbb\xbf//g' | ファイル中のBOMをすべて削除する |
これで前回からの続き物は、ひとまず、完。
Unicodeの背景
はてぶやTwitterのコメントを見ていて、ミスリードを誘ってはいけないと感じたので追記。
- Unicodeが悪とか、UTF-8-MACが悪とか、そのような一言で片付けられる問題ではないと思っている。
- それぞれの仕様、そして実装に至るまでには、歴史的な背景や既存の規格との互換性の問題など、ざまざまな事情がある。
- 何らかの問題を解決しようとして仕様が追加されるが...
- 追加された仕様が、また別の問題を引き起こす。
- だからと言って、その仕様を追加しなければ、現状の問題さえ解決できない。
この辺の事情は、以下のページがたいへん詳しく、興味深い。(関連する記事のみ抜粋)
- “情報化時代”に追いつけるか? 審議が進む「新常用漢字表(仮)」: 第2部 新常用漢字表と文字コード規格第3回 互換漢字という「例外」
- “情報化時代”に追いつけるか? 審議が進む「新常用漢字表(仮)」: 第2部 新常用漢字表と文字コード規格第4回 互換漢字をめぐる非漢字圏諸国との「波風」
- “情報化時代”に追いつけるか? 審議が進む「新常用漢字表(仮)」: 第2部 新常用漢字表と文字コード規格第5回 なぜUnicode正規化は生まれたか
- “情報化時代”に追いつけるか? 審議が進む「新常用漢字表(仮)」: 第2部 新常用漢字表と文字コード規格第6回 重複符号化を排除するUnicode正規化と互換漢字
- “情報化時代”に追いつけるか? 審議が進む「新常用漢字表(仮)」: 第2部 新常用漢字表と文字コード規格第7回 Unicode正規化と互換漢字
- 上記ページにとどまらず、検索して、すべての記事を熟読しておくべきかもしれない。
- そもそも人の使う言語は、とても曖昧なもので、文字も曖昧なもの。
- 曖昧なものを符号化して白黒はっきりさせる過程で、様々な歪みが表面化してくる。
- その歪みを最小限に抑え、Unicodeを使う人たちの幸福度を最大化するのは、永遠の課題なのかもしれない。