やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「多言語化」を読む

 多言語化というか文字コードの話だった。

成果物

情報源

言語化

Ruby は US-ASCII はもちろん、US-ASCII 以外の文字エンコーディングもサポートしています。文字列の内部表現のエンコーディングは固定されておらず、プログラマは目的に応じて使用するエンコーディングを選ぶことができます。

 UCS正規化方式でなくCSI方式である。

同じプロセスの中で異なるエンコーディングの文字列が同時に存在することができます。全ての String や Regexp などのオブジェクトは自身のエンコーディング情報を保持しています。これにより各オブジェクト内の文字を適切に取り扱うことができます。

 最強。

後述のマジックコメントでスクリプトエンコーディングを指定すると、 Ruby スクリプトに非 ASCII 文字を使うことができます。(magic comment) 文字列リテラル正規表現リテラルだけでなく変数名、メソッド名、クラス名などにも非 ASCII 文字を使うことができます。ただし文字列リテラル正規表現リテラル以外での非 ASCII 文字の使用は推奨されません。

 非推奨なんだったら意味ないじゃん。まあ名前の制約とも相性悪いしね。

グローバル変数 $KCODE は廃止されました。

 知らんし。

M17N プログラミングの基本

プログラマは文字列を扱うときエンコーディングを常に意識しなければいけません。オブジェクトが生成される段階で、適切なエンコーディング情報を持つよう心がけるべきです。文字列が生成されるのは主に「リテラルから」「IOから」「文字列操作から」の 3 通りです。このうち文字列操作に関しては通常 Ruby 実行系が適切に処理しますから、プログラマは 「リテラルから生成」「IO から生成」 の二通りに関して注意する必要があります。 IO から生成される文字列のエンコーディングに関しては IO/多言語化と IO のエンコーディング を参照してください。

 意識したくないなー。もうユニコードだけでいいよ。それでもたくさんあるけど。

エンコーディングの変更

文字列のエンコーディングを変更するには、次の2つのメソッドを用います。

String#encode メソッドは文字列のエンコーディングを変換した新しい文字列を生成して返します。 Ruby 1.9 ではこれを用いてエンコーディングを変換するのが標準的なやり方です。

String#force_encoding メソッドは文字列のエンコーディング情報を破壊的に書き換えます。新しい文字列は生成されません。例えばエンコーディングが不明のテキストファイルから読み込んだ文字列に後からエンコーディングを設定する場合などに使います。

例:

"いろは".encode("Shift_JIS")
"\xA4\xA4\xA4\xED\xA4\xCF".force_encoding("EUC-JP")

Ruby 1.8 からの移行

Ruby 1.8 からの移行措置として、コマンドオプション -K を指定すれば Ruby 1.8 用に書かれたスクリプトもできる限り動くような仕様になっています。ただし Ruby 1.9 には多言語化以外にも多くの非互換性が存在するので必ずしも動くとは限りません。

 そんな古いのは知りません。

いずれにしても、これからは Ruby 1.9 への移行を考慮してスクリプトに非 ASCII 文字を使う場合、マジックコメントでスクリプトエンコーディングを指定しておくのがよいでしょう。magic comment を参照してください。

 マジコメしろと? たしかUTF-8がデフォルトだったよね? 書かなくていいよね?

例:

スクリプトを EUC-JP で書いていて、扱うテキストも EUC-JP の場合。 => コマンドオプションに -Ke を指定。

Ruby がサポートするエンコーディング

Rubyエンコーディングのサポート水準はエンコーディングの種類によって異なります。

ASCII互換エンコーディング

フルサポートです。UTF-8, EUC-JP, Shift_JIS などがこれにあたります。

 まあだいたいこれでしょ。

ASCII互換ではないエンコーディング

スクリプトエンコーディングに使えません。またエンコーディングが固定されていない正規表現がマッチングを行うと例外が発生します。UTF-16LE, UTF-16BE などがこれにあたります。

 たぶん使わないよね?

ダミーエンコーディング

文字の列としての処理をサポートしません。Rubyエンコーディングの名前だけ知っている状態です。 ISO-2022-JP, UTF-7 がこれにあたります。

 たぶん使わないよね?

サポートするエンコーディングのリストは Encoding.list, Encoding.name_list で取得することができます。また拡張ライブラリを作成することによりサポートするエンコーディングを動的に増やすことができます。

p Encoding.list

それぞれの用語の定義は以下を参照してください。

ASCII互換エンコーディング

「ASCII互換エンコーディング」とは、 US-ASCII に含まれる文字を \x00-\x7F で表し、ロッキングシフトを用いないエンコーディングです。日本語の文字を表現するエンコーディングでは、 Shift_JIS やcp932などのその変種、EUC-JPとその変種、UTF-8などが ASCII 互換エンコーディングです。 UTF-16ISO-2022-JP などが ASCII 互換でないエンコーディングの代表例です。

 ロッキングシフトってなんじゃい。

ASCII互換エンコーディングである文字列や正規表現は、7bit クリーンな文字列や正規表現と、結合・比較・マッチ等を行うことができます。

 7bitクリーンってなんじゃい。8bitダーティーもあるんか?

7bit クリーンな文字列

ASCII 互換エンコーディングをもつ 7bit クリーンな文字列は、他の ASCII 互換エンコーディングを持つ文字列と結合・比較が可能です。例えば、ASCII 互換エンコーディングをもつ文字列に、 7bit クリーンな文字列をエンコーディングの変換なしで結合することができます。また、通常 Ruby の文字列比較メソッドである String#== は、 2 つの文字列のエンコーディングが異なっていると、バイト列としては一致していても false を返します。しかし、7bit クリーンな文字列同士の比較の際は、両者の文字エンコーディングが異なっていても、バイト列として一致していれば true を返します。

例:

a = "abc"
e = a.encode("EUC-JP")
u = a.encode("UTF-8")
p e == u                           #=> true
p e + u                            #=> "abcabc"
p "" + e                         #=> "あabc"
p "" + u                         #=> "あabc"

 そうですか。そのほうが都合がいいのだろうね。エンコーディングが異なっていても、バイト列として一致していれば true を返したほうが都合がいいのだろう。たぶん。

バイナリの取扱い

Ruby の String は、文字の列を扱うためだけでなく、バイトの列を扱うためにも使われます。しかし、Ruby M17N には直接にバイナリを表すエンコーディングは存在しません。このため、バイナリを String で扱う際には、ASCII 互換オクテット列を意味する ASCII-8BIT を用います。これにより、ASCII 互換であるこの String は 7bit クリーンな文字列と比較・結合が可能となります。

 さっぱりわからん。

ダミーエンコーディング

@todo

 え。

ダミーエンコーディングとは Ruby が名前を知っているが、文字の列としての処理に対応していないエンコーディングのことです。実際には ISO-2022-JPUTF-7 のようなステートフルエンコーディングがダミーエンコーディングになります。ダミーであるかどうかは Encoding#dummy? を使って識別できます。ダミーエンコーディングを持つ文字列の扱いは以下のように制限されます。

  • String のインスタンスメソッドは 1 文字ではなく 1 バイトを単位として動作します。
  • エンコーディングの異なる 7bit クリーンな文字列との結合ができません。 例外 (Encoding::CompatibilityError) が発生します。
p Encoding::ISO_2022_JP.dummy? # => true
s = "漢字".encode("ISO-2022-JP")
p s[0]   #=> "\e"
s + "b"  #=> Encoding::CompatibilityError: incompatible character encodings: ISO-2022-JP and UTF-8
p Encoding::ISO_2022_JP.dummy? # => true
s = "漢字".encode("ISO-2022-JP")
p s[0]   #=> "\e"
s + "b"  #=> Encoding::CompatibilityError: incompatible character encodings: ISO-2022-JP and UTF-8

 文字数カウントが正しくできないのは困る。バイト数で返されてもねぇ。

またダミーエンコーディングスクリプトエンコーディングとして使うことができません。

スクリプトエンコーディング

スクリプトエンコーディングとは Ruby スクリプトを書くのに使われているエンコーディングです。スクリプトエンコーディングは マジックコメントを用いて指定します。スクリプトエンコーディングには ASCII 互換エンコーディングを用いることができます。 ASCII 非互換のエンコーディングや、ダミーエンコーディングは用いることができません。

現在のスクリプトエンコーディングENCODING により取得することができます。

例:

# coding: euc-jp
p __ENCODING__     #=> #<Encoding:EUC-JP>

 マジコメせずにやったらUTF-8だった。

p __ENCODING__ #<Encoding:UTF-8>

 OSによって変わるとかないよね?

magic comment

マジックコメントを使うことにより Ruby 実行系にスクリプトエンコーディングを伝えることができます。マジックコメントとはスクリプトファイルの1行目に書かれた

# coding: euc-jp

という形式のコメントのことです。1 行目が shebang である場合、マジックコメントは 2 行目に書くことができます(それ以降の行ではいけません。無視されます)。上の形式以外にも

# encoding: euc-jp
# -*- coding: euc-jp -*-
# vim:set fileencoding=euc-jp:

などの形式を解釈します。

#!/bin/sh
exec ruby19 -x "$0" "$@"
#!ruby
# coding: utf-8

このように -x オプションを使っている場合には「#! で始まり、ruby がある行」の次の行に書きます。

 長すぎる。

マジックコメントによりスクリプトファイル毎にスクリプトエンコーディングを指定することができます。

 指定したくない! 自動でやってくれ。

マジックコメントが指定されなかった場合、コマンド引数 -K, RUBYOPT およびファイルの shebang からスクリプトエンコーディングは以下のように決定されます。左が優先です。

magic comment(最優先) > -K > RUBYOPTの-K > shebang

上のどれもが指定されていない場合、通常のスクリプトなら UTF-8、-e や stdin から実行されたものなら locale がスクリプトエンコーディングになります。 -K オプションが複数指定されていた場合は、後のものが優先されます。

 ふつうはUTF-8ってことでいいよね。よかった。

1.8 からのスクリプトエンコーディングに関する非互換性

  • スクリプトリテラル中に非 ASCII 文字が含まれている場合、 1.8 では -K オプションなしで正常に動作していたとしても、1.9 では必ずパース時に エラーになります。 -K オプションがない場合、1.8 では 1.9 の ASCII-8BIT 相当の挙動でしたが、1.9 では US-ASCII として扱われるためです。
  • マジックコメントがあった場合、1.8 では無視されますが、1.9 ではそれ がスクリプトエンコーディングに反映されます。これは -K オプションよりも優先されます。
  • -K オプション・RUBYOPT・shebang の間の優先順位が 1.8 と 1.9 では違います。 それぞれの優先順位は以下の通りです。左が優先です。
Ruby 1.8 : shebang > RUBYOPTの-K > -K
Ruby 1.9 : -K      > RUBYOPTの-K > shebang

 そんな古いのは考えなくていい! さよなら後方互換

リテラルエンコーディング

文字列リテラル正規表現リテラルそしてシンボルリテラルから生成されるオブジェクトのエンコーディングスクリプトエンコーディングになります。

 ソースコードに書いたリテラルスクリプトエンコーディングになる。ってことでいいよね?

またスクリプトエンコーディングが US-ASCII である場合、7bit クリーンではないバックスラッシュ記法で表記されたリテラルエンコーディングは ASCII-8BIT になります。

 はあ。

さらに Unicode エスケープ (\uXXXX) を含む場合、リテラルエンコーディングUTF-8 になります。

例:

# coding: us-ascii
p __ENCODING__        #=> #<Encoding:US-ASCII>
p "abc".encoding      #=> #<Encoding:US-ASCII>
p "\x80".encoding     #=> #<Encoding:ASCII-8BIT>


# coding: euc-jp
p __ENCODING__        #=> #<Encoding:EUC-JP>
p "abc".encoding      #=> #<Encoding:EUC-JP>
p "\x80".encoding     #=> #<Encoding:EUC-JP>
p "\u3042".encoding   #=> #<Encoding:UTF-8>  (Unicode エスケープがあるので UTF-8 になる)
p "\x80\u3042".encoding #=> エラー

 UTF-8が基本っぽい感じ。ですよね。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux