Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2013-11-24

UTF-8にもいろいろある

前回からの続き。

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

実験環境

  • OSX 10.8 Mountain Lion以前*1
  • ターミナル
    • 言語環境:Unicode(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 .

f:id:zariganitosh:20131120153059p:image:w320


  • Finderで選択してコピー、

f:id:zariganitosh:20131120153252p:image:w320


  • 標準テキストなテキストエディットにペースト。
  • file_list.txtというファイル名で保存した。

f:id:zariganitosh:20131120153411p:image:w320


  • 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というファイル名で保存した。

f:id:zariganitosh:20131120154758p:image:w320

$ 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で処理される。
  (キーボードからの入力はNFCなUTF-8で受け取っているようだ)
  • Finderでファイル名をコピーした時の「お読みください」は、内部的には「お読みくた゛さい」となっているのだ*2
  • 一方、grepの引数として入力した「お読みください」は、内部的にも「お読みください」と変化していない。
どちらかに統一する
  • 正しくgrep検索できるようにするためには、NFDかNFCのどちらかに統一して処理すれば良いはず。
  • 例えば、grepの引数の「お読みください」をキーボードから入力せず、Finderからコピーした「お読みください」にしてみる。
$ cat file_list.txt | nkf -wLu | grep お読みください
お読みください.txt
  • 上記結果のとおり、NFDの「お読みくた゛さい」でgrep検索する分には、ちゃんとヒットするのだ!
  • しかし、NFDの「お読みくた゛さい」を入力するのは容易ではない。
  • 通常、キーボードから入力したテキストは、すべてNFCなUTF-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*3は、入力側でNFDなUTF-8だよと明示することで、出力側でNFCなUTF-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.

Page Not Found - Apple Developer
  • なぜ標準の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面*4以外のユニコードでは同じ状況に陥る。
  • 例えば、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-macNFCをMAC仕様のNFDに変換する
iconv -f utf8-mac -t utf8MAC仕様のNFDをNFCに変換する
nkf -wLu --ic=utf8-macBOMなし、改行コードLF、NFCなUTF-8に変換する
nkf -w8BOM付きUTF-8に変換する
sed $'s/\xef\xbb\xbf//g'ファイル中のBOMをすべて削除する

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


Unicodeの背景

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

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

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

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

*1: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

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

*3:バージョン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.

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

昔マック昔マック 2013/11/26 07:40  昔マックです。

SnowLeopardのgrepはバージョンが古く日本語処理に問題があります。
0:% /usr/bin/grep --version
grep (GNU grep) 2.5.1
Copyright 1988, 1992-1999, 2000, 2001 Free Software Foundation, Inc.
私は(元cygwinユーザーでもあるので)2.14を自分でmakeして使っています。
2.14のほうが日本語処理を通します。
どこからどこまで正確かはわかりませんが、
MountainLionやMavericksのgrepバージョンが気になるところです。

zariganitoshzariganitosh 2013/11/26 09:26 コメントありがとうございます。
確かにバージョンが古いですね。
調べてみると、SnowLeopardのgrepは2004年のリリース。

http://ftp.gnu.org/gnu/grep/

一方、Mavericksのgrepは2010年のリリース。(たまたまバージョン番号は同じですが、BSDのgrepなのでこちらは2010年リリース)
  $ grep --version
  grep (BSD grep) 2.5.1-FreeBSD

http://gihyo.jp/admin/clip/01/fdt/201008/03

しかし、Mavericksと言えども2年前なので、GNUの最新版のgrepをインストールして試してみました。
その結果、やはり最新のgrepと言えどもNFD・NFCの違いはそのまま反映されるようです。(上記日記と同じ結果になりました)

昔マック昔マック 2013/11/26 20:27  SnowLeopardとMavericksでgrepにGNUとBSDの違いがあるとは知りませんでした。
驚きです。コマンドの再実行もとても助かります。有難うございます。

OSのリリース年は関係なく2.5.1などのバージョン数だけが問題なのではないでしょうか?
>Mavericksのgrepは2010年のリリース。(たまたまバージョン番号は同じですが、BSDのgrepなのでこちらは2010年リリース)
というのは誤解を与える感じがします。BSDは2.5.1が2010年頃のGNUの2.6〜2.9あたりと同じ、あるいは最新と同じ、という流儀なのでしょうか?

ファイルの日付はビルドした日時なのでgrepコマンドファイルがどれであるかはversionしか無いと思います。

zariganitoshzariganitosh 2013/11/27 08:57 なるほど、2010年は9-CURRENTにマージされた時期であって、BSD grep 2.5.1がリリースされた年ではないのですね。
BSD grep 2.5.1っていつ頃のリリースなのかと思って調べてみました。

http://www.filewatcher.com/m/grep-2.5.1-57.fc7.i386.rpm.179063-0.html
http://www.filewatcher.com/m/grep-2.5.1-59.fc9.i386.rpm.186692-0.html

正確には分かりませんでしたが、およそ2007〜2008年頃かそれ以前、と思うことにしました。

DoraDora 2013/11/28 02:41 細かいことかもしれませんが、NFCやNFDというのは正規化の方式、つまり「変換の動作」の名称であって、UTF8のバイト列の「状態」を指す用語ではありません。
UTF8の規格上、1つのUTF8文字列の中に「た」+「゛」と「だ」の2種類の表現を混在させることも可能で、このような状態の文字列はNFCによってもNFDによっても得られるものではありません。どちらかの形式に「統一するための変換操作」がNFCやNFDです。

統一するために、どちらの正規化でもまずは
ftp://ftp.unicode.org/Public/UNIDATA/UnicodeData.txt
に定められたルール(セミコロンによって区切られた1列目の文字を6列目の文字に変換)に従って分解可能な文字を全て分解し、NFCの場合はその後結合可能な部分を全て結合します。

その前半の分解変換で、例えば示申(FA19)は UnicodeData.txt 中の
FA19;CJK COMPATIBILITY IDEOGRAPH-FA19;Lo;0;L;795E;;;;N;;;;;
に従ってネ申(795E)に変換され、後半の結合変換では変化しません。
それゆえ、NFC/NFDどちらの正規化を行っても、示申がネ申に変換されます。

zariganitoshzariganitosh 2013/11/28 10:55 コメントありがとうございます。
理解が深まりました。

> それゆえ、NFC/NFDどちらの正規化を行っても、示申がネ申に変換されます。

その変化を嫌って、AppleはNFDの正規化をそのまま適用せずに、一部を正規化しない例外を設けたのですよね。
具体的には「U2000-U2FFF, UF900-UFA6A, U2F800-U2FA1D」のUnicodeの範囲は正規化しないのです。
実際、コマンドラインから操作する分には、示申もネ申もちゃんと区別されています。
しかし、それを出力するターミナルや、ファイル名を入力する時のFinderの処理がちゃんと対応できていないのだと思っています。(OSX 10.6.8の環境において)
OSX 10.9の環境では、ターミナルの出力は改善されています。(Finderの処理はまだ問題がありますが)
あとはコマンドを操作する側で、合成済みの1文字なのか、組み合わされた2文字以上なのか、を常に意識しておくしかないと考えています。

トラックバック - http://d.hatena.ne.jp/zariganitosh/20131124/utf8_nfd_nfc_bom