UTF-8にもいろいろある

前回からの続き。

改行コードの違いも知った。文字コードロケール、ターミナルの言語環境との関係も知った。これで文字にまつわる悩みとはおさらばできると思ったら、まだダメだった...。

実験環境

  • OSX 10.8 Mountain Lion以前((OSX 10.9 Mavericksでは、Mac仕様なNFDのUTF-8を表示しようとするとエラーになってしまったため、10.8以前の環境で実験した。
    Assertion failed: (width > 0), function conv_c, file /SourceCache/shell_cmds/shell_cmds-175/hexdump/conv.c, line 137.
    ** ** Abort trap: 6
    ))

体感する道具

  • 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
  • 同じように、grep検索してみる。
  • 不要かもしれないけど、条件を揃えるためにnkfも通しておいた。
$ 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で処理される。
  (キーボードからの入力はNFCUTF-8で受け取っているようだ)
  • Finderでファイル名をコピーした時の「お読みください」は、内部的には「お読みくた゛さい」となっているのだ*1
  • 一方、grepの引数として入力した「お読みください」は、内部的にも「お読みください」と変化していない。
どちらかに統一する
  • 正しくgrep検索できるようにするためには、NFDかNFCのどちらかに統一して処理すれば良いはず。
  • 例えば、grepの引数の「お読みください」をキーボードから入力せず、Finderからコピーした「お読みください」にしてみる。
$ cat file_list.txt | nkf -wLu | grep お読みください
お読みください.txt
  • 上記結果のとおり、NFDの「お読みくた゛さい」でgrep検索する分には、ちゃんとヒットするのだ!
  • しかし、NFDの「お読みくた゛さい」を入力するのは容易ではない。
  • 通常、キーボードから入力したテキストは、すべてNFCUTF-8で受け取るようだ。
  • Finderからファイルをコピーするか、テキストファイルを開いて「お読みください」をコピーするしかない。
  • 見つからないから検索するのに、最初から「お読みください」を選択できるならgrep検索なんて不要なはず。
NFDとNFCを変換する
  • NFDを直接入力することはできないが、変換することは簡単にできる。
  • お決まりのnkfコマンドには、--ic=UTF8-MACというオプションがある。
    • icは、input_codesetの意味。
$ cat file_list.txt | nkf -wLu --ic=UTF8-MAC | grep お読みください
お読みください.txt
  • 最新のnkf((バージョン2.1以降のnkfで確認した。
    $ nkf --version
    Network Kanji Filter Version 2.1.0 (2009-11-17)
    Copyright (C) 1987, FUJITSU LTD. (I.Ichikawa).
    Copyright (C) 1996-2009, The nkf Project.
    ))は、入力側でNFDなUTF-8だよと明示することで、出力側でNFCUTF-8に変換してくれるのだ。
  • 但し、その逆はできないようだ。
$ cat file_list.txt | nkf -wLu | grep `echo お読みください | nkf -wLu --oc=UTF8-MAC`
  • ところで、OSXには標準インストールされているiconvコマンドがある。
  • iconvコマンドなら、NFDとNFCの相互変換が可能である。素晴らしい。
$ 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
  • ところで、nkfやiconvで指定するUTF8-MACは、Unicode標準が規定するNFDとは若干異なる。

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に自動変換されてしまう。
  • 冬(=点の部分が「ン」のU+2F81A)の問題もある。
    • 以下コマンドにおいて、冬=点の部分が「ン」のU+2F81Aである。
  • この文字を含むテキストを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

この辺りの話は、以下の参考ページが詳しい。非常に興味深い内容である。

Unicode正規化に関する参考ページ
  • Unicode正規化については、とても奥が深い問題であり、すべてを正確に理解するのは大変である。(自分の理解もどうやら怪しい)
  • 以下、参考にさせて頂いたページ多数である。どのページもたいへん興味深く、読み入ってしまう。(素晴らしい情報に感謝です!)

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を含んでいる。
  • そして、nkfコマンドが削除できるのはファイル先頭のBOMだけ。
  • nkfコマンドと言えども、ファイル中の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 NFCMAC仕様のNFDに変換する
iconv -f utf8-mac -t utf8 MAC仕様のNFDをNFCに変換する
nkf -wLu --ic=utf8-mac BOMなし、改行コードLF、NFCUTF-8に変換する
nkf -w8 BOM付きUTF-8に変換する
sed $'s/\xef\xbb\xbf//g' ファイル中のBOMをすべて削除する


これで前回からの続き物は、ひとまず、完。


Unicodeの背景

はてぶやTwitterのコメントを見ていて、ミスリードを誘ってはいけないと感じたので追記。

  • Unicodeが悪とか、UTF-8-MACが悪とか、そのような一言で片付けられる問題ではないと思っている。
  • それぞれの仕様、そして実装に至るまでには、歴史的な背景や既存の規格との互換性の問題など、ざまざまな事情がある。
  • 何らかの問題を解決しようとして仕様が追加されるが...
    • 追加された仕様が、また別の問題を引き起こす。
    • だからと言って、その仕様を追加しなければ、現状の問題さえ解決できない。

この辺の事情は、以下のページがたいへん詳しく、興味深い。(関連する記事のみ抜粋)


  • そもそも人の使う言語は、とても曖昧なもので、文字も曖昧なもの。
  • 曖昧なものを符号化して白黒はっきりさせる過程で、様々な歪みが表面化してくる。
  • その歪みを最小限に抑え、Unicodeを使う人たちの幸福度を最大化するのは、永遠の課題なのかもしれない。

*1:正確には、単独の「゛」と「だ」に結合する濁点の文字コードは違う。

*2:Unicodeにおいて、U+0000〜U+FFFFの範囲が0面と呼ばれている。