ソフトウェア開発者なら絶対知っておくべき!Unicodeと文字セットについての最低限の知識

 HTTP Unicode
2014.12.03

Joel on Softwareの The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) を読み通す機会があったので、メモ的に訳しておきます。

 
それにしてもWikipediaとかより圧倒的にわかりやすいですね。

もう11年も前の記事なんですが・・

 


 

これまでに、神秘的なタグ、Content-Typeに思いを巡らせたことはあるだろうか。
そう、HTMLに使えることは知っていても、実際何なのかまでは知らない、アレだ。
 
ブルガリアにいる友人から、件名が「”???? ??? ??”?」というメールを受け取った経験はない?
 
いかに多くの開発者が、文字セット・エンコーディング・Unicodeといった神秘的な世界のスピードについていけていないかという現実を知るたびに、私はがっかりしてきた。
 
例えば2年前、FogBUGZのβテスターは日本語のメールを受け取れるかどうかを気にしていた。 え、日本語?彼らが日本語のメールを受け取ることがあるのかって?いや、それは知らない。
例えば私が、MIMEメールのメッセージを読み込むのに使っていた商用ActiveXコントロールについて調べているとき、それが文字セットの扱いを間違っていることに気づいてしまったことがある。 その結果、すでに間違えて処理されていた会話をすべて取り消し、正しく処理し直す勇ましいコードを書く羽目になった。
他にもある。また別の商用ライブラリを調べていたときのこと、やはりそれも完全に間違った文字コードの実装をしていた。 そのパッケージの開発者に連絡をとったら、彼はこういった。
「そこはどうしようもなかったんだ」
多くのプログラマがそうするように、彼は、どうにか動いてくれたらなぁと祈っただけだった。
 
しかし、うまくいくわけはない。
有名なWeb開発ツールであるPHPが、文字エンコーディング問題をほぼ完全に無視していることに気づいたとき 私はすでに不用意に文字に8ビットを選択しており、もはや国際対応した優れたWebアプリケーションは作れない状態だった。 ああ、もう!
 
そこで、言いたいことがある。
もし君が2003年時点でプログラマをやっていて、文字・文字セット・エンコーディング・Unicodeの基礎を知らないなら、 君を捕まえて、6ヶ月間、潜水艦の中でタマネギの皮むきをしてもらう罰を与えようと思う。本気だよ?ほんと。
 
あ、それともう一つ。

基礎、そんなに大変じゃないからね?

この記事では、すべてのプログラマが最低限知っておくべきことを伝えようと思う。
プレインテキスト = Ascii = 文字は8ビットみたいなことだけが間違いという話ではない。 まあそれはそれで絶望的な間違いだし、そういうプログラムを組み続けるなら、君は病原菌を信じない医者と同程度ってことなんだけど。
 
さて始める前に断っておきたい。もし君が国際化対応のなんたるかを知る稀有な一人なら、私の議論がちょっと単純化しすぎなんじゃないかと思うかもしれない。 しかし私は、誰もがこの話を理解し、どんな言語でも動くかもしれないと思えるコードを書けるよう、ハードルを下げようとしているんだ。 それともうひとつ言っておきたいのは、文字を正しく扱うことは、国際的なソフトウェアを作るために必要なことの、ほんの一部だということ。 とはいえ私は同時にひとつのトピックしか書けないし、国際化については、今日は文字セットのことだけにしておこうと思う。
 

歴史的な観点

この手のことを理解する最も簡単な方法は、歴史を振り返ることだ。
 
EBCDICのような、いにしえの文字セットを話すつもりなのか?と疑ったみなさん。話さないって。 EBCDICは君の人生にまったく関わらないのだし、そこまで遡る必要はない。

 
ちょっとだけ昔にもどってみよう。
Unixが開発されはじめ、K&RがC言語を書いているころ、すべてはとてもシンプルだった。 EBCDICはなくなりかけていた。気にすべき文字は古き良きアクセントのない英字だけであり、32から127の数字で何らかの文字を表すようマッピングされたASCIIと呼ばれるルールを用いれば それぞれの文字にコードをもたせることができた。半角スペースは32であり、Aという字は65だ。

便利なことに、これは7ビットに収まる。 当時のコンピュータの多くはその処理に8ビットを利用しており、ということは、すべてのASCII文字を扱えるだけなく、 よこしまな気持ちがあればそのような目的に未使用ビットを使うことができたということだ。 例えば事実、WordStar社のうすのろ君は、単語の最後の文字を特定するために1ビットを利用してしまい、WordStarを英語しか使えないものにしてしまった。
 
32未満の文字は印字不能文字と呼ばれ、呪いのために使われている。ごめん冗談だって。 それらは制御文字として利用されている。例えば7はビープ音を鳴らせという命令を意味するし、12はプリンターに改ページを伝えるためのものだ。
 
すべてうまくできていたわけだ。まあ君が英語を話す前提の上に、だけど。
 
バイトの上限が8ビットであるがために、多くの人はこう考えた。
「ああ、128から255までのコード(1バイト中のASCIIでは利用しない範囲)は自由に使えるわけだね」
問題だったのは、ほんとうに多くの人が同時にこう考えたことだった。そして誰もが、その128から255までのスペースをどう使うかそれぞれ独自の考えをもってしまったことだった。
例えばIBMのPCは、後にOEM文字として知られるようになるものをもっていた。ヨーロッパ言語におけるアクセント文字が使えるだけでなく、 横線、縦線、右側に何かがぶら下がっている横線などたくさんの描画文字を含んでおり、 これを使えばスクリーン上にスマートな箱や線を書くことができた。 これは今でも、クリーニング店においてある8088コンピュータで動いているのを見ることができる。

実際、アメリカ以外でPCが買われ始めてすぐに、さまざまな目的のためにこの上位128文字分が使われる、あらゆるOEM文字セットが考え出されたのだ。 例えばあるPCは130をéを表示するために使っているのに、イスラエルで売られた別のPCではヘブライ語のGimel(ג)のことだったりする。 だからアメリカ製品からレジュメ(résumés)をイスラエルに送ると、彼らはrגsumגsを受け取ることになる。 多くの場合、ロシアのように、上位128文字分をどう使うかということについてはとてもたくさんのアイディアがあったために、 例えばロシアのドキュメントを期待通りに置き換えるということはとても困難なことだった。
 
このどんなことにも使える自由なOEM、最終的にはANSI標準というものに体系化される。 ANSI標準では、128未満のコードについてはその用途が合意されている(まあそれはASCIIそのものなのだが)一方、 128以上のコードについては居住地域に応じさまざまな目的に使ってよいとされた。
このそれぞれ異なったシステムのことを、コードページといった。 例えばイスラエルのDOSでは862と呼ばれるコードページを使い、ギリシャでは737を使う。 いずれも128未満の文字は同じなのだが、128以上にはおもしろい文字が入っていて、異なっている。 MS-DOSの国内版は英語からアイスランド語まですべてを扱えるよう数十種類のコードページを含んでいたのだが、 1台のコンピュータでエスペラントとガリシア語を同時に使えるといった、マルチリンガルなコードページを持つものまであったんだ!わお! でも、ちなみに、ヘブライ語とギリシャ語を同じコンピュータに載せるのは不可能なのだ。 これらの言語は上位のコード群の解釈が異なっているから異なるコードページが必要であり、 君がビットマップを駆使してすべてが表示できるようなプログラムを組まない限り動作しない。
 
さて一方、アジアではどうだったか。
アジアの言語には8ビットには到底収まらない数千の文字があり、それを考慮しなければいけないというもっと断然クレイジーな状況だった。 大概は、ある文字だけは1バイトなのにその他ほとんどは2バイトに収める、DBCSという複雑なシステムによって解決されていた。 このシステムは、文字列を読み進めるのは簡単なのだが、後ろ向きに進む処理はなんとも不可能に思えるものだった。 プログラマは文字列の操作でs++s--は使わず、代わりにこのカオスな文字を扱えるWindowsのAnsiNextAnsiPrevを使うようにと指導された。
 
しかしそれでもまだ、多くの人は文字列をコンピュータ間で移動させたり多言語を扱わない限り、1バイトは1文字、1文字は8ビット、ある種それで動くんだと言い張った。 だがもちろん、インターネットが誕生してすぐにコンピュータ間で文字列を送り合うのはありふれたことになったし、その結果このカオスは崩壊していった。 が、幸い、Unicodeが考案されていたのだ。
 

Unicode

Unicodeはたったひとつの文字セットを作ろうという勇敢な試みだった。 目指すのは(空想の世界であるクリンゴンのようなものさえも含んだ)地球上のすべての適切な書記体系を取り入れた文字セットの構築だ。 Unicodeは単に65,536文字を扱える16ビットのコード体系だと勘違いしている人もいる。実は、違う。 それはUnicodeに関してもっともよくある誤った解釈だから、仮に今までそう思っていたのだとしても、気にすることはない。
 
実は、Unicodeは文字について、これまでとは異なった考え方に基づいて作られている。 だからまずUnicodeの考え方を理解しないと、この後の話は何も意味がないだろうと思う。
 
今まで文字は、ディスクやメモリに書き込めるよう、とあるビットにマッピングされると仮定してきた。このように:
 
A -> 0100 0001
 
Unicodeでは、コードポイントと呼ばれるものにマッピングされる。 これは実際のビットなどを表すものではなく、依然として、理論的な概念でしかない何か、である。 このコードポイントがどうやってディスクやメモリに書き込まれるかというのは、まったく別の話になる。
 
Unicodeでは、Aという文字は、観念的なイデアだ。天国に浮かんでいる。

A
このイデアであるAは、Bではない。aでもない。 しかしAAとは同じものだ。 Times New Romanで書かれたAと、Helveticaで書かれたAが同じものであり、小文字のaとは違うという概念は、議論の余地はないように思える。

しかしいくつかの言語においては、その文字が何なのかを判断することそのものが議論になりうる。 ドイツの文字であるß、これはほんとうに文字として使われているのか、ssと書く代わりに使っているのか? 単語の最後の文字の形が変わる場合、それは異なる文字なのか?ヘブライ語ならYesだが、アラビア語ならNoだ。 まあいずれにせよ、Unicodeコンソーシアムの頭のいい連中は最後の10年かそこらでこの問題に気づき、 高度に政治的な議論を重ねた結果、君はこの種のことを気にしなくてよくなったのだ。 彼らが既にすべてを調べ、考え尽くしてくれたおかげだ。
 
文字にまつわるこのすべての観念上の文字は、Unicodeコンソーシアムによって定義されたマジックナンバーに割り当てられる。 こんな書き方だ: U+0639 。このマジックナンバーこそが、コードポイントと呼ばれるものである。 U+Unicodeを意味し、後続の数字は16進数で表記される。 U+0639は、アラビア文字のAinだ。英語のAなら、U+0041。 Windows 2000/XPのキャラマップユーティリティを使うか、UnicodeのWebサイトを見れば確認できる。
 
Unicodeの定義できる文字の数に上限はない。 2バイトに詰め込めないユニコード文字もあるが、実際すでに65,536以上の文字を扱っている。 上限があるといった話は根拠のない俗説だ。
 
OK、こんな文字列がある。

Hello

これはUnicodeでは、以下のように5つのコードポイントとして表現することができる。

U+0048 U+0065 U+006C U+006C U+006F

コードポイントの羅列だ。ただの数字だ。 これまで、これをどうやってメモリに保存するのか、どうやってEメールのメッセージとして表示するのかといった話はしてこなかった。
 

エンコーディング

そう、それがエンコーディングが生まれた理由だ。
 
初期のUnicodeエンコーディングのアイディアは(2バイトにまつわる俗説を引き起こしたものでもあるが)、 なあ、2バイトずつ書き込んじゃおうぜ、というものだった。つまりHelloなら、

00 48 00 65 00 6C 00 6C 00 6F

これでいいの?これじゃそんなに速くないんだけど!こうは書けないの?

48 00 65 00 6C 00 6C 00 6F 00 ?

うーん、厳密に言えば、書ける。書けると思う。それに実際、初期の実装者はUnicodeコードポイントを ビッグエンディアンでもリトルエンディアンでもそのCPUで最速に処理できる形式で格納できるようにしたかったのだ。 そして2通りのUnicodeの格納方法ができてしまったというわけだ。 結果として、人々はすべてのUnicode文字列はFE FFで始めなければいけないという奇妙な慣習を強要された。 これはUnicodeバイトオーダーマークと呼ばれるもので、FF FEと逆にして始まっていた場合は 後続のすべてのバイトを逆に読まなければいけないことが分かる。ふぅっ。 とはいえ野良Unicode文字列には、バイトオーダーマークで始まらないものもある。
 
しばらくは、それで十分回るようにも思えた。しかし不満をもらすプログラマがいたのだ。
「見ろよこのゼロの羅列をさ!」
彼らはアメリカ人であり、U+00FFより上のコードポイントは滅多に使わない英語を使う人々なのだ。 まあ彼ら、カリフォルニアの保守的なリベラルヒッピーでもあるから(冷笑)。 テキサス生まれだったなら、2バイトの数字をがつがつ処理することなんて気にしなかっただろうにね。 このカリフォルニアの弱虫くんたちは文字列の格納に2倍の容量を使うことにさえ我慢できなかったのに、 その上、すでにANSIやDBCSといった文字セットを使って作られた忌々しいドキュメントがあったわけで、 じゃあ誰がこれをコンバートするかってことになるわけで。え、ワタシ? まあそんなわけで多くの人は数年間Unicodeを無視しようと決め、そうしている間に事態は悪化したのであった。
 
だからこそ発明されたのが、 素晴らしい概念であるUTF-8である。 UTF-8はUnicodeコードポイント、つまりU+マジックナンバーでできた文字列を、8ビットでメモリに保存するもうひとつの仕組みだった。 UTF-8では、0から127までのすべてのコードポイントは1バイトに収められる。 そして128を超えるコーポポイントだけが、2か3、実際は6バイトまでの複数バイトに収められる構造になっているのだ。

これには、巧みな副産物があった。なんと英語の場合、UTF-8形式のデータが、ASCII形式のそれと、まったく一致しているのだ。 こうなるとアメリカ人にとって、もうなんの問題も見当たらない。他の国々の人々だけが超えればいいハードルになったのだ。 具体的に確認しよう。Helloは、コードポイントでいえばU+0048 U+0065 U+006C U+006C U+006Fだが、 保存される際は48 65 6C 6C 6Fである。見てくれ!ASCII、ANSI、地球上のあらゆるOEM文字セットで保存した場合と同じだ。 さて、もし君が果敢にもアクセント文字やギリシャ文字、果てはクリンゴン文字を扱ったとして、 そのコードポイントの保存には複数バイトを利用することにはなるものの、アメリカ人は気づかない。 (UTF-8には、0終端文字と言われる古き文字列処理コードを無視するという素敵な特徴もある)
 
これまでのところ、Unicodeをエンコードする方法は3つ紹介してきた。 伝統的な、2バイトに保存手法は、UCS-2(2バイトだから)またはUTF-16(16ビットだから)と呼ばれており、 UCS-2についてはビッグエンディアンなのかリトルエンディアンなのかを特定する必要がある。 そしてASCII以外の存在にまったく気づかない無能なプログラムでも英語との組み合わせならうまく動作するという優れた特徴をもつ UTF-8という新しいスタンダードがある。
 
実は他にもUnicodeをエンコードする方法はいくつかある。
UTF-8ととてもよく似ているが上位ビットが必ず0になるUTF-7と呼ばれるエンコーディングは、 7ビットもあれば十分というとても厳しい警察国家のようなEメールシステムにUnicodeを送信しなければいけないなら 無傷で文字列を送信できるエンコーディングだと感謝したくなるかもしれない。 すべてのコードポイントを4ビットで保存するUCS-4というものもある。 これを使えばすべてのコードポイントを同じ長さのバイト列として保存できるメリットはあるものの、 まさかのテキサス生まれの男でさえ、それほど多くのメモリを浪費することに厚かましくはなれないだろう。
 
そして、いまや、君はもうUnicodeのコードポイントで表現される観念的なイデアで文字を考えることができるのだから、 そのコードポイントは昔学校で習ったようなエンコーディング手法でもエンコードできるはずだ! 例えばHelloというUnicode文字列(U+0048 U+0065 U+006C U+006C U+006F)をASCIIにエンコードできるだろうし、 古いギリシャのOEMエンコーディングやヘブライ語ANSIエンコーディング、 これまで発明されてきた数百のエンコーディングを利用することができるはずだ。 まあ、いくつかの文字は表示できないだろうけどね! もしそのエンコーディングにUnicodeコードポイントへの対応がないものがあるなら、 小さな疑問符?をつけるか、君が優しい人ならば、箱マークをつけてあげるかすればいい。どっちがいい? -> �
 
一部のコードポイントしか正しく表示できず、それ以外が疑問符になる伝統的なエンコーディングは山のようにある。 英文エンコーディングで有名なものを挙げれば、Windows-1252、ISO-8859-1、aka Latin-1などだ。 これらのエンコーディングでロシア語やヘブライ語を保存しようとすると、大量の?になる。 一方でUTF 7、8、16、そして32であれば、いずれもどんなコードポイントでも保存できるのだ。
 

エンコーディングについて重要な、たったひとつの事実

これまで私が説明してきたことをすべてすっかり忘れてしまっても、これだけは憶えておいてほしい。とても重要なことだ。 それは、使われているエンコーディングを知らないと、文字列として意味を成さないということだ。 もはや君は見て見ぬふりをして、プレインテキストがASCIIだなんて言ってはいられない。

プレインテキストなんてものは存在しない

メモリやファイル、Eメールの本文に文字列があるとき、まず君はエンコーディングに何を使っているのかを知らなければいけない。 知らないままにはデータを文字列として解釈することはできないし、ユーザへ正しく表示してあげることもできないのだ。
 
「あれ、僕のWebサイト、ちんぷんかんぷんだ」とか「アクセントを使うと、彼女メールが読めないって言うんだ」という ほとんどすべての間の抜けた問題は、ごく簡単な事実を理解していない浅はかな開発者に行き着く。 事実とはつまり、 UTF-8なのかASCIIなのか、それともISO 8859-1 (Latin 1)なのか、Windows 1252 (Western European)なのか、 とにかくその文字が何でエンコードされているかを教えてあげないと正しく表示できないと言うこと、 さらにいえば、どこで文章が終わりなのかさえ分からないということである。 エンコーディングは無数にあり、コードポイントが127より大きい場合、もはやどうなるのかは予想がつかない。
 
ではどうやって、どのエンコーディングを使っているかという情報を残せばいいのだろうか。 実はこれには、標準的なやり方がある。Eメールなら、メールヘッダにこう書けばいいのだ。

Content-Type: text/plain; charset="UTF-8"

Webページなら、当初のアイディアでは本文に添えて Content-Type HTTPヘッダに似たものをWebサーバから返せばいいのではというものだった。 HTMLそのものに書くのではなく、レスポンスヘッダのひとつとしてHTMLページの前に送信するという方法だ。
 
しかしこれにはちょっと問題がある。
仮に君がたくさんのWebサイトをホストするサーバをもっていて、そこには多くの人がたくさんのページを投稿してくるものとしよう。 しかもそれらは様々に異なる言語で書かれ、それぞれがMicrosoft FrontPageが適切と考えるエンコーディングを使っている。 となると、もはやWebサーバ自身は各ファイルがそれぞれどのエンコーディングで書かれているのか分からないため、 Content-Typeヘッダをユーザに返すことはできない。
 
HTMLファイルそのものに、何らかの特別なタグを使ってそのContent-Typeを記述できれば便利になるだろう。 「内部のエンコーディングを知らずに、どうやってそのHTMLを読めるわけ?」と言って、きっと純粋主義者は発狂するけどね・・。 でも幸いなことに、32から127までの文字についてはほとんどすべてのエンコーディングが同じ仕様なのだ。 だから、先頭に変なバイト列をもたなくても、HTMLのこのあたりまでなら常に読み込めるというわけだ。

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

しかしこのMetaタグ、<head>セクションのほんとうに最初のほうに書かなければいけない。 というのも、Webブラウザはこのタグを見つけるとすぐにHTMLの解析を中断し、 判明したエンコーディングに基づいてHTML全体を再解釈し始めるからだ。
 
もしHTTPヘッダにもMetaタグにもContent-Typeが見つからない場合、Webブラウザは何をするだろう。 Internet Explorerは実はとてもおもしろい動きをする。 各言語の、あるエンコーディングにおける特定の文章で現れるいくつかのバイト列の頻度をもとに、 そのHTMLが何語で書かれ、どのエンコーディングが使われているのかを推測するのだ。 この推測の根拠となるのが、 古い8ビットコードページではコード128から255の範囲に言語特有の文字をあてこんでいること、 そして各言語には歴史があり、文字の利用頻度にはばらつきがあるという事実だ。 これはたしかに、推測がうまくいくチャンスがありそうだ。 ところでとても変な話なのだけど、Content-Typeヘッダが必要だなんてまったく知らないWebページ制作者が ブラウザでページを確認してよさそうに思えたはずなのに、次の日、彼は母国語の文字出現頻度に一致しない何かを書き、 IEがそれを韓国語だと判断してそのように表示してしまうといったことがある。 思うにこれは、率直に言ってしまえば、「送信するものに関しては厳密に、受信するものに関しては寛容に」という ポステルの法則が優れたエンジニアリングの原理原則にはなっていないということの証明だ。 まあとにかく、そのブルガリア語で書いたのに韓国語に見えるそのサイトだが、 数少ない読者はどう対処すればいいのだろうか。メニューを開き、表示 > エンコーディングから、 いろんなエンコーディングを試し(東ヨーロッパ言語だけでも十数種類あるが)、画像がきれいに表示されるまでそれを繰り返すしかない。 まあもちろん、そのやり方を知っていればまだよくて、多くの人は知りもしないのだけど。
 
随分長い記事になったわりには文字エンコーディングとUnicodeについて知っておくべきことのすべてには言及できなかったが、 ここまで読んでいただけたのであれば、プログラミング業務にもどるには十分な知識が身に付いているはずだと信じたい。 ヒルと魔法ではなく、抗生物質と私がいま君に渡した知識を使ってね。