Ruby M17N の設計と実装

はじめに

2007 年 12 月 25 日 (UTC) についに Ruby 1.9.0-0 がリリースされ、さらに 1 年余の開発を経て、2009 年 1 月 31 日 (JST) に Ruby 1.9.1 がリリースされました。

Ruby 1.9 では 1.8 と非互換な物を含む、多くの拡張・変更がなされました。評価器の YARV への移行、正規表現エンジン鬼車の採用、Enumerator の組み込みなどなどなど。それらに並んで大きな変更が Ruby M17N の導入です。

Ruby 1.9 における多言語化仕様 Ruby M17N では、多くの言語・システムで採用されている Unicode 正規化方式でなく、CSI 方式を採用したり、独自のエンコーディング変換エンジンである transcode を搭載したりと、野心的な目標を掲げ、なんとか実現することができました。この記事では、多言語とは何かから、Ruby M17N の内容、実際に対応させる際の指針までを見ていきます。

多言語化概論

M17N とは

そもそも M17N とは Multilingualization の略、つまり多言語化を意味します。 元来、コンピュータはビットやそれを束ねたバイト、そしてその列しか扱うことが出来ません。US-ASCII やその他 1 byte 系のエンコーディングを一つだけ使っている場合にはそれでもなんとかやっていけますが、複数のエンコーディングを扱おうとした場合や、1 byte に収まらないエンコーディングを用いる場合には工夫が必要です。

L10N

L10N とは Localization の略で、地域化を意味しています。(cf. nls / national language support) 具体的には、それぞれの地域・言語に適したように変更することとなります。日本で用いられるソフトウェアのうち、一定の割合は欧米で作られたソフトウェアが占めています。これらを日本で用いる際には、様々なメッセージの日本語化が当然必要になります。また、欧米の言語はシングルバイトエンコーディングで表されていますが、日本語は Shift_JIS にしても EUC-JP にしてもマルチバイトエンコーディングで表されています。そのため、欧米のソフトウェアで日本語を扱おうとすると、そのままでは文字送りや文字境界の判定に際して不具合が発生します。

日本における多言語化は、まず欧米のシングルバイトエンコーディングを前提としたソフトウェアをマルチバイトエンコーディングに対応させることから始まりました。

I18N

I18N とは Internationalization の略で、国際化を意味しています。I18N とは、

  • ソフトウェアのマルチバイト対応
  • 各種メッセージや通貨記号等を地域ごとに容易に切り替えられる仕組みの整備

を行うことです。マルチバイト対応は当初は ISO 2022 フレームワークの構築であったり、後には Unicode 化と同義となっていきました。また、後者はあらかじめソフトウェア側を gettext 等で抽象化しておくこと言語を切り替えて利用できるような環境が整備されていきました。Rails では後者の意味での I18N が整備されつつあります。

M17N

M17N は Multilingualization の略で、多言語化を意味しています。M17N は

と言った意味がありますが、Ruby M17N では後者の意味で用いています。

I18N 等の語源

ちなみに、「最初の 1 文字 + 間の文字数 + 最後の文字」という略し方は、DEC 起源だそうです。

UCS Normalization と CSI

多言語化の手法には、UCS 正規化方式と CSI 方式が存在します。両者はシステム内部で文字列のエンコーディングをどうするかにおいて異なり、それぞれ利点と欠点が存在します。

UCS Normalization 方式

UCS 正規化方式では、システムの内部コードを一つの文字集合 (Universal Character Set) に統一し、システムではこのコードに決め打ちして文字を扱います。このため、基本的な部分においては、それまでのロケールに決め打ちしたスタイルを続けることができる点が、この方式の最大のメリットになります。入出力に際しては、内部コードへのデコード・内部コードからのエンコードを行います。具体的には、入力時に外部から入ってくるバイト列は全て内部コードに変換してから取り扱います。また、出力時には内部コードからバイト列に変換を行ってから出力を行います。このような、唯一特別な内部コードに正規化し、入出力を変換するというアプローチは、現在多くの言語・環境で採用されており、Perl, Python, Java, .NET, Windows, Mac OS X などなど。つまるところ、Ruby 以外のほとんどがこの手法を採用しています。

Perl's case (UTF-8)

例えば PerlUnicode による UCS Normalization 方式を採用しています。扱える文字の集合は Unicode のバージョンに依存します。UTF-8 は可変長のエンコーディングですので、Perl を始めとして UTF-8 を内部コードとして採用した環境では、何文字目かと何バイト目かの変換で苦労することになります。Perl では位置をキャッシュする等かなり手を入れているそうです。通常 Unicode が内部コードの場合、文字とは Unicode Scalar Value の事であると定義します。結合文字を考慮した概念である 書記素クラスタ (Graphem Cluster) をいかにうまく扱うかが今後の課題でしょう。

$str   = decode("UTF-8", "\xE3\x81\x82"); #=> "あ"
$bytes = encode("UTF-8", "あ"); #=> "\xE3\x81\x82"
Java's case (UTF-16)

Java の内部は UTF-16 です。Java 1.5 より前では、U+0000-U+FFFF の範囲、つまり 今の Unicode や ISO/IEC 10646 の基本多言語面 (Basic Multilingual Plane, BMP) を 16bit の固定長で表していました*1。しかし、16bit では世界中の文字が収まらないことが明らかになり*2、Unicode 2.0 からはサロゲートペアという、16bit の code unit を 2 つ用いて BMP 外を表す仕組みを導入しました*3。このため、Java 1.5 以降.NET Framework のような UTF-16 を内部コードに採用した言語では、「文字」の単位がサロゲートペアを構成する片方の unit のみになってしまっている場合があることに注意する必要があります。

ちなみに、Python もデフォルトでは内部コードに UTF-16 を用います。 (2.x は --enable-unicode=ucs4、3.0 では --with-wide-unicode で UTF-32 を用いるようにも変更可能。なお、Fedora や Ubuntu では UTF-32 を用いるものが配布されている)

Mosh's case (UTF-32)

Scheme 処理系である Mosh は内部 UTF-32 です。UTF-32 は通信用のエンコーディングとしてはほとんど使われないため、入出力ではほぼ常に変換が必要になりますが、一方で文字が固定長になるので処理が簡単になります。

TRON's case (TRON コード)

TRON も UCS Normalization 方式を採用していますが、内部コードが Unicode ではありません。TRON プロジェクトでは Unicode 2.0 を内包した TRON コードを定義し、それを内部コードとして利用しています。TRON 以外には soopy が TRON コードを内部コードとして採用しています。

CSI 方式

Code Set Independent 方式の場合には UCS 正規化方式のように、唯一絶対の内部コードというようなものは存在しません。全てのエンコーディングを対等に扱っています。また、この方式の場合、外部で用いられているコードと内部で用いられるコードが一致するため、不必要な変換が行われません。ここから、変換にかかる処理の削減を図ることができるだけでなく、変換による思わぬ情報の欠落を未然に防ぐことができます。CSI は Ruby 以外に、SolarisCitrus といった、__STDC_ISO_10646__ でない C を用いて構築された環境で用いられています。__STDC_ISO_10646__ が定義されている C の場合、wchar_t の中の数値が 0x3042 ならば、それはひらがなの「あ」を意味しています。しかし、CSI の場合、そうだとは限りません。よって、メモリの中に入っている値を覗き見て、その意味を勝手に断定することは、バグの元となります。CSI 方式で文字を扱う場合は、必ず文字を扱う関数を通して文字列を扱わなければなりません。

Ruby M17N の概念

CSI 方式

先述の通り、Ruby は他のほとんどの言語が採用している UCS 正規化方式ではなく、Code Set Independent 方式、つまり文字集合独立な手法を選択しています。これにより、不必要なエンコーディングの変換のオーバーヘッドを減らすことが可能であったり、Unicode 以外の文字集合に基づくエンコーディングに無理なく対応することができたりします。

String がエンコーディングを持つ

Ruby M17N では CSI 方式を採用しているので、文字列のエンコーディングを決めうちすることができません。String ごとに全く別のエンコーディングである可能性もあります。よって、Ruby M17N の String は自分のエンコーディングを知っています。そして、全ての文字列処理はそのエンコーディングに基づいて行われています。

 # coding: UTF-8
 "あいうえお".encoding #=> #<Encoding:UTF-8>

Script Encoding

ソースコード中のリテラルのエンコーディングは、基本的に script encoding によって決定されます。script encoding はソースファイルごとに異なり、Ruby からは __ENCODING__ で取得することができます。なお、script encoding として利用できるのは ASCII 互換なエンコーディングに限られます。後で述べる magic comment を記述しなかった場合、script encoidng は US-ASCII になります。よって、ソースコード中に 非 ASCII な文字列を書きたい場合は、次に説明する magic comment を書く必要があります。

なお、標準入力から読み込んだスクリプトや、コマンドラインオプション -e で与えたスクリプトの場合は、magic comment がなかった場合、ロケールが script encoding として用いられます。このため、1 行スクリプトを書く場合にまでいちいち magic comment を書く必要はありません。

通常のスクリプトの優先順位
magic comment > コマンドラインの -K > RUBYOPT の -K > shebang の -K > US-ASCII
-e や標準入力の優先順位
magic comment > コマンドラインの -K > RUBYOPT の -K > locale

Magic Comment

magic comment は XML でいう XML 宣言の encoding 属性のようなもので、これを記述することで script encoding を指定できます。magic comment を書かなかった場合、script encoding は US-ASCII とみなされます。magic comment は 1 行目が shebang 行ならば 2 行目、なければ 1 行目に、正規表現で /coding[:=]\s*[\w.-]+/ にマッチする形式、一般的には Emacs か Vim の modeline の形式でエンコーディングを表記します。なお、magic comment はその名の通りコメントとして記述する必要があります。

 #!/bin/env ruby
 # -*- coding: utf-8 -*-
 puts "Emacs 風"
 # vim:fileencoding=utf-8
 puts "Vim 風 1"
 # vim:set fileencoding=utf-8 :
 puts "Vim 風 2"
 #coding:utf-8
 puts "シンプル"

なお、非 ASCII な文字を含むリテラルを magic comment なしに書いていた場合、US-ASCII ではない文字が存在しているとして、invalid multibyte char というエラーになります。これはソースコードの可搬性を確保するための処置です。スクリプトの作者は自分の書いたスクリプトがどのエンコーディングで記述されているかを知っています。しかし、そのスクリプトを入手した第三者が後からエンコーディングを知ることは簡単ではありません。日本語ならば NKF.guess 等でエンコーディングを推測することも不可能ではありませんが、ヨーロッパ系のエンコーディングの場合は、後から推測することが不可能な場合もあります。このため、Ruby 1.9 では ASCII 外の文字をソースコード中に記述する場合は、magic comment が必須、という方針になっています。以上のような理由から、magic comment の効果は書かれたスクリプトのみに限定され、例えば script encoding を指定して require するといった機能は提供されていません。

外部エンコーディングと内部エンコーディング

Ruby 1.9 では IO は入力された文字列にエンコーディングを設定したり、エンコーディングを変換したりします。また、出力も自動変換させることが出来ます。この挙動を決定するのが、それぞれの IO の外部エンコーディングと内部エンコーディングです。

ある IO からの入力を String#force_encoding したくなったら、その IO に外部エンコーディングを指定できないか考えてみるべきです。また、String#encode したくなったら、内部エンコーディングを指定できないか考えてみるべきでしょう。

IO については後に詳しく説明しています。

default_external と default_internal

Encoding.default_external は IO のデフォルトの外部エンコーディングを、Encoding.default_internal は IO のデフォルトの内部エンコーディングを返します。これらは標準入出力、コマンドライン引数、open 等で開くファイル等で、明示的な指定を行わなかった場合に外部または内部エンコーディングとして用いられます。

Encoding.default_internal が設定されている場合は、全ての入力された String のエンコーディングは Encoding.default_internal の返すエンコーディングと等しいと仮定することが可能になります。この場合、ライブラリが返す文字列も Encoding.default_internal になっているべきです。

ライブラリ等が返す文字列のエンコーディングの初期値を、Encoding.default_external にすることは避けるべきです。なぜならば、あくまでこれはデフォルトの外部エンコーディングであって、内部エンコーディングについては一言も触れていないからです。Encoding.default_internal は一見この基準として用いることができるかのように見えます。しかし、デフォルトでは Encoding.default_internal が nil であることを忘れてはいけません。

default_external
コマンドラインオプションの -E / -U / -K > RUBYOPT の -E 等 > shebang の -E 等 > locale
default_internal
コマンドラインオプションの -E / -U > RUBYOPT の -E 等 > shebang の -E 等 > nil

Command Line option -E と -U

コマンドラインオプション -E は、-Eex[:in] という形式で、Encoding.default_external と Encoding.default_internal を与えます。また、-U は両者に UTF-8 を設定します。これらは標準入力から与えたスクリプトやコマンドラインオプション -e で与えたスクリプトの script encoding にも影響します。通常の引数として与えたスクリプトの script encoding には影響しません。

ロケールエンコーディング

ロケールエンコーディングを決定するには、まず locale_charmap を決定します。まず、Unix 環境でも Windows 環境でも、環境変数 $LANG が設定されていた場合は、そこエンコーディング名を決定します。Windows (cygwin を含む) で $LANG が設定されていなかった場合、GetConsoleCP*4 を用います。以上から決定された名前は Encoding.locale_charmap で取得することができます。なお、Encoding.locale_charmap は、miniruby では ASCII-8BIT を、nl_langinfo 等がない環境では nil を返します。

こうして得られた locale_charmap からロケールエンコーディングは決定されます。基本的には Encoding.find(Encoding.locale_charmap) の値と等しくなりますが、locale_charmap が nil だった場合には US-ASCII、Ruby が理解できない名前の場合には ASCII-8BIT になります。以上で決定されたロケールエンコーディングを取得するには、Encoding.find("locale") を用います。

ロケールエンコーディングの Ruby 内での主な利用用途は、前述の default_external のデフォルト値を与えることです。先述のとおり、default_external は IO の外部エンコーディングのデフォルトですが、ファイル等を開く際には別にエンコーディングを指定することが推奨されるので、default_external が影響するのは Ruby が最初から開いている IO、つまり $stdin、$stdout、$stderr になります。通常これらはコンソールとの入出力となるため、コンソールで用いられるエンコーディング、すなわち、基本的には $LANG、Windows では GetConsoleCP を用いるのが妥当だと判断されました。なお、Windows においては入出力に Unicode 版 API を用いて、UTF-16LE を使うという方法も存在はしますが、Ruby 1.8 系との互換性を著しく損なうため採用されていません。*5

以上の通り、ロケールエンコーディングは「default_external のデフォルト値」やコンソールとの関係を念頭に決定されているため、特に Windows においてはそれ以外の用途で用いた場合、期待とは異なるエンコーディングが返る可能性があります。「default_external のデフォルト値」以外の意味で用いる場合には ruby-dev に一報を入れておいた方がよいかも知れません。また、将来的に GetConsoleCP から GetACP への切り替えが行われる可能性があります。

ファイルシステムエンコーディング

ファイルシステムとのやりとりに用いられるエンコーディングは、ロケールエンコーディングとは別に決定されるファイルシステムエンコーディングに基づきます。具体的には、システムから取得したファイル名等を表す文字列のエンコーディングとしてファイルシステムエンコーディングは用いられます。このエンコーディングを取得するための Ruby API は提供されていません*6

Windows の場合

FAT32 や NTFS など、ロングファイルネーム対応ファイルシステムの場合、ファイル名は UTF-16LE*7 で格納されています。また、FAT でも NT 系ならばシステムに読み込んだ後は UTF-16LE で扱われます。つまり、Windows、特に NT 系においてはシステム内部では UTF-16LE でファイル名を扱っています。

Ruby 1.9.1 は ANSI 版 API を用いているので、この文字列は、Windows によって ANSI または OEM コードページ*8 の文字列に変換されて Ruby へと渡されます。つまり、結局 Ruby 1.9.1 から見るとファイル名は常に ANSI または OEM コードページとして見えることになるので、ファイルシステムエンコーディングは ANSI または OEM コードページになります。Ruby はこの文字列を ANSI または OEM コードページとしてエンコーディングを設定し、さらにオプションでエンコーディングが設定されていた場合はそのエンコーディングに変換して返します。

将来 Unicode 版 API を用いるようになった場合は、UTF-16LE のファイル名を Ruby は取得し、オプションでエンコーディングが指定されている場合にはこれを直接そのエンコーディングへ、指定されていない場合はファイルシステムエンコーディングに変換して返すようになるでしょう。ファイルシステムエンコーディングを UTF-16LE や UTF-8 に変えてしまうと、Ruby 1.8 系との互換性を著しく損なうため、この場合でもそのまま ANSI または OEM コードページになります。

Unix 系の場合

Unix 系の場合、ファイルシステムに保存されているファイル名のエンコーディングは一般には特定できません。よって、ロケールエンコーディングをファイルシステムエンコーディングとし、取得したファイル名を表すバイト列にはこれを設定して返します。

Mac OS X の場合

Mac OS X の HFS+ の場合、ファイル名はアップルによって修正された Normalization Form D (分解済み) という形式の UTF-16 で格納されており、POSIX API ではこれを UTF-8 形式に変換して返します。つまり、Carbon 経由で保存したファイルの名前は UTF8-MAC として返されることになります。よって、ファイルシステムエンコーディングは UTF8-MAC としています。ただし、POSIX API はファイル名をバイト列として扱うため、POSIX API から書き込んだファイルの扱いは Unix と同様になります。

Windows の場合ファイルシステム自体のエンコーディング
→システムの内部エンコーディング (UTF-16LE) へと変換 (Windows 内部)
→Ruby のファイルシステムエンコーディングへと変換 (Ruby 内部)
Unix 系の場合ファイルシステムにはバイト列として保存
→Ruby のファイルシステムエンコーディングを設定

こちらの詳細は「ファイルパスのエンコーディング」を参照してください。

Ruby M17N の実装

Ruby M17N では以上のような概念を元に実装が行われています。しかし、実装ではただその概念を実装するだけでなく、開発リソースによる制約や、使い勝手を考慮した調整、過去との互換性を考慮した機能など、様々な要素が混在しています。

Ruby の扱うエンコーディング

Ruby は多くのエンコーディング (Ruby 1.9.1 にて Encoding.list.length #=> 83) をサポートしています。と、言ってもここでの「サポート」とは、Unicode 正規化方式を採用したシステムのように、そのエンコーディングから Unicode への変換表を持っているという意味ではありません。Ruby M17N は CSI 方式ですので、そのエンコーディングで符号化された文字列の扱い方を知っている、ということになります。 Ruby M17N では主に開発リソース的な理由から、全てのエンコーディングを平等に扱うのではなく、エンコーディングを 3 種類にわけ、それぞれの段階に応じたサポートを提供しています。具体的には、ASCII 互換エンコーディング、ASCII 非互換エンコーディング、ダミーエンコーディングの 3 つです。

ASCII Compatible Encoding

US-ASCII に含まれる文字を \x00-\x7F で表すエンコーディングのことを、ASCII 互換エンコーディングと呼びます。Ruby はこのエンコーディングの文字列をフルサポートします。Ruby のソースコードで用いるエンコーディングはこの ASCII 互換エンコーディングに限られます。また、最も大きな特徴として、本来同じエンコーディング同士でしか比較・結合等ができないところ、ASCII 互換エンコーディングの文字列は、ASCII のみを含む文字列 (String#ascii_only? が真 な文字列) と比較・結合することができます。

ASCII の範囲

コードポイント 0x00-0x7F はどのエンコーディングでも常に US-ASCII と一致すると仮定しています。そのため、Ruby M17N では Shift_JIS もこの範囲は JIS X 0201 Roman ではなく、US-ASCII とみなしています。(なお、transcode による変換については一般的な Shift_JIS の定義に準じている)

ASCII ONLY

文字列の内容が ASCII のみでかつ、ASCII 互換エンコーディングである場合、この文字列を ASCII ONLY であると呼びます。ASCII ONLY な文字列は、他の ASCII 互換エンコーディングの文字列と自由に比較・結合・正規表現マッチさせることができます。

 # coding: UTF-8
 a = "いろは".encode("Shift_JIS") # Shift_JIS にする
 a.ascii_only? #=> false
 b = "ABC".encode("EUC-JP") # EUC-JP にする
 b.ascii_only? #=> true
 c = a + b #=> "いろはABC" # a と b のエンコーディングが異なっていてもよい
 c.encoding #=> #<Encoding:Shift_JIS>
ASCII-8BIT

ASCII 互換エンコーディングに属するエンコーディングの中に、ASCII-8BIT があります。これは「ASCII 互換オクテット列」に与えられるエンコーディングです。言い換えると、一般的な「文字列」とは異なっているが、バイナリとも異なり ASCII 互換であるということです。 つまり、後で述べる ASCII 非互換なエンコーディングな文字列と異なり、ASCII のみの文字列と結合・比較等を行うことが可能です。なお、Ruby 1.9.1 では ASCII 非互換なバイナリエンコーディングは必要性がないと思われたので用意されていません。

Emacs-Mule

Emacs/Mule が内部で用いているエンコーディング です。ISO 2022 的なアプローチでの多言語化を目指しつつ、ステートレスな可変長のエンコーディングとなっています。独自の ISO-2022-JP をステートレスに扱うためのエンコーディング、stateless-ISO-2022-JP はこのエンコーディングを元に実装されています。

ASCII Incompatible Encoding

US-ASCII に含まれる文字を \x00-\x7F 以外で表すエンコーディングを ASCII 非互換なエンコーディングと呼びます。これらに対しては限定的なサポートが提供されます。ASCII 非互換なエンコーディングはソースコードで用いることはできませんし、先述の ASCII のみの文字列との結合もサポートされません。Ruby 1.9.1 では UTF-16BE、UTF-16LE、UTF-32BE、UTF-32LE の 4 つがこれに該当します。

UTF-16 & UTF-32

先述の通り、Ruby 1.9.1 では UTF-16BE、UTF-16LE、UTF-32BE、UTF-32LE がサポートされています。これらは全て BOM *9なしのエンコーディングです。よって、U+FEFF は ZERO WIDTH NO-BREAK SPACE として認識されるので、BOM であることがわかっている場合は適宜削ってください。また、Ruby 1.9.1 では、"UTF-16" と "UTF-32" はサポートしていないことに注意してください。

なお、BOM 付きの UTF-16 と UTF-32 がサポートされていないのは、開発リソース上の問題です。一時本格サポートが検討はされましたが、BOM を考慮しながらのバイト位置計算や、1 つのエンコーディングでのエンディアンに応じた処理の提供、IO が絡んだ際の処理の複雑さ等から、1.9.1 でのサポートは断念されました。取り込むこと自体への反対はないので、誰かが手を挙げてパッチを作成すればおそらく取り込まれることでしょう。

ASCII 非互換なエンコーディングは一部の先述の通りサポートが限定されているので、本格的に扱いたい場合は UTF-8 に変換して扱うことが推奨されます。

ちなみに、UCS-2BE というエイリアス名が UTF-16BE のエイリアス名として定義されていますが、これはあくまで UCS-2BE と名付けられたデータを読み込む際の便宜を図ったもので、Ruby 1.9.1 は UCS-2BE をサポートしません。

Dummy Encoding

ダミーエンコーディングは Ruby が名前を知っているだけのエンコーディングです。Ruby はただバイト列としてのみこれらを扱い、文字列としてのサポートは一切提供されません。もちろん ASCII のみの文字列との結合・比較等も行うことができません。もっぱらステートフルなエンコーディングがこれにあたり、Ruby 1.9.1 では ISO-2022-JP や UTF-7 がこれに該当します。 これらのエンコーディングを Ruby で扱う場合は、stateless-ISO-2022-JP や UTF-8 に変換してから扱うことが推奨されます。

 Encoding::ISO_2022_JP.dummy? #=> true
 a = "いろは".encode("ISO-2022-JP") # ISO-2022-JP にする
 b = "ABC".encode("EUC-JP") # EUC-JP にする
 b.ascii_only? #=> true
 c = a + b
 #=> Encoding::CompatibilityError: incompatible character encodings: ISO-2022-JP and EUC-JP

エンコーディングの追加

拡張ライブラリは自分で新しいエンコーディングを定義することが可能です。全く新規に定義するのは手間ですが、C API の rb_enc_replicate で他のエンコーディングのレプリカを作ったり (「レプリカ」の概念は C API からのみ扱うことができ、Ruby のレイヤーからは触れることができない)、rb_define_dummy_encoding でダミーエンコーディングを作ったりできます。もっとも未サポートのエンコーディングを追加したい場合は標準サポートの要望を出すのが原則です。

やむをえず新たなエンコーディングを追加する際は、極力既存のエンコーディングから近いものをレプリカ元にするべきです。また、ダミーエンコーディングを定義する場合は、ダミーは ASCII ONLY な文字列と結合できないことを思い出し、本当にダミーでいいのか一度考え直しましょう。

Special Encoding Name

これら以外に、Ruby 内部で用いているエンコーディング、locale encoding, default external encoding, default internal encoding を参照するための名前、"locale", "external", "internal" が定義されています。 なお、それぞれのソースファイルの script encoding を参照するには、特殊変数 __ENCODING__ を用います。

 # coding: UTF-8
 locale = Encoding.find("locale")
 external = Encoding.find("external") # Encoding.default_external でも取得可能
 internal = Encoding.find("internal") # Encoding.default_internal でも取得可能
 __ENCODING__ #=> #<Encoding:UTF-8>

Encoding

以上に挙げたエンコーディングのリストや特殊なエンコーディング等の管理、さらにそれぞれのエンコーディングの情報を司るのが Encoding です。なお、Ruby の Encoding オブジェクトが内部に持っているのは変換表ではなく、そのエンコーディングのバイト構造や文字の情報 (具体的には鬼車用のエンコーディングモジュール) が入っています。変換表は、transcode 管轄となる、Encoding::Converter の中に入っています。

エンコーディングの取得

Ruby のサポートしているエンコーディングのリストを得るには、Encoding.list や Encoding.name_list、Encoding.aliases を用います。

 p Encoding.list # サポートするエンコーディングのオブジェクトの配列
 #=> [#<Encoding:ASCII-8BIT>, #<Encoding:UTF-8>, #<Encoding:US-ASCII>, ...]

 p Encoding.name_list # サポートするエンコーディング名とエイリアス名の配列
 #=> ["ASCII-8BIT", "UTF-8", "US-ASCII", ..., "locale", "external", "internal"]

 p Encoding.aliases # エイリアス名とエンコーディング名のハッシュ
 #=> {"BINARY"=>"ASCII-8BIT", "SJIS"=>"Shift_JIS", "CP932"=>"Windows-31J", ...}

特定のエンコーディングオブジェクトを得るには、Encoding.find を用います。

 p Encoding.find("UTF-8") #=> #<Encoding:UTF-8>

 p Encoding.find("eucJP") #=> #<Encoding:EUC-JP>

 p Encoding.find("locale") #=> #<Encoding:Windows-31J> # 日本語 Windows の場合

 p Encoding.find("jis") #=> ArgumentError: unknown encoding name - jis

また、Encoding を表す定数を用いることもできます。定数名はエンコーディング名やエイリアス名を全て大文字にし、- を _ にしたものです。

 p Encoding::UTF_8  #=> #<Encoding:UTF-8>

 p Encoding::EUC_JP #=> #<Encoding:EUC-JP>
 p Encoding::EUCJP  #=> #<Encoding:EUC-JP>

デフォルトの外部・内部エンコーディング等を得るには Encoding.default_external 等を用います。また、Encoding.find を用いる方法もあります。

p Encoding.default_external #=> #<Encoding:Windows-31J> # 日本語 Windows のデフォルト
p Encoding.find("external")

p Encoding.default_internal #=> nil
p Encoding.find("internal")

個別エンコーディングの情報取得

Encoding#nameEncoding#inspect 以外に、そのエンコーディングが dummy encoding かどうかを得る Encoding#dummy? が存在します。

なお、ある Encoding が ASCII 互換かどうかを知るには Encoding クラスのメソッドでなく、Encoding::Converter.asciicompat_encoding を用いるのがよいでしょう。このメソッドは ASCII 互換なエンコーディングや存在しないエンコーディングの場合には nil を、ASCII 非互換なエンコーディングやダミーエンコーディングの場合は、同じ文字集合を持つ ASCII 互換エンコーディングを返します。

その他

これらの他に、2 つの String や Encoding が比較・結合できるか判断し、結合した場合の結果となる Encoding を返す Encoding.compatible?(str1, str2) などがあります。 これ以外の Encoding オブジェクトの機能については、るりまの Encoding の項目 を参照ください。

String

Ruby 1.8 では String とは、基本的にただのバイト列でした。バイト列ゆえに自由度は高く、それが Shift_JIS だと思えば Shift_JIS に、それが EUC-JP だと思えば EUC-JP に、それが UTF-8 だと思えば UTF-8 にと、どのエンコーディングでも一応扱うことはできましたが、 「文字列」としてのサポートは $KCODE を設定した場合の正規表現等や jcode.rb を通して提供される、限定的だったり使いづらいものに留まっていました。

Ruby 1.9 の Ruby M17N では String それぞれにエンコーディングが関連付けられています。これにより、Ruby はバイト列に対してそのエンコーディングに沿った扱いをすることが可能となります。Ruby が知っているエンコーディングで符号化された String は、その String がどのようなエンコーディングで符号化されているかに関わらず、文字列として扱うことができます。さて、これから 1.9 で String オブジェクトがどう変わったか見ていきましょう。

文字列のエンコーディング

先述の通り、Ruby 1.9 では文字列それぞれが自分のエンコーディングを持っています。String#encoding で、その文字列のエンコーディングを表す Encoding オブジェクトを取得することができます。

 # coding: EUC-JP
 "あいうえお".encoding #=> #<Encoding:EUC-JP>
 "\u{3042}".encoding #=> #<Encoding:UTF-8>

文字オブジェクト

まず、文字列は「文字」の「列」なので、「文字」について見ていきます。Ruby M17N にはいわゆる文字オブジェクト、文字を表す専用のクラスは存在しません。Ruby M17N では文字を表す際に、内容が 1 文字の String を用いています。

Ruby M17N 開発初期には文字専用のクラスの導入が検討されていました。しかし、「文字オブジェクト」に必要な要素が、コードポイント、エンコーディング、バイト列であるところ、String はこれらをすべて持っているため、大クラス主義をとる Ruby では文字を表現する際に 1 文字 String を用いることになりました。

この方法は文字列中の文字位置の取得が可変長のエンコーディングで遅くなる代わりに、外部から読み込んだバイト列にエンコーディングをつけるだけで文字列として扱えたり、文字の単位をエンコーディングの付け替えだけで変えられたりと、応用の範囲が広がるというメリットもあります。

String#[]

文字列から文字を取得するには、String#[] を用います。1.8 では String インデクサの戻り値は、そのインデックスの示すバイトの値、つまり数値でした。つまり、この例でインデックスが「0」ならば、バイト列の 0 バイト目の値、この例では 0xE3 が返ります。

1.9 ではインデクサの戻り値はその添え字の示す文字 (1 文字 String)。つまり、添え字が 0 ならば、0 文字目の「あ」が返ります。Ruby 1.9 の String はまさに「文字」の「列」となっているわけです。

1.8
 String#[] #=> Fixnum (1 byte)
 "あいう"[0] #=> 0xE3 # UTF-8 の場合
1.9
 String#[] #=> 1 文字 String
 "あいう"[0] #=> "あ"

文字リテラル

文字オブジェクトは存在しませんが、実際に文字をプログラム中に記述する際には文字リテラルを用いることができます。旧来通りの ?a といった表記だけでなく、 ?あ のように日本語の文字も同様に書くことができるようになっています。また、ASCII コードをエスケープして記述する記法と似た、Unicode 記法も導入されました。 その文字のエンコーディングは、Unicode 記法を用いた場合は UTF-8 に、それ以外ではそのソースコードの script encoding が設定されます。

?a
?\t
?あ
?\u3042

String#ord と Integer#chr

こうしてできた文字をコードポイントに変換したい場合は、String#ord を用います。ひらがなの「あ」の場合、"あ".ord はどのような値になるでしょうか。正解はエンコーディングに依存し、例えば UTF-8 なら 12354 になります。 逆にこの、例えば 12354 を chr すると・・・例外が出ます。正しくは、エンコーディングを与える必要があります。12354.chr("UTF-8") と指定してやると、「あ」を得られるようになります。 以上の通り、Ruby M17N のような CSI 方式を採用するシステムでは、コードポイントを直接操作する処理はエンコーディングに依存し、無用な複雑さを導入することになります。よって、コードポイントやバイト列を直接操作することは極力避け、文字列として操作することを心がけましょう。

 # coding: utf-8
 "あ".ord #=> 12354

 12354.chr("UTF-8") #=> "あ" in UTF-8

文字列リテラル

文字列リテラルの基本は Ruby 1.8 から変わりません。変更点として、従来の \OOO \xHH に加えて、Unicode エスケープとして \uXXXX と \u{XXXX} が追加されました。

なお、文字列リテラルから作られた String のエンコーディングは、基本的に書かれたソースコードの script encoding に一致します。例外として、Unicode エスケープを用いた場合、その String のエンコーディングは UTF-8 となります。また、script encoding が US-ASCII の場合にバイトエスケープを用いて生成された非 ASCII な String のエンコーディングは ASCII-8BIT になります。

 # coding: EUC-JP
 "あ".encoding #=> #<Encoding:EUC-JP>
 "\u3042".encoding #=> #<Encoding:UTF-8>
 "\u{3042 3044 3046}" #=> "あいう"

 "abc".encoding #=> #<Encoding:US-ASCII>
 "\x82\xA0".encoding #=> #<Encoding:ASCII-8BIT>

String#length

String#length も文字を意識するように変わりました。1.8 では String のバイト列としての長さを返していましたが、1.9 では文字列としての長さを返すようになっています。 なお、バイト列としての長さが欲しい場合は新しく追加された、String#bytesize メソッドを使用します。

1.8
 * String#length   #=> byte length
 "あいう".length #=> 9 (UTF-8)
1.9
 * String#length   #=> character length
 * String#bytesize #=> byte length
 "あいう".length #=> 3
 "あいう".bytesize #=> 9 (UTF-8)

String#each_*

Ruby 1.9 では String#each は削除されました (つまり、String は Enumerable ではない)。なぜかというと、String の何について繰り返すのかが明確ではないためです。

その代わり、4 つの each 系メソッドが追加されました。バイトごとの String#each_byte、コードポイントごとの String#each_codepoint、文字ごとの String#each_char、行ごとの String#each_line が追加されています。これらのメソッドはブロックを取った場合は従来の each の様に振る舞い、ブロックを省略すると Enumerator を返します。また、それぞれに対応する複数形メソッド String#bytesString#codepointsString#charsString#lines も追加されています。これは、今でも Ruby の String がただ文字の列であるだけでなく、バイトの列や、コードポイントの列、行の列をも表していることを意味しています。

なお、これらのうち、each_codepoint 以外のメソッドは 1.8 系にもバックポートされ、1.8.7 から実装されています。

文字列の比較・結合

Ruby 1.9 では文字列の比較・結合に大きな変更が入っています。まず、文字列の比較ではバイト列として一致していても、エンコーディングが異なる場合は String#== は false を返します。バイト列表現とエンコーディングの両方が一致して初めて true を返すのです。なお、双方が ASCII 互換エンコーディングでかつ、内容が ASCII 文字のみの場合は、エンコーディングが異なっていても true を返します。

文字列の結合では、双方のエンコーディングが異なる場合、基本的には例外 Encoding::CompatibilityError が発生します。ただし、双方が ASCII 互換エンコーディングで、少なくとも一方が ASCII 文字のみで構成される場合は結合が可能です。また、片方が空文字の場合はエンコーディングにかかわらず結合が可能となります。

バイト列としての String

ここまで文字列としての String を取り上げてきましたが、今でも String はバイト列をもサポートしています。しかし、現在、Ruby 1.9 におけるバイト列のサポートは限定的なものに留まっています。1 バイト読み込む String#getbyte(index)、1 バイト書き込む String#setbyte(index, value)、バイト長を取得する String#bytesize の 3 つが現在のバイト関係のメソッドです。 より高レベルな機能の案がある方は提案してみるといいかもしれません。

String#force_encoding

String#force_encoding は String のエンコーディングを設定する破壊的メソッドです。同じバイト列で異なるエンコーディングを持つ新しい String を生成したい場合は、Object#dup と組み合わせます。

たいていの場合は文字列を生成したとき、ファイル等から読み込んだときに、エンコーディング名を指定することで、String にエンコーディングが設定されているはずですから、明示的に String#force_encoding を呼ぶ機会は滅多にないはずです。例えば、ネットワークライブラリや XML のライブラリ等が、HTTP ヘッダや XML 宣言など別のレイヤーで与えられるエンコーディングを、ライブラリ内部で設定したい場合。また、文字列として処理していた String をバイト列として処理したい場合に str.force_encoding("ASCII-8BIT") したい場合など、限られたケースになるでしょう。

ライブラリのユーザが自分で String#force_encoding を呼ばなければならないならば、多くの場合それはライブラリの設計が間違っています。このメソッドの名前が set_encoding や encoding= でなく、また破壊的メソッドしか用意されているのは、安易な利用を戒める意図もあります。

String#valid_encoding?

String#valid_encoding? はそのエンコーディングにおけるバイト構造に、その String の内容が沿っているかを判断します。このメソッドではある String がそのエンコーディングにおいて「正しい」かをバイト構造のレベルでは判断できますが、その String に含まれる文字が全て実際に定義されているかまでは保証しません。文字が定義されているかどうかまでを判断するには、Encoding::Converter 等を用いて変換を行ってみるという方法があります。

String#gsub(pattern, hash)

Ruby 1.9.1 では String#gsub(pattern, hash) が追加されました。旧来の String#gsub では置換文字列をマッチした文字列によって変えたい場合、ブロックを与える方法しか用意されていませんでした。しかし、ブロックの呼び出しはそれなりにコストのかかる処理です。

str.gsub(pattern, hash) は str.gsub(pattern){hash[$&]} と等しい処理になります。しかし、前者ではブロックを呼び出さず C レベルでハッシュ引きまでを行うため、より高速な処理が可能となります。主に特定の文字をエスケープするような処理で効果を発揮すると思われます。

String#inspect / String#dump

これは 1.9 での変更点ではありませんが、念のため。String#inspect は人がパッと見てそれが何か知るための簡易メソッドです。String をエスケープしたりダンプしたい場合は String#dumpMarshal.dump を使ってください。なお、String#dump は元の文字列のエンコーディングを、ASCII 互換エンコーディングの場合は戻り値のエンコーディングとして保持するので、ファイルに書き出す場合などは、読み込み時に別途エンコーディングを指定する必要があります。

  • String#inspect #=> p 等でのぱっと見用
  • String#dump #=> ダンプ用 (str == eval(str.dump) が保障される)
 # coding: UTF-8
 "あいう".inspect #=> "あいう" (UTF-8)
 "あいう".dump #=> "\u{3042}\u{3044}\u{3046}" (UTF-8)

 "あいう".encode("EUC-JP").inspect #=> "あいう" (EUC-JP)
 "あいう".encode("EUC-JP").dump #=> "\xA4\xA2\xA4\xA4\xA4\xA6" (EUC-JP)

 "あいう".encode("UTF-16LE").inspect #=> "B0D0F0" (US-ASCII)
 "あいう".encode("UTF-16LE").dump #=> "B0D0F0".force_encoding("UTF-16LE") (ASCII-8BIT)

 "あいう".encode("ISO-2022-JP").inspect #=> "\e$B$\"$$$&\e(B" (US-ASCII)
 "あいう".encode("ISO-2022-JP").dump #=> "\e$B$\"$$$&\e(B".force_encoding("ISO-2022-JP") (ASCII-8BIT)

ちなみに、Kernel#p の戻り値は 1.8 では nil でしたが、1.9 では引数をそのまま返すようになっています。

Regexp

意識されることは少なかったと思われますが、正規表現には以前からエンコーディングが存在していました。例えば Ruby 1.8 では、/a/e.kcode は "euc" を返していました。しかし、Ruby 1.8 における GNU regex ベースの実装では SJIS、EUC、UTF8 にしか対応しておらず、機能的にも戻り読みがないなど一歩後れを取っていました。

Ruby 1.9 では 鬼車 5.9.1 相当 がエンジンとして用いられています。利用できる語彙は大幅に増え、従来用いることができなかった戻り読みや、名前付き捕獲式集合、さらには文脈自由文法へのマッチが可能となる部分式呼び出し等が可能になります。

正規表現とエンコーディング

正規表現マッチの場合でももちろん、エンコーディングが考慮されます。つまり、/./ は改行以外の文字に対してマッチします。この場合も比較・結合の場合と同様、エンコーディングが一致しない場合は Encoding::CompatibilityError が発生します。

Regexp#force_encoding は immutable なのでありません。Regexp.new(reg.source.force_encoding(enc)) を使ってください。また、Regexp#encode も存在しないので、正規表現のエンコーディングを変更したい場合は Regexp.new(reg.source.encode(enc)) などとします。ASCII 非互換なエンコーディングの正規表現を作りたい場合も同様に Regexp.new を用います。なお、正規表現にコードポイントの値や順番に依存した記述を行っている場合は、エンコーディングの変換によって意図しない動きとなってしまうことに注意してください。

 # coding: UTF-8
 kanji_of_jis_lv1_and_lv2_in_euc_jp = Regexp.new('[亜-煕]'.encode("EUC-JP"))
 broken_regexp = Regexp.new(kanji_of_jis_lv1_and_lv2_in_euc_jp.source.encode("UTF-8"))
 # U+4E9C-U+7155 にマッチしても特にうれしくない

ASCII ONLY

正規表現でも ASCII ONLY に似た概念は存在します。Regexp が ASCII 互換エンコーディングを持ち、かつ内容が ASCII のみである場合、ASCII 互換エンコーディングを持つ String にマッチさせることが可能になります。このとき、Regexp#fixed_encoding? は false を返します。

キャプチャ構文

従来、正規表現でマッチさせた文字列をロジックで扱う際は、$& や Regexp.last_match、$1、$2 等を用いたり、String#match の戻り値を用いたり、それらをさらに別の変数に代入したりしていました。

正規表現のキャプチャ構文を用いると、キャプチャした文字列を直接ローカル変数に代入することができます。具体的には、名前付き捕獲集合を含むが #{} 等の動的な展開を含まない正規表現リテラルが =~ の左辺にある状態で正規表現マッチを行ったとき、変数名として正しい名前付き捕獲集合名を持つキャプチャ文字列を、対応するローカル変数に代入します。

 /(?<foo>f\w+)(?<bar>b\w+)/i =~ "foobar2000"
 p [foo, bar] #=> ["foo", "bar2000"]

 /(?<foo>f\w+)(?<bar>b\w+)/i =~ "FreeBSD"
 p [foo, bar] #=> ["Free", "BSD"]

 /(?<foo>f\w+)(?<bar>b\w+)/i =~ "Firebug"
 p [foo, bar] #=> ["Fire", "bug"]

ちなみに、この構文によって既存のローカル変数を上書きしてしまうのが心配な場合は、コマンドラインオプション -w を指定することで警告を出すことができます。

バイト列とのマッチ

何らかの文字列に対して、エスケープしたバイト列の正規表現をマッチするのは、たまに行う処理ではないかと思います。しかし、これは例えば ASCII-8BIT と UTF-8 で互換性がないと怒られます。 これはなぜかというと、このような処理は文字列処理ではなく、バイト列の処理だからです。この正規表現はバイト列に対する正規表現ですし、実際に使う際には右辺には不正なバイト列が現れることもあるでしょう。よって、この場合は両者を ASCII-8BIT にして処理を行うのが原則です。なお、2 つめの例が可能なのは、ASCII-8BIT が ASCII 互換であるがゆえに可能な、ASCII ONLY な正規表現に対するマッチ処理です。ASCII 非互換な encoding では、1 つめの例と同様 ArgumentError になってしまいます。

 # coding: UTF-8
 /\xE3\x81\x82/n =~ "あ"
 #=> ArgumentError: incompatible encoding regexp match (ASCII-8BIT regexp with UTF-8 string)
 # これはバイト列処理であって、文字列処理ではないため

 #両者とも ASCII-8BIT にして行うのが正しい
 bytes = "aあ".force_encoding("ASCII-8BIT")
 /\xE3\x81\x82/n =~ bytes #=> 1
 /a/ =~ bytes #=> 0

IO

IO クラスもエンコーディングを意識するようになっています。ゆえに、戻り値が文字列なのかバイト列なのかを意識する必要があります。また、文字列のエンコーディングの指定や変換を制御する「外部エンコーディング」と「内部エンコーディング」という概念が導入されました。

外部エンコーディングと内部エンコーディング

外部エンコーディングと内部エンコーディングは IO によるエンコーディングの設定や、自動変換を制御します。内部エンコーディングが設定されていない場合、入力された String には外部エンコーディングが設定されます。この詳細な動作は後に表で示します。

IO の外部エンコーディングや内部エンコーディングは、IO#open の第 2 引数や、オプションのハッシュ、開いた後は IO#set_encodingで指定できます。

 p [Encoding.default_external, Encoding.default_internal]
 #=> [#<Encoding:UTF-8>, nil] # UTF-8 ロケールの場合

 open(path, "r")        {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:UTF-8>, nil]
 open(path, "r:Shift_JIS")        {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:Shift_JIS>, nil]
 open(path, "r:Shift_JIS:EUC-JP") {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:Shift_JIS>, #<Encoding:EUC-JP>]

 open(path, "r", :encoding => "Shift_JIS")        {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:Shift_JIS>, nil]
 open(path, "r", :encoding => "Shift_JIS:EUC-JP") {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:Shift_JIS>, #<Encoding:EUC-JP>]

 open(path, "r", :external_encoding => "Shift_JIS", :internal_encoding => "EUC-JP") \
   {|f| p [f.external_encoding, f.internal_encoding] }
 #=> [#<Encoding:Shift_JIS>, #<Encoding:EUC-JP>]

 open(path, "r", :encoding => "Shift_JIS:EUC-JP") do |f|
   p [f.external_encoding, f.internal_encoding]
   #=> [#<Encoding:Shift_JIS>, #<Encoding:EUC-JP>]

   f.set_encoding(nil)
   p [f.external_encoding, f.internal_encoding]
   #=> [#<Encoding:UTF-8>, nil]
 end

エンコーディングの設定・変換

IO は前述の外部エンコーディングと内部エンコーディングの設定の有無を見て、読み込んだ文字列や書き込む文字列を変換したりエンコーディングを設定したりします。

外部内部default_internal読み込み時の動作書き込み時の動作
未指定未指定nildefault_external を設定バイト列をそのまま出力
未指定未指定指定default_external から default_internal に変換バイト列をそのまま出力
指定未指定nil外部エンコーディングを設定外部エンコーディングに変換
指定未指定指定外部エンコーディングから default_internal に変換外部エンコーディングに変換
指定指定nil外部エンコーディングから内部エンコーディングに変換外部エンコーディングに変換
指定指定指定外部エンコーディングから内部エンコーディングに変換外部エンコーディングに変換

バイト・文字・バイト列・文字列

IO のメソッドは何を扱うかによって、バイト、文字、バイト列、文字列、の 4 種類に分類できます。

文字を扱う IO#getcIO#ungetcIO#readchar は、文字を Fixnum でなく、String で表すようになりました。IO#each_char も文字を扱うメソッドです。これらの戻り値には上記ルールに則ったエンコーディングが設定された上で返されます。

IO#getc が文字を扱うメソッドとして変更されたことに伴い、バイトを扱う IO#getbyteIO#readbyte が追加されています。IO#each_byte もこれに分類できるでしょう。

IO#binreadIO#read(size)IO#read_nonblockIO#readpartialIO#sysread がバイト列を扱うメソッドです。バイト列の場合は常に ASCII-8BIT がセットされます。

サイズ指定のない IO#read など、文字列を扱うメソッドの場合は状況に応じたエンコーディングが設定されます。

IO#external_encoding と IO#internal_encoding

IO#external_encodingIO#internal_encoding は、最終的に変換等で判定で用いられる外部エンコーディングと内部エンコーディングを返します。単純に IO が持っている外部エンコーディングと内部エンコーディングを返すのではないことに注意する必要があります。

default_internal外部内部モード: w / w+ / r+モード: r
未指定未指定未指定external_encoding: nil
internal_encoding: nil
external_encoding: default_external
internal_encoding: nil
未指定未指定指定external_encoding: default_external
internal_encoding: 内部
external_encoding: default_external
internal_encoding: 内部
未指定指定未指定external_encoding: 外部
internal_encoding: nil
external_encoding: 外部
internal_encoding: nil
未指定指定指定external_encoding: 外部
internal_encoding: 内部
external_encoding: 外部
internal_encoding: 内部
指定未指定未指定external_encoding: default_external
internal_encoding: default_internal
external_encoding: default_external
internal_encoding: default_internal
指定未指定指定external_encoding: default_external
internal_encoding: 内部
external_encoding: default_external
internal_encoding: 内部
指定指定未指定external_encoding: 外部
internal_encoding: default_internal
external_encoding: 外部
internal_encoding: default_internal
指定指定指定external_encoding: 外部
internal_encoding: 内部
external_encoding: 外部
internal_encoding: 内部

ファイルパスのエンコーディング

ファイルパスのエンコーディングは基本的にはプラットフォームごとに決定されるファイルシステムエンコーディングに基づきますが、その周りの挙動もプラットフォームによって異なります。なお、このファイルシステムエンコーディングを取得する Ruby API は提供されていません。

Unix

Unix 系の OS では一般にはファイルシステムのエンコーディングは決定できません。よって、ファイル名を返す際は、システムから得たファイル名を表すバイト列に、ファイルシステムエンコーディング、またはオプションで与えたエンコーディングを設定して返します。このとき、バイト列の変換等は行いません。また、ファイル名を渡す際は渡された String のバイト列をそのままシステムに渡します。

Mac OS X

Mac OS X におけるファイルシステムエンコーディングは UTF8-MAC です。よって、ファイル名を返す際は UTF8-MAC、またはオプションで与えたエンコーディングを設定して返します。一方、ファイル名を渡す際は、渡された文字列のエンコーディングが ASCII-8BIT ならばバイト列とみなしてそのままシステムに、それ以外では UTF8-MAC へと変換した上でシステムに渡します。なお、この動作は検討が不十分なため、将来変更が行われる可能性があります (Unix に動作は合わせるなど)。

Windows

Windows の ANSI 版 API が返す文字列は ANSI または OEM コードページと一致します。よって、ファイル名を返す際は基本的にはファイルシステムエンコーディングの文字列が返されます。ただし、オプションでエンコーディングが指定された場合この文字列を指定したエンコーディングへと変換して返します。ファイル名を渡す際は、渡された文字列のエンコーディングが ASCII-8BIT ならばバイト列とみなしてそのままシステムに、それ以外では 渡された文字列をファイルシステムエンコーディングへと変換した上でシステムに渡します。

1.9.1 では開発リソースの関係から上述のような動作ですが、1.9.2 では Unicode 版 API を用いるように変更される予定です。つまり、ファイル名を返す際はオプションで Unicode 系のエンコーディングを指定すると、ANSI または OEM コードページに変換することなくそのまま Unicode で返します。また、ファイル名を渡す際も Unicode 系のエンコーディングで渡した場合は Unicode のままシステムに渡します。これにより、ロケールのエンコーディングでは表現できない文字を含むファイル名を取得したり、そうしたファイル名のファイルを扱えるようになるでしょう。

1.9.1 で廃止されたもの

$KCODE

Ruby 1.9 では $KCODE は廃止されました。$KCODE を見て処理を変更していた場合は、そのコードを変更する必要があります。 代替となりうるスクリプトの内部エンコーディングを与えるものとして、Ruby 1.9.1 からは Encoding.default_internal が提供されますが、デフォルトでは nil がセットされていること、Encoding.default_internal を設定すると IO においてエンコーディングの自動変換が行われることに注意してください。

レプリカエンコーディングとベースエンコーディング

1.9.0 に存在した「レプリカエンコーディング」や「ベースエンコーディング (Encoding#base_encoding)」という概念は Ruby 1.9.1 では Ruby 上の概念としては廃止されました。これらはもともと、同じバイト構造をもつエンコーディング間で実装を流用するために作られた「レプリカエンコーディング」という実装と概念を、文字集合の包含関係を定義するものとしても用いようとしたものでした。

しかし、エンコーディングのバイト構造と文字集合の関係は EUC-JP 系や Shift_JIS 系の場合でこそ使えるものの、他の世界のエンコーディングのことまで考慮すると別に定義しなければならないケースが多かったため、現時点では整備不可能と判断され、削除されることになりました。

なお、C API からはこれらの概念に今も触れることができます。

Command Line Option -K

コマンドラインオプション -K は Ruby 1.9 でも非推奨ながら健在です。Ruby 1.9 での -K は script encoding のデフォルト (-K が無ければ US-ASCII) と、Encoding.default_external に影響します。(Encoding.default_internal は nil のままになります)

なお、-K が script encoding に影響するのは、あくまで互換性確保のための処置で、1.8 のコードを修正なしに 1.9 でも動かしたい場合に利用するものです。1.8 でも 1.9 でも動くようにしたい場合や、1.9 用に新規に書く場合は magic comment を用いてください。

エンコーディングの変換

Ruby には旧来から Kconv とその実装である NKF、そして Iconv が文字コード変換ライブラリとして標準添付されてきました。しかし、これらのライブラリでは、サポートするエンコーディングに制限があったり、プラットフォームに依存したりといった欠点が存在していました。そこで、Ruby 1.9 では Martin さんによる transcode が組み込まれ、それを利用したメソッド String#encode やクラス Encoding::Converter が提供されています。

Ruby 1.9 のエンコーディング変換ライブラリは、String の内容のバイト列と、設定されているエンコーディング双方を変更します。設定されているエンコーディング情報のみを差し替える場合は、前述の String#force_encoding を用いてください。

transcode の実装に関する詳細は Martin さんによる RubyKaigi での発表 を、旧来の変換 API に関する情報は 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconvも参照してください。

String#encode

String#encode および String#encode! は、Ruby 1.9 における最も基本的なエンコーディング変換ための手段です。オプションをハッシュで与えることができ、変換元のエンコーディングにおいて不正なバイトがあった場合や (:invalid => nil | :replace) 、エンコーディングにおいて文字が定義されていない場合 (:undef => nil | :replace) の処理方法を指定したり、変換した文字列を XML 中に用いたい場合のエスケープの指定 (:xml => :text | :attr)、LF 改行への置換 (:universal_newline => true) などがあります。

 # coding: UTF-8
 u = "いろは"
 puts u.dump #=> "\u{3044}\u{308d}\u{306f}"
 p u.encoding #=> #<Encoding:UTF-8>

 e = u.encode("EUC-JP") # u.encode("EUC-JP", u.encoding) と同義
 puts e.dump #=> "\xA4\xA4\xA4\xED\xA4\xCF"

これ以上細かい指定を行いたい場合は、後述の Encoding::Converter#convert を用いましょう。

Encoding::Converter

Encoding::Converter は transcode の変換器をオブジェクトにした物で、String#encode より細やかな変換が可能となります。Encoding::Converter を用いて変換を行う場合は、Encoding::Converter#convert か、より細かな変換処理が可能な Encoding::Converter#primitive_convert が利用できます。

Encoding::Converter オブジェクトは Encoding::Converter.new に変換元・変換先のエンコーディングを与えたり、変換経路の配列を与えて生成します。Encoding::Converter.new では String#encode でのハッシュオプションに加えて、以下の定数が利用可能です。

  • Encoding::Converter::INVALID_REPLACE
  • Encoding::Converter::UNDEF_REPLACE
  • Encoding::Converter::UNDEF_HEX_CHARREF
  • Encoding::Converter::UNIVERSAL_NEWLINE_DECORATOR
  • Encoding::Converter::CRLF_NEWLINE_DECORATOR
  • Encoding::Converter::CR_NEWLINE_DECORATOR
  • Encoding::Converter::XML_TEXT_DECORATOR
  • Encoding::Converter::XML_ATTR_CONTENT_DECORATOR
  • Encoding::Converter::XML_ATTR_QUOTE_DECORATOR

Encoding::Converter#convert

Encoding::Converter#convert を用いると、文字列の一部を渡して変換を行うことができます。よって、不正なバイトを意識せずにストリームから読み出した文字列を変換したいときには Encoding::Converter#convert が適します。

 ec = Encoding::Converter.new("EUC-JP", "UTF-8")
 dst = ec.convert("あいうえお")

Encoding::Converter#convert の処理中に、不正なバイト列があった場合には Encoding::InvalidByteSequenceError が、未定義文字があった場合には Encoding::UndefinedConversionError が発生します。なお Encoding::Converter#convert では、これらの例外を捕獲しても、例外を起こしたところから変換を再開することはできません。不正なバイトや未定義文字をエスケープしたい場合やさらに細かい指定を行いたい場合は、後述の Encoding::Converter#primitive_convert を用います。

Encoding::Converter#primitive_convert

Encoding::Converter#primitive_convert はエンコーディング変換のためのメソッドの中で、もっとも緻密な扱いが可能なメソッドです。可搬性を確保しつつ、不正なバイトや変換先で未定義な文字の扱いを細かに指定したいときは、Encoding::Converter#primitive_convert が唯一の方法になります。

 ec = Encoding::Converter.new("UTF-8", "EUC-JP")
 src = "abc\x81あいう\u{20bb7}\xe3"
 dst = ''

 begin
   ret = ec.primitive_convert(src, dst)
   p [ret, src, dst, ec.primitive_errinfo]
   case ret
   when :invalid_byte_sequence
     ec.insert_output(ec.primitive_errinfo[3].dump[1..-2])
     redo
   when :undefined_conversion
     c = ec.primitive_errinfo[3].dup.force_encoding(ec.primitive_errinfo[1])
     ec.insert_output('\x{%X:%s}' % [c.ord, c.encoding])
     redo
   when :incomplete_input
     ec.insert_output(ec.primitive_errinfo[3].dump[1..-2])
   hen :finished
   end
   break
 end while nil

不正なバイトや変換先で未定義なバイトをエスケープしつつ変換する例です。以上のように、戻り値で分岐させつつ、Encoding::Converter#primitive_errinfo の情報を参照して処理していくことになります。

Kconv

変換結果に encoding を付加するようになった以外は基本的に 1.8 から変わっていません。バックエンドの NKF に対する変更は、基本的に Kconv には影響しないはずです。

NKF

古からある漢字コード変換ライブラリ nkf のラッパーです。Ruby 1.9.1 では nkf 2.0.9 相当のライブラリが標準添付されています。

Iconv

Unix 系プラットフォームで搭載されている文字コード変換ライブラリのラッパーです。環境によってサポートするエンコーディングや動作が異なるので、Ruby 1.9.1 では transcode を用いたメソッドを用いた方がよいでしょう。

実践編

ライブラリ

Ruby 1.9 でライブラリを書く場合、基本的に US-ASCII のみで書くのがよいでしょう。magic comment を書けば非 ASCII な文字列リテラルを埋め込むことも可能ですが、特定のエンコーディング・文字集合に依存することは避けるべきです。ひらがなカタカナ変換等、依存せざるをえないときは、引数のエンコーディングにあわせて変換を行い、引数の文字列を変換することは避けるのが無難です。

出力時のエンコーディングは、引数があれば引数のエンコーディングに、Encoding.default_internal が指定されていればそれに (String#encode が使えます)、さもなければ元の文字列のエンコーディングをいじらずに出力するのがよいでしょう。

ライブラリはよほど大きい物や、XML や YAML など、仕様上 Unicode 前提の処理になっている物でない限りは、CSI 方式で処理するべきです。

1.9 CSI

  • US-ASCII のみで書く
  • String は常に「文字列」として扱い、処理は常に文字単位で行う。
  • 文字を扱っている時にバイトやコードポイントをいじらない。

1.9 UCS

  • UCS をどれか一つ決める (UTF-8 でも EUC-JP でもなんでもよい)
  • Encoding.default_internal に そのエンコーディングをセットする
  • UCS 以外は UCS に変換して処理する。
  • 文字とバイトを区別するのは上に同じ。

1.8 互換 UCS

  • Ruby 1.8 は内部コードとして 1 つのエンコーディングしか利用することが出来ない。よって、基本的に UCS 方式を採用することになる
  • 自動変換を利用するとトラブルの元なので、Encoding.default_internal は nil。
  • 文字列操作メソッドは 1.9 のものを用いる、1.8.7 より前ならば自分で定義。
  • バイト列を操作する場合も 1.9 のものを用いる、1.8.7 より前ならば自分で定義。

1.8 互換 CSI

正規表現エンジンが SJIS, EUC, UTF8 しか対応していないので、なかなか難しい。

変更履歴

2009 年 2 月 12 日

  • Ruby 1.9.1 の UCS-2 に対する態度を追記
  • その他 Unicode について追記
  • ファイルシステムエンコーディングについて追記
  • ロケールエンコーディング等についてうささんの意見を反映
  • Emacs 用の magic comment の例を小文字に修正 (mamamoto さんの指摘)

2009 年 2 月 8 日

  • 目次を追加
  • BOM 付きの UTF-16 と UTF-32 に対する言及を追加
  • ロケールエンコーディングとファイルシステムエンコーディングについて追記
  • Encoding 定数についての言及を追加
  • レプリカエンコーディングについて追記
  • String#inspect の文言修正と、Kernel#p に対する言及を追加
  • IO#binread への言及を追加
  • その他細かな文言修正

著者について

成瀬。nkf とかやってます。


*1 つまり、UCS-2 を用いていた

*2 明らかになった正確な時期は調べきれなかったが、N833 Proposal for Extended UCS-2Concerning Future Allocations を見るに、ISO/IEC 10646-1: 1993 制定時点ですでに明らかだったようだ

*3 実際にBMP 外の追加多言語面 (Supplementary Multilingual Plane, SMP) と 追加漢字面 (Supplementary Ideographic Plane, SIP) への収録が開始されたのはUnicode 3.1 から

*4 chcp に追従して欲しいため。また、GetConsoleOutputCP ではない。なお、将来のバージョンで変更されうる

*5 コンソールへの出力でかつ、出力する文字列が Unicode 系エンコーディングの場合に Unicode 版 API を用いるといった実験は win32-unicode-test ブランチ で行われている

*6 C API は rb_filesystem_encoding()

*7 正確には今もなお UCS-2 らしい。つまり、サロゲートペアの片方のみの場合がある

*8 AreFileApisANSI() が真なら ANSI、偽なら OEM

*9 Byte Order Mark。ビッグエンディアンとリトルエンディアンの判定をするための符号。文字列の先頭に付加される U+FEFF。