2011年10月20日木曜日

日本語エンコーディングの可視化

世の中の文字コードはUnicodeに統一される方向で動いているようで、ウェブアプリを作っていた頃に文字コードに悩まされた者としては、慶ばしい限りです。が、諸事情でレガシーコード("Shift_JIS"とか)を扱う必要があり、このサイトで勉強し直しました。以下がエンコーディングのまとめです。なお、「符号化文字集合」については省略します。


ISO-2022-JP
メールに使われているエンコーディング。それ以外ではみたことない。JISコードと呼ばれるらしいけど、個人的にはあんまり聞いたことないです。
ISO-2022-JPはISO-2022準拠のエンコーディングなので、複数のテーブルをエスケープシーケンスによって切り替えて使います。(ISO-2011-JPの説明は複雑なのでここでは省略します。このページをみてください。)簡単に言うと、特定の制御文字列を送ることで、「これからアルファベットを使います」「これから漢字を使います」と、状態を切り替えることで、たった7bitでアルファベット、数字、漢字をすべて表現することができるのです。

エスケープシーケンス
  • これから送る文字はASCIIです: 0x0F, "ESC ( B", or "ESC ( J"
  • これから送る文字は漢字です: "ESC $ @", or "ESC $ B"
  • これから送る文字は補助漢字です: "ESC $ ( D"
  • これから送る文字は半角カタカナです: 0x0E, or "ESC ( I"
なお、初期状態はASCIIです。なので、アルファベットと数字だけからなるメールにはエスケープシーケンスはいっさい含まれていません。(未確認ですがそのはず)

使用領域
7bitの領域をどのように使っているのか図示します。なお、制御文字が水色、ASCIIがオレンジ、漢字・補助漢字が緑、半角カタカナがグレーです。



ASCII使用時:
01234567
00010203040506070
10111213141516171
20212223242526272
30313233343536373
40414243444546474
50515253545556575
60616263646566676
70717273747576777
80818283848586878
90919293949596979
a0a1a2a3a4a5a6a7a
b0b1b2b3b4b5b6b7b
c0c1c2c3c4c5c6c7c
d0d1d2d3d4d5d6d7d
e0e1e2e3e4e5e6e7e
f0f1f2f3f4f5f6f7f

漢字・補助漢字使用時:
01234567
00010203040506070
10111213141516171
20212223242526272
30313233343536373
40414243444546474
50515253545556575
60616263646566676
70717273747576777
80818283848586878
90919293949596979
a0a1a2a3a4a5a6a7a
b0b1b2b3b4b5b6b7b
c0c1c2c3c4c5c6c7c
d0d1d2d3d4d5d6d7d
e0e1e2e3e4e5e6e7e
f0f1f2f3f4f5f6f7f

半角カタカナ使用時:
01234567
00010203040506070
10111213141516171
20212223242526272
30313233343536373
40414243444546474
50515253545556575
60616263646566676
70717273747576777
80818283848586878
90919293949596979
a0a1a2a3a4a5a6a7a
b0b1b2b3b4b5b6b7b
c0c1c2c3c4c5c6c7c
d0d1d2d3d4d5d6d7d
e0e1e2e3e4e5e6e7e
f0f1f2f3f4f5f6f7f


Shift_JIS / Windows-31J など
依然としてクライアントサイドでは最も使われているエンコーディング。符号化文字集合が微妙に異なる親戚がたくさんいる。Shift_JISと呼ばれているものの多くは正確にはWindows-31Jであろう。
エスケープシーケンスを使わずASCII、漢字、半角カタカナを表現可能なのが便利だが、あるバイトを見たときに、それがASCIIなのか漢字を表現する2バイト目なのかを判別できない(場合がある)ので、波紋を呼んだりもした。

使用領域
1バイト目:
0123456789abcdef
000102030405060708090a0b0c0d0e0f0
101112131415161718191a1b1c1d1e1f1
202122232425262728292a2b2c2d2e2f2
303132333435363738393a3b3c3d3e3f3
404142434445464748494a4b4c4d4e4f4
505152535455565758595a5b5c5d5e5f5
606162636465666768696a6b6c6d6e6f6
707172737475767778797a7b7c7d7e7f7
808182838485868788898a8b8c8d8e8f8
a09192939495969798999a9b9c9d9e9f9
a0a1a2a3a4a5a6a7a8a9aaabacadaeafa
b0b1b2b3b4b5b6b7b8b9babbbcbdbebfb
c0c1c2c3c4c5c6c7c8c9cacbcccdcecfc
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfd
e0e1e2e3e4e5e6e7e8e9eaebecedeeefe
f0f1f2f3f4f5f6f7f8f9fafbfcfdfefff

2バイト目:
0123456789abcdef
000102030405060708090a0b0c0d0e0f0
101112131415161718191a1b1c1d1e1f1
202122232425262728292a2b2c2d2e2f2
303132333435363738393a3b3c3d3e3f3
404142434445464748494a4b4c4d4e4f4
505152535455565758595a5b5c5d5e5f5
606162636465666768696a6b6c6d6e6f6
707172737475767778797a7b7c7d7e7f7
808182838485868788898a8b8c8d8e8f8
909192939495969798999a9b9c9d9e9f9
a0a1a2a3a4a5a6a7a8a9aaabacadaeafa
b0b1b2b3b4b5b6b7b8b9babbbcbdbebfb
c0c1c2c3c4c5c6c7c8c9cacbcccdcecfc
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfd
e0e1e2e3e4e5e6e7e8e9eaebecedeeefe
f0f1f2f3f4f5f6f7f8f9fafbfcfdfefff

1バイト目のオレンジ(ASCII)と、2バイト目の緑(漢字)が一部重なっているのがわかりますね。

EUC-JP
Unix系で使われていた・・・ような気がするけど、あんまり覚えてない。これもISO-2022準拠のエンコーディングですが、エスケープシーケンスは使わず、シングルシフトを使います。(しつこいですが、ISO-2022についてはこのページをみてください。)
簡単に言うと、1バイト目が0x8Eだったら次の1バイトを半角カタカナとして認識する、1バイト目が0x8Fだったら次の2バイトを補助漢字として認識する、というルールです。

使用領域
Shift_JISと違って、空白が目立ちます。空間を贅沢に使ってる分、「漢字の2バイト目がアルファベットと間違えられる」というような問題は起こりません。

デフォルト:
0123456789abcdef
000102030405060708090a0b0c0d0e0f0
101112131415161718191a1b1c1d1e1f1
202122232425262728292a2b2c2d2e2f2
303132333435363738393a3b3c3d3e3f3
404142434445464748494a4b4c4d4e4f4
505152535455565758595a5b5c5d5e5f5
606162636465666768696a6b6c6d6e6f6
707172737475767778797a7b7c7d7e7f7
808182838485868788898a8b8c8d8e8f8
909192939495969798999a9b9c9d9e9f9
a0a1a2a3a4a5a6a7a8a9aaabacadaeafa
b0b1b2b3b4b5b6b7b8b9babbbcbdbebfb
c0c1c2c3c4c5c6c7c8c9cacbcccdcecfc
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfd
e0e1e2e3e4e5e6e7e8e9eaebecedeeefe
f0f1f2f3f4f5f6f7f8f9fafbfcfdfefff

1バイト目が0x8Eだったときの2バイト目:
0123456789abcdef
000102030405060708090a0b0c0d0e0f0
101112131415161718191a1b1c1d1e1f1
202122232425262728292a2b2c2d2e2f2
303132333435363738393a3b3c3d3e3f3
404142434445464748494a4b4c4d4e4f4
505152535455565758595a5b5c5d5e5f5
606162636465666768696a6b6c6d6e6f6
707172737475767778797a7b7c7d7e7f7
808182838485868788898a8b8c8d8e8f8
909192939495969798999a9b9c9d9e9f9
a0a1a2a3a4a5a6a7a8a9aaabacadaeafa
b0b1b2b3b4b5b6b7b8b9babbbcbdbebfb
c0c1c2c3c4c5c6c7c8c9cacbcccdcecfc
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfd
e0e1e2e3e4e5e6e7e8e9eaebecedeeefe
f0f1f2f3f4f5f6f7f8f9fafbfcfdfefff


1バイト目が0x8Fだったときの2, 3バイト目:
0123456789abcdef
000102030405060708090a0b0c0d0e0f0
101112131415161718191a1b1c1d1e1f1
202122232425262728292a2b2c2d2e2f2
303132333435363738393a3b3c3d3e3f3
404142434445464748494a4b4c4d4e4f4
505152535455565758595a5b5c5d5e5f5
606162636465666768696a6b6c6d6e6f6
707172737475767778797a7b7c7d7e7f7
808182838485868788898a8b8c8d8e8f8
909192939495969798999a9b9c9d9e9f9
a0a1a2a3a4a5a6a7a8a9aaabacadaeafa
b0b1b2b3b4b5b6b7b8b9babbbcbdbebfb
c0c1c2c3c4c5c6c7c8c9cacbcccdcecfc
d0d1d2d3d4d5d6d7d8d9dadbdcdddedfd
e0e1e2e3e4e5e6e7e8e9eaebecedeeefe
f0f1f2f3f4f5f6f7f8f9fafbfcfdfefff


まとめ
意外にきれいな表になって嬉しい。Rubyを使ったのは半年ぶりだが、なんとなくそれっぽいコードを書けば動いてしまうのがRubyのすばらしいところだ。


余談: 元サイトの余談の引用
余談ですが、制御文字の中でDELだけ0x7Fなんていうところに飛ばされているのはなぜでしょうか? これは、データ入力媒体として紙テープを使っていたころの名残だといわれています。 紙テープでは、所定の位置に穴を空けてあるかどうかによって 2進数の1/0を区別します(SunOSのppt(6)を参照)。 1文字は7ビットですから、7つの位置の穴の有無で1文字を表します。 で、もし穴を空けるときにまちがってしまった場合、 穴をふさぐのは面倒ですので、そのまちがいを含む7ビット分全部の穴を空けて 「この字はまちがいだから削除ね」ということにしていました。 つまり、「削除」→「全ビット1」→「0x7F」というわけです。

Rubyソースコード
#!/usr/bin/ruby

ctrl = [0x00..0x1f, 0x7f..0x7f]
ascii = 0x0..0x0
#ascii = 0x20..0x7e
katakana = 0x0..0x0
#katakana = 0x21..0x5f
#katakana = 0xa1..0xdf
#katakana = 0xa1..0xdf
#kanji = []
#kanji = [0x21..0x7e]
#kanji = [0x81..0x9f, 0xe0..0xfc]
#kanji = [0x40..0x7e, 0x80..0xfc]
kanji = [0xa1..0xfe]

print <<EOS
<style type="text/css">
table.charcode {
  border-collapse: collapse;
  empty-cells: show;
}

table.charcode th,
table.charcode td {
  width: 2em;
  border: 1px solid gray;
  text-align: center;
}

td.ctrl {
  background-color: cyan;
}

td.ascii {
  background-color: orange;
}

td.katakana {
  background-color: lightgray;
}

td.kanji {
  background-color: lightgreen;
}
</style>
EOS

print %Q!<table class="charcode">\n!

print "<tr><th></th>"
for b in 0x0..0xf
  print "<th>#{sprintf("%x", b)}</th>"
end
print "</tr>"

for b0 in 0x0..0xf
  print "<tr><th>#{sprintf("%x", b0)}</th>"

  for b1 in 0x0..0xf
    b = b1 * 0x10 + b0
    
    if ctrl.any? { |r| r.include? b }
      print %Q!<td class="ctrl">#{sprintf("%02x", b)}</td>!
    elsif ascii.include? b
      print %Q!<td class="ascii">#{sprintf("%02x", b)}</td>!
    elsif katakana.include? b
      print %Q!<td class="katakana">#{sprintf("%02x", b)}</td>!
    elsif kanji.any? { |r| r.include? b }
      print %Q!<td class="kanji">#{sprintf("%02x", b)}</td>!
    else
      print %Q!<td>#{sprintf("%02x", b)}</td>!
    end
  end
  
  print "</tr>\n"
end

print "</table>\n"

0 件のコメント:

コメントを投稿