Archive for January, 2008
Unicode and Character Sets (Translation)
勉強を兼ねての勝手に翻訳シリーズ第3弾です。今回はJoel Spolsky氏のブログに掲載されていたThe Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)です。掲載されたのは2003年10月と、5年近く前のことなので、現状にそぐわないところもあるかもしれませんが、とても参考になる解説です。
ソフトウェ開発者なら絶対に最低限知っていなければならないユニコードと文字セットについて(言い訳はなしですよ!)
不可解なContent-Typeタグについてかつて疑問に思ったことはないでしょうか?おそらくHTMLファイルに書き込むものということは知ってるでしょうが、なんのためにそれなければいけないのかまでは知らないのではないでしょうか?
ブルガリアにいる友人から貰った電子メールのタイトルが”???? ?????? ??? ????”になっていたという経験はありませんか?
私は文字セットやエンコーディング、ユニコードといったたぐいの不思議な世界について全く知識を持ち合わせていないソフトウェア開発者がどのくらいいるかを知って失望させられてきました。数年前、FogBUGZのベータテスターが日本語の電子メールが届いたときに扱えるかどうかを疑問視していました。日本語?彼らは日本語で電子メールを受け取るのか?まったくわかりません。あるとき、われわれがMIMEの電子メールを解析するのに使っている商用のActiveXを詳しく調べたところ、ActiveXは確実に間違った方法で文字セットを扱っていることにきがつきました。結局、われわれは間違っている変換をもとに戻し、正しくやりなおすための大規模なコードを書かなければなりませんでした。また、あるとき、別の商用のライブラリを調べていたら、これも全く間違った文字コードの実装をおこなっていたことがわかりました。そのパッケージの開発者に連絡をとってみると、そのあたりの実装は”なにもしない”といった考えを持っていたのでした。よくいるプログラマ同様、彼は単に文字コードの部分はすべて忘れ去られるであろうと願っていたのです。
実際にはそんなわけにはいきません。Web開発ツールとして有名なPHPが文字エンコーディング関連の問題をほぼ完全に無視している、つまり、軽率にも文字を8bitで表現していることを発見したとき、PHPではちゃんとした国際化されたWebアプリケーションを作るのはほぼ不可能だと思いました。もううんざりです。
そんなわけで、私は次のような宣言をしたいと思います:もしあなたが2003年時点でプログラマとして働いていて、文字や文字セット、エンコーディング、ユニコードの基本を知らないとしたら、あなたを捕まえて、潜水艦の中で6ヶ月間タマネギの皮をむくという罰を与えます。必ず、誓って。
そしてもう一つ:
この記事では今、現役プログラマとして働いている人全員が知っているべき最新情報を述べることにします。”プレーンテキスト=アスキー=8ビット”といった概念は単にまずいという程度ではなく、救いようがないほど悪いのです。それでもなおそのようなプログラミングを行っているとしたら、あなたは黴菌の存在を信じない医者よりもわるいと言えます。頼みますから、この記事を読み終わるまでプログラムの次の行を書かないでください。
話を始める前に、もしあなたが国際化について知っている数少ない人たちのひとりだとしたら、この記事で述べられている内容全体がかなり簡易化されていると思うだろうということをお断りしておきましょう。私はここでは敷居をできるだけ低くして誰もが何がどうなっているのかを理解できるようにして、アクセント記号なども含まない英語のサブセットばかりではなく、他のどの言語でも動いてくれそうなコードを書けるようにしようとしています。さらに付け加えておきたいのは、文字の取扱いというのは国際的に動くソフトウェアを作るのに必要なわずかな一部でしかないということです。しかし、私は一度にひとつのことしか書けないので、今日は文字セットだけです。
A Historical Perspective
歴史を理解する一番簡単な方法は年代順に見ていくことです。
こういうと、EBCDICのようなほんとうに古い文字セットについて話し始めると思うかもしれません。でも、そのつもりはありません。EBCDICは現在は適当な文字セットではありません。そんなに昔の話にまでもどる必要もないでしょう。
Unixが発明されて、K&RがThe C Programing Languageを書いたやや古い時代まで遡ってみると、全てがとても簡単でした。EBCDICはこの世に登場しようとしていました。唯一気にしておかなければならなかった文字は古き良きアクセント記号すらない英語の文字だけでした。われわれがASCIIと呼んでいたコードは全ての文字を32から127までの数字を使って表現できたのです。
スペースは32で、”A”という文字は65というふうにです。このコードはうまいことに7ビットで表現できました。当時のほとんどのコンピュータは8ビットからなるバイト列を使っていましたから、使用される可能性のあるすべてのASCII文字を扱えるばかりではなく、まるまる1ビットをとっておけました。この空いている1ビットは魔が差したとしたら、何か悪いことに使えたでしょう。WordStarのほの暗いバルブが単語の最後の文字を指し示す上位ビットのところで実際に点灯し、英語のテキストしかないことをWordStarに主張するかのように。32よりも小さいコードはアンプリンタブルと呼ばれ、のろいをかけるために使われていました。冗談です。それらは制御文字として使われていました。例えば、7(訳注:BEL)ならコンピュータはビープ音をならしますし、12(訳注:FF(form feed))ならプリンタから現在、印字されている紙が送出されて新しい紙がフィードされます。
英語しか使わないと仮定すれば、全てはうまくいきます。
1バイトは8ビットあるので、大勢の人が考えました。”そうだ、128から255までのコードを自分のために利用できるじゃないか”と。まずいことに、大勢の人が同時にこのアイディアを思い付き、それぞれに128から255までの空き領域のどこに何をいれるかを考えたのでした。
IBM-PCにはOEM文字セットとして知られるようになったコードがありました。ここにはヨーロッパの言語のためのアクセント付き文字や横棒や縦棒、横棒の右端に小さな棒がぶらぶらと垂れ下がっているものというように、線を引くためのたくさんの文字があり、これを使ってスクリーン上にしゃれた箱や線を作れるようになっていました。これは未だにクリーニング屋にあるような8088コンピュータで動いているのを目にすることがあるかもしれません。実際、人々がPCを米国以外の国で買い始めるとすぐに、あらゆる種類のOEM文字が考え出されましたが、これらはすべて128以上の上位文字をそれぞれの用途のために使っていました。例えば、とあるPCでは文字コード130はéを表示したでしょうし、イスラエルで売られたコンピュータならヘブライ語の文字Gimel (ג)を表示したでしょう。もし、アメリカ人がrésumésをイスラエルに送ったとしたら、受け取られたときにはrגsumגsになっていたはずです。ロシア語などは、同一の言語でも文字コード128より大きい場所にさまざまなアイディアを作りこんでいました。このため、ロシア語の文書どうしでさえも互換性はありませんでした。
たまたま、このOEMから誰もが無償で参照できるANSI標準が集大成されました。ANSI標準ではASCIIと全く同様だった128までのコードの使用方法については異論ななかったのですが、128以上についてはどの地域かに依存した様々な文字処理方法がありました。この多様性のあるシステムはコードページと呼ばれました。例えば、イスラエルのDOSは862と呼ばれるコードページを利用していて、ギリシャは737というコードページです。コードページは128よりも小さいところは全く同じですが、たくさんの妙な文字がある128以上は違います。国内版のMS-DOSにはいくつものコードページがあり、英語をはじめアイスランド語まで扱えるようになっていました。さらに、エスペラントやガリシア語(訳注:スペイン内の自治区で使われている言葉)といったわずかながら”多言語”のためのコードページでさえもひとつのコンピュータ上に!ありました。すごい!しかし、ヘブライ語とギリシア語を一つのコンピュータで扱えるようにするには、自分でカスタマイズしたプログラムを書いて、全ての文字がビットマップの画像を使うようにしないかぎり全く不可能でした。というのも、ヘブライ語とギリシア語は別のコードページ、上位のコード番号のための別のインタープリタが必要だったためです。
一方、アジアではもっと気違いじみたことが起きてきました。とうてい8ビットには収まらない数千とあるアジアのアルファベットを扱おうとしていたのです。通常、このような問題はいくつかの文字は1バイトで表現し、他の文字は2バイトで表現するという汚い実装の”2バイト文字セット”、DBCS(double byte character set)によって解決されていました。この方法は文字列内を順方向に走査するのは簡単ですが、逆方向は実に、ほとんど不可能でした。プログラマは文字列内を前後に移動するのにs++とs–を使わず、代わりに、WindowsのAnsiNextやAnsiPrevのようなごちゃごちゃをどう扱えばいいかがわかっている関数を実行するように仕向けられていました。
それでもなお、ほとんどの人々は1バイトが1文字で、1文字は8ビットであると信じるそぶりをし、文字列を違うコンピュータに移動させない限り、一つ以上の言語を使わない限り、常にうまく動作するものとみなしてきました。しかし、もちろん、インターネットが使われるようになるとすぐに、文字列を一つのコンピュータから別のコンピュータに移動させるのはまったく当り前になり、文字コードのめちゃくちゃで急にまともに動かなくなってしまいました。ここで、幸運なことに、ユニコードが発明されたのです。
Unicode
ユニコードは地球上のきちんと体系化された書きことばすべてを、存在すると信じられているクリンゴン語(訳注:映画『スタートレック』シリーズに登場する架空のヒューマノイド型異星人、クリンゴン人が話す言葉)でさえも含むたった一つの文字セットを作ろうという大胆な作業でした。ユニコードは単に16ビットのコードで、どの文字も16ビット使うので、おそらく全部で65536文字を表現できるはずだと誤解している人も何人かいるでしょう。これは正確には正しくありません。これはユニコードにまつわるもっとも有名な神話です。もしあなたがそう思っている一人だとしても、気をわるくしないでください。
実際、ユニコードは文字について違うとらえ方をするのでユニコード的に考えないと、意味をなさないことになります。
ここまでのところは、一つの文字はディスクやメモリに格納するために使用する複数ビットにマッピングされると仮定してきました:
A -> 0100 0001
ユニコードでは一つの文字は一つのコードポイントと呼ばれるものにマッピングされますが、これはまだ理論的な概念です。メモリやディスク上でコードポイントがどのように表現されているかはまだ別の話しです。
ユニコードの世界では、Aという文字は概念的に理想的な状態にあります。あたかも天国をただよっているかのようにです:
A
概念的なA はBとは違いますし、aとも違います。しかし、Aとは同じで、 A や Aとも同じです。 フォントスタイルがTimes New RomanのAという文字であるという概念はAという文字はフォントスタイルがHelveticaであっても同じ文字だということです。しかし、小文字の”a”とはdifferent(違う)なのです。これはあまり議論の余地がないかのように見えますが、is という文字が議論を呼ぶ可能性があるいくつかの言語では問題です。ドイツ語の ßという文字は実際の文字でしょうか、それともssをちょっと変形させたのでしょうか?もし、文字の形が単語の終わりで形が変化するとしたら、それは違う文字になるのでしょうか?ヘブライ語ならyesですが、アラビア語ではnoです。とにかく、かれこれ10年以上もの間、ユニコードコンソーシアムの賢い人達が多大な政治的な討論まで行って、この問題をなんとかしようとしてきました。ですから、あなたはそういった問題を心配する必要はありません。現在では全て解決済です。
各種アルファベットの概念的な文字全てにはユニコードコンソーシアムによってU+0639のように記述するマジックナンバーが割り当てられています。このマジックナンバーはコードポイントと呼ばれています。U+は”ユニコード”であることを意味し、数字は16進数です。このU+0639はアラビア語の文字Ainを表しています。英語の文字AならU+0041になります。Windows 2000/XPのcharmapユーティリティを使うか、ユニコードのWebサイトへいけば全てのコードポイントを見つけられます。
ユニコードが定義できる文字の数字部分には実際には制限はなく、事実、65,536をすでに越えています。ですから、すべてのユニコード文字が2バイトに押し込められるというわけではありませんが、それはまぁ神話のたぐいでした。
では、次のような文字列があったとします:
Hello
これはユニコードではこのような5つのコードポイントに対応しています:
U+0048 U+0065 U+006C U+006C U+006F
単なるコードポイントの束です。本当に数字だけです。どのようにしてメモリに格納したり、電子メールで表現するかについてはまだ話していませんが。
Encodings
そこでエンコーディングの話しが必要になってきます。
早期の段階ではユニコード・エンコーディングのアイディアは2バイト神話をもたらしていたので、「おい、その数値をそれぞれ2バイトのなかに入れればいいじゃないか」、となっていました。つまりHelloとい文字列なら
00 48 00 65 00 6C 00 6C 00 6F
になります。これでいいでしょうか?そんなに急がないでください!こうもできるのではないでしょうか:
48 00 65 00 6C 00 6C 00 6F 00 ?
技術的にはyesです。私は本当にこれはありうると信じています。実際、初期の段階で実装者はビッグ・エンディアン、リトル・エンディアンのどちらのモードでも各CPUにとって最速となる方法でユニコードのコードポイントを格納できるようにしたいと考えていたので、とにもかくにも、ユニコード格納方法は2種類あったのです。このため、全てのユニコード文字列の最初にはFE EEを格納するという奇妙なしきたりを強要されることになりました;これはユニコード・バイトオーダー・マークと呼ばれています。もし、上位バイトと下位バイトを入れ換えるなら、FF FEのように記述して、文字列を読む人がバイトが入れ換えられていることがわかるようにします。ふう。当初は世間のユニコード文字列すべてにバイトオーダー・マークがあったわけではありません。
はじめのうちはこの方法で十分のように見えましたが、プログラマは不平を言っていました。「このたくさん並んでいる0を見てみろ!」と彼らは言いました。というのは、彼らはアメリカ人で滅多にU+00FF以上のコードポイントを使用しない英語のテキストだけを見ていたからです。また、彼らは節約をよしとするカリフォルニアの自由主義のヒッピーでもあったのです(笑)。もし彼らがテキサスの人間だったら、2倍ものバイト数を浪費しても気にしなかったでしょう。しかし、カリフォルニアの節約家たちは文字列が使うストレージの総量が2倍になってしまうアイディアには耐えられないのでした。そして、とにかく、世の中にはすでに様々なANSIやDBCS文字セットを使っている文書が溢れてたわけですが、これら全てをいったい誰が変換するのでしょうか?ワタクシ?この理由だけで、人々は数年間ユニコードを無視することにしてしまいました。そうこうしているうちに、自体は悪化しました。
ここで、すばらしいUTF-8の概念が発明されました。UTF-8は文字列をユニコード・コードポイントに格納する新たなシステムで、U+で始まるマジックナンバーをメモリ上では8ビットごとの複数バイトで表現する手法です。UTF-8では0から127までの各コードポイントは1バイトに格納されます。128以上のコードポイントだけ2,3…と実際には6バイトまで使って格納します。
この方法には英語のテキストはASCIIとUTF-8で全く同じに見えるという素晴らしい副次的な効果があり、アメリカ人は何が悪いのかさえ気がつかないでしょう。世界のアメリカ以外の地域で試練を味わうことになるだけです。つまり、 U+0048 U+0065 U+006C U+006C U+006Fで表現されるHelloは48 65 6C 6C 6Fで格納されることになります。これは、見てください!ASCIIともANSIとも、地球上の全てのOEM文字セットとも同じです。そして、もし、あなたにアクセント記号がある文字やギリシア文字、クリンゴン文字を使う勇気があるなら、一つのコードポイントを格納するのに複数バイトを使わなければならないだけです。ただし、アメリカ人は何も気がつかないでしょう。(UTF-8はnullターミネータとしてすべてのビットに0が格納されている1バイトを使うような無知な古い文字列処理コードが必要な文字を削除しないというすばらしい特徴も兼ね添えています)。
こまでのところで、私はユニコードをエンコードする3種類の方法を話しました。伝統的な2バイトに格納する方法はUCS-2(2バイトあるので)かUTF-16(16ビット使うので)と呼ばれています。UCS-2の場合はビッグ・エンディアンかリトル・エンディアンかは区別しないといけません。そして、よく使われている、新しいUTF-8標準があります。UTF-8はASCII以外の文字を全く意に介さない腐ったプログラムと英語のテキストにうれしい偶然をもたらして、立派に動かしくれるという素敵な特徴もあります。
ユニコードをエンコードする手法は他にもたくさんあります。例えば、UTF-7と呼ばれている手法です。これはUTF-8によく似ていますが、UTF-8と違い、上位ビットが常に0であることを保証しています。このため、厳格な警察の電子メールシステムのたぐいが7ビットでまったく十分、それ以上は要らないと考えているところでユニコードを通さなければならない場合でも大丈夫です。UCS-4というエンコーディングもあります。これは各コードポイントを4バイトに格納する方法で、それぞれのコードポイントが同じバイト数で格納されるという利点があります。しかし、まあ、テキサスの人間だったとしてもそんなにたくさんのメモリを浪費するのは勇気がいるでしょう。
ということで、実際にあなたはユニコード・コードポイントで表現できる概念的に理想的な文字という観点から何かを考えているかもしれませんが、ユニコード・コードポイントというのは古くさい体系にもエンコードするのは可能です。例えば、 Helloのユニコード文字列 (U+0048 U+0065 U+006C U+006C U+006F)をASCIIに、あるいは昔のOEMのギリシア文字エンコーディングやヘブライ語のANSIエンコーディング、さらに、これまでに発明されてきた数百のエンコーディングにちょっとした作業でエンコードできます:おそらくいくつかの文字が表示されないでしょうけれど。もし、あるエンコーディングを使って表示させようとしているユニコード・コードポイントが同じではなかった場合には、通常、小さな疑問符:?が表示されるか、うまくいけば箱が表示されます。これは何が表示されましたか? -> �
一部のコードポイントだけ正しく扱えて他のコードポイントをすべて疑問符に置き換えてしまう伝統的なエンコーディングは数百もあります。英語のテキストでよく使われるエンコーディングとしてはWindows-1252(西ヨーロッパ言語用のウィンドウズ9x標準)やISO-8859-1、別名Latin-1(これもまた西ヨーロッパの言語には便利)があります。しかし、ロシア語やヘブライ語の文字をそのようなエンコーディングで格納しようとすると疑問符の山を見ることになるでしょう。UTF 7や8, 16, 32には全てどのようなコードポイントでも正しく格納できるよい特徴があります。
The Single Most Important Fact About Encodings
もし、私が今説明した内容をすっかり忘れてしまったとしても、非常に重要な事実をひとつだけ忘れないでいてください。文字列に使われているエンコーディングが何なのかを知らないというのはまったく意味をなさないということです。あなたはもはや問題が無いふりをして全てうまくいくと思ってはいけません。”プレーン”テキストがASCIIだというふりもしてはいけません。
プレーンテキストのようなものは何もないのです。
もし、メモリ上にあるいはファイル内に、電子メールに文字列があったら、その文字列のエンコーディングが何なのかを知っていなければなりません。そうでなければ正しく解釈することも、ユーザに正しく表示して見せることもできません。
“私のwebサイトはちんぷんかんぷんだ”とか”私がアクセント記号を使うと彼女は読めないんだ”といったばかげた問題を口にするのは、とある文字列がUTF-8かASCIIかあるいはISO 8859-1 (Latin-1)かWindows 1252(西ヨーロッパ)のどれでエンコードされているかを言ってくれないと、単に正しく表示できないどころかどこで終わっているかすら見つけられないのだという事実さえ理解していなかったような世間知らずのプログラマにまで落ちぶれることになります。世の中には百種類くらいのエンコーディングがあってコードポイント127よりも上があり、これで全てが変わってしまうのです。
文字列が使っているエンコーディングが何であるかの情報をどのようにしてとっておけるでしょうか?実は、そのための標準的な方法がいくつかあるのです。電子メールではフォームのヘッダにこのような文字列があると仮定できます。
Content-Type: text/plain; charset=”UTF-8″
Webページなら、webページ自身に付随してwebサーバが同様のConetn-Type HTTPヘッダを返してくるというのが、もともとのアイディアでした。これはHTMLにではなく、HTMLページの前に送り返されてくるレスポンスヘッダの一つとしてです。
ところが、この方式では問題が発生します。大規模なwebサーバに多くのドメインが共存していて、大勢の人が様々な言語で貢献してくれるページが数百もあると仮定してみましょう。使われる言語はマイクロソフトのFrontPageがちょうどよいと判断したエンコーディングすべてだ想定します。すると、Webサーバ自身はそれぞれのファイルがどのようなエンコーディングで書かれているかはまったくわかりません。ですから、Content-Typeヘッダを送り返すこともできません。(訳注:webサーバが動いているプラットフォームのディフォルトエンコーディングを適用できない)
もし、HTMLファイルに、まさにHTMLファイル自身に、何か特別なタグを使ってContent-Typeを付けられたとしたら、とても便利なはずです。もちろんこの方法は純粋主義者の反感をかうでしょう。。。どのエンコーディングが書き込まれているのかを知るまえにどうやってHTMLファイルを読むことができるのか?!、と。幸運なことに、一般的に使われるエンコーディングはほぼ全てが、32から127までの文字と同様のことをしています(訳注:HTMLファイルの最初の部分、metaタグが現れるところまでは32から127までの文字しか使われていない)。つまり、妙な文字を使って始めなくても、いつでもHTMLページのエンコーディング情報を入手できるのです:
<html>
<head>
<meta http-equiv=“Content-Type” content=“text/html; charset=utf-8″>
しかし、そのmetaタグは<head>タグの中で絶対に最初に書いておかなければなりません。というのは、webブラウザはこのタグを見つけ次第、webページの構文解析を中止し、metaタグで指定されているエンコーディングを使ってページ全体を再び読み直してから、構文解析を最初からやりなおすためです。
HTTPヘッダにもmetaタグにもContent-Typeが指定されていなかったら、Webブラウザは何をするでしょうか?Internet Explorerは実におもしろい動きをします:IEは様々な言語の一般的なエンコーディングで書かれた文書によく現れるテキストのバイト列の頻度に基づいて、どの言語あるいはエンコーディングが使われているかを類推します。様々な古い8ビットのコードページは各々の国の文字を128から255までの違う場所に割り当てる傾向があり、また、どの自然言語も文字使用頻度の度数分布に特徴的な違いがあるので、この類推手法がうまく動いてくれる可能性はあります。これはまったく理解に苦しむやり方ですが、webブラウザを使ってwebページを見るにはContent-Typeヘッダが必要だなどとは夢にも思わないような無垢なwebページ作成者には存分にうまく動作する手法で、一見、これで全ていいかのように見えます。しかしそれは、母国語の文字頻度分布にまったく合致しない何かを書いて、IEがそれを韓国語であると判断してそのように表示してしまう日がくるまでのことです。つまり、私が思うには、Postelの法則にある”送り手としては控えめに振る舞い、受け手としてはおおらかに振る舞う”(訳注:Internetの父、Jon PostelがTransmission Control Protocol, RFC 793を定義したときに述べた言葉。参照)という概念ははっきり言って、エンジリアニングの基本としてはいいものではありません。とにかく、ブルガリア語で書かれているにもかかわらず韓国語で(しかも、全く意味をなさない韓国語で)表示されてしまったwebサイトの気の毒な読者は何をするでしょうか?おそらく、View|Encodingメニューを使って、何が書かれているかわかるようになるまで、いろいろなエンコーディング(少なくても東ヨーロッパの言語のエンコーディングが12はあります)をできるだけたくさん試すことでしょう。もしも、そのようなことができると知っていればであって、ほとんどの人は何もしないはずです。
私の会社が開発しているwebサイト管理ソフトCityDeskの最新版では内部的にはUCS-2(2バイト) ユニコードを全てに適用することにしました。このエンコーディングはVisual Basic, COM, Windows NT/2000/XPがネイティブの文字型として使っているものです。C++のコードではcharの代わりにwchar_t (“wide char”)を文字列の型として宣言して、str関数の代わりにwcs関数を使うようにしています(例えば、strcatやstrlenの代わりにwcscatとwcslenを使います)。CのコードでUCS-2の文字列を生成するにはL”Hello”のように、文字列の直前にliteralのLを付けます。
CityDeskがwebページを生成するときには、文字エンコーディングはUTF-8に変換されます。UTF-8は過去何年にもわたりwebブラウザがきちんとサポートしてきたエンコーディングです。このため、Joel on Softwareは29言語の翻訳版がそれぞれの言語にエンコードされていますが、見えないというトラブルは一度も聞いたことがありません。
この記事はかなり長くなってしまいましたが、それでも、文字エンコーディングとユニコードについて知っておくべきこと全てを網羅できてはいないでしょう。しかし、あなたはこれを読んだので、十分な知識が身についてプログラミングに戻れるはずです。お医者さんと呪文に代わって抗生物質を使ってです。今、あなたに託した課題ですよ。
You are currently browsing the Servlet Garden blog archives for January, 2008.
