Edited at

nkfを使わずにUTF-8↔Unicode相互変換やってみた

More than 3 years have passed since last update.


はじめに


  • この記事は第2のドワンゴ Advent Calendar 2015の16日目です。



  • 誰?


    • @nyango

    • 2015年度ドワンゴ新卒

    • 業務ではScalaを扱ってます。



  • 何をやったの?


    • nkfを使わずシェルスクリプトでUnicode↔UTF-8の文字コード相互変換書いてみた。

    • nkfを使うとより簡単。




  • 何故そんなことを…


    • 業務で文字コード変換を行う機会があった。ふと、UTF-8のような定番文字コードの定義を自分は知らないなと思い立った。実装してみたくなった。

    • UTF-16はほとんどの場合固定長でUTF-32は固定長文字コードなので、可変長文字コードの代表格であるUTF-8が一番面白そう。また簡単そうだった。

    • Scalaは楽しい。でも、たまには!もっとジャンクな!……そう、シェルスクリプトが書きたい!

    • ついカッとなってやってしまった。




nkfを使わずにUTF-8↔Unicode相互変換やってみた


16進数・10進数・2進数の変換

UTF-8に変換する都合上2進数表示が扱いたいので、まず基数の変換を考えてみたい。

dcという最古級のUnixプログラムだとこれが短く書けるようだ。

$ dc -e "10o 2i 10000 p"

16

"10o 2i 10000 p"dcにとって「10進数を出力、2進数を入力として"10000"をプリント」という意味になり、

dcに通すと結果として2進数の"10000"を10進数で表現した16が出力される。上手く基数変換が出来そうだ。

ただこれには注意すべき点があり、10o2iの順序を入れ替えてみると

$ dc -e "2i 10o 10000 p"

10000

となり、「2進数を入力、"10(2進数)"進数で出力」と解釈される。なんと入出力ともに2進数として解釈されてしまう。結果としては10000(2進数)が出力されてしまった。

古代語ならではのエグみが垣間見えた。

ただ、大きい基数を先に書き、次のものはそれにしたがって書けばこの問題は避けられそうだ。

では文字コード変換をやってみよう。


UTF-8の仕様・特徴

軽く仕様を確認する。


  • 8bit(UTF-8の由来、つまり1バイト)ごとに文字コードを読む

  • 2進数表示にしたとき先頭につく1の数で役割を変える


    • 先頭の1が0個(0で始まる)



      • 7bit (0x00-0x7F)



    • 先頭の1が1個(10で始まる)



      • 後続バイト(他の続きの内容を表す。6bit使える)



    • 先頭の1が2個(110で始まる)


      • 残り5bitと後続バイト1つ(6bit)で11bit (0x80-0x7FF)



    • 先頭の1が3個(1110で始まる)


      • 残り4bitと後続バイト2つ(6bitx2)で16bit (0x800-0x7FFF)



    • 先頭の1が4個(11110で始まる)


      • 残り3bitと後続バイト3つ(6bitx3)で21bit (0x10000-0x1FFFF)





  • 可変長

  • Unicodeの入れ物

  • 細かくはRFCやWikipedia

1バイトごとに2進数表示にしたとき最上位bitから連続する1の数で種類を区別、残りのbitでUnicodeを表現しているようだ。


UTF-8をUnicodeに(nkfなし)

$ echo -en "寿司(SUSHI)🍣食べたい🍖" | xxd -b -c1 | \

awk '{match($2, "^1*0"); if(RLENGTH!=2) print ""; printf substr($2,RLENGTH+1)}' | \
xargs -I ZZ dc -e "16o2i ZZ p"
5BFF
53F8
28
53
55
53
48
49
29
1F363
98DF
3079
305F
3044
1F356

上の仕様にしたがってUTF-8からUnicodeを取り出してみた。


  • やっていること


    • xxdでUTF-8のバイナリを2進数の文字列に

    • 1バイトごとに^1*0 にマッチする文字列長を数え、後続バイトになるか判定。後続バイトの場合は前のものにくっつける。

    • 2進数から16進数へ変換




UnicodeをUTF-8に(nkfなし)

$ echo 5bff 53f8 28 53 55 53 48 49 29 1f363 98df 3079 305f 3044 1f356 a | tr " " "\n" | tr a-f A-F | \

xargs -I ZZ dc -e "16i2o ZZ p" | awk 'BEGIN{a[1]=7;a[2]=11;a[3]=16;a[4]=21;}{for(i=1;i<5;i++){if(length($1)<=a[i]){b=sprintf("%0"a[i]"s",$1); if(i==1){print "0"b;break;}; h="";for(j=0;j<i;j++){h=sprintf("%s%s","1",h);}; printf h"0"substr(b,1,7-i); for(k=7-i+1;k<a[i];k+=6){ printf "10"substr(b,k,6); } print""; break;}}}' | \
xargs -I ZZ dc -e "10o2i ZZ p" | xargs -I ZZ printf "%08X" ZZ | xxd -r -ps
寿司(SUSHI)🍣食べたい🍖

逆にUnicodeをUTF-8にパッキングするものも書いてみた。こちらの方がやや煩雑。


  • やっていること


    • Unicodeを2進数の文字列に

    • 長さに応じてUTF-8の枠にパッキング

    • 2進数から16進数へ変換


    • xxd -r -psでバイナリに




nkfを使ってUTF-8↔Unicode相互変換やってみた


  • Unicode(UCS-4)とUTF-32(ビッグエンディアン、BOMなし)がほぼ等価。


    • コアとなるのはnkf -W -w32B0 および nkf -W32B -w のみ。



  • 実際にUnicodeとUTF-8を変換する必要があれば、これを使うべき。


UTF-8からUnicodeに

$ echo -en "寿司(SUSHI)🍣食べたい🍖" | nkf -W -w32B0 | xxd -ps -c4 | sed 's/^0*//'

5bff
53f8
28
53
55
53
48
49
29
1f363
98df
3079
305f
3044
1f356


UnicodeからUTF-8に

$ echo 5bff 53f8 28 53 55 53 48 49 29 1f363 98df 3079 305f 3044 1f356 a | tr " " "\n" | tr a-f A-F | \

xargs -I ZZ dc -e "16iAo ZZ p" | xargs printf "%08X" | xxd -ps -r | nkf -W32B -w
寿司(SUSHI)🍣食べたい🍖


おわりに


  • 仕様を読みシェルスクリプトで文字コードの変換をやってみた。

  • ワンライナーを作る途中、知らないコマンドやオプションが学べて楽しかった(OSX環境なのでBSDベース)。

  • シェルスクリプトは危険だけど、覚えた分だけ動くし、すぐに動くから楽しい。危険なものは楽しい。

  • 業務と毛色の違うものを使ってみることは刺激になる。


  • xxd -ps -r を使えば16進数をバイナリに変換できるので便利(POSIXコマンドではないけれど…)


    • 検索したら:!%xxdおよび:!%xxd -rがvimでバイナリをいじる時のおまじないとして紹介されていることが多かった。



  • 特に何かに入門はしてないが許してもらいたい。


  • 第2のドワンゴ Advent Calendar 2015、明日の担当は @dogwood008 さんです。