daily dayflower

2008-06-25

perluniadvice

perluniadvice の前半部分を訳してみました。かなり意訳。

アドバイスと銘うってますが,中級者〜上級者向きです*1

抄訳 perluniadvice

Perl 付属のドキュメント perlunitut はもう読んだ?もしまだなら,そこから読み始めること :)

以下にわたし(http://juerd.nl/)からのアドバイスの要約を挙げる。perlunitut には記述しなかった内容も含んでいる。

  • もしあなたが古い Perl を使っているのなら,最新版にアップグレードしよう
  • もしあなたが古い CPAN モジュールを使っているのなら,最新版にアップグレードしよう
  • 「外界」からやってくるデータはすべて decode() しよう
  • 「外界」へ出力するデータはすべて encode() しよう
  • 「テキスト文字列」と「バイト列」をあなたのプログラムの中で完全に分離しよう
  • ある「string」が「テキスト文字列」なのか「バイト列」なのか Perl は判断できない,ということを認識しよう
  • Unicode 文字列」とは「Unicode文字列であって「UTF-8文字列ではない,ということを認識しよう(!!)
  • UTF8 flag なんて存在しないかのように振る舞え
  • UTF8 flag を明示的にいじるな(on したり off したり)
  • UTF8 flag の状態を取得するな(Encode::is_utf8(), utf8::is_utf8()
  • 「string」に UTF8 flag が立っている場合にそれが「テキスト文字列」であるとは言えるけれど,逆は必ずしも成り立たないことを認識しよう
  • もしあなたが「string」を utf8::downgrade() しなきゃいけないとしたら,それはたいていそれ以前の処理が間違っている
  • ソースコードUTF-8 エンコーディングで書かれているときだけ use utf8; を使え
  • use bytes; を使うな
  • bytes:: ネームスペースの関数を使うな
  • 正規表現\C を使うな
  • use encoding; を使うな
  • pack() で「バイト列」を生成する場合,「C*テンプレートを使え
  • 「テキスト文字列」を unpack() するとき「Uテンプレート以外を使うな
  • 「バイト列」を unpack() するとき「Uテンプレートを使うな
  • pack() / unpack() で「Uテンプレートと「非 Uテンプレートを混ぜて使うな
  • pack() で「U0」や「C0テンプレートを使うな
  • lc() / lcfirst() / uc() / ucfirst() する前に utf8::upgrade() しよう
  • 大文字小文字を区別しないマッチングをする前に utf8::upgrade() しよう
  • \w\s などのキャラクタクラスをマッチングで使う場合,その前に utf8::upgrade() しよう

内部のごちゃごちゃ(UTF8 flag など)を直接さわらないと解決できないことがあるかもしれない。PerlUnicode サポートと相性が悪いモジュールを使ったり,実装が間違っているモジュールを使ったりしなきゃいけない時とか。ということで,以上にあげた項目は大まかな原則である。


以下,各項目について補足です。

最新版にアップグレードしよう

いわずもがな。Unicodeネイティブに取り扱いたいのなら Perl 5.6 以前はおすすめできません。

ちなみにそういう意味では,RHEL 5 や Ubuntu 8.04 LTS で標準的に含まれている Perl 5.8.8 は充分にこなれていると(個人的には)思います。

perldelta によると Perl 5.10 の Unicode にまつわる advantage は

こんなところみたいです。ほかバンドルされてるモジュールが新しくなってたりするので変更点があるかもですが,これらだけが違うのなら,まぁ Perl 5.8.8 でもそれなりに使えますよね。

「テキスト文字列」だの「バイト列」だの,分離する,だの

先日書きました*2。んが,string についてまた新しく図版をおこしてみました。

f:id:dayflower:20080625134349p:image:w480

こないだは「Unicode 文字列」とか「byte stream」とかいってたのに,「テキスト文字列」だの「バイナリ文字列」だの「バイト列」だの,なんなんだよーと思われるかもしれません(すみません*3)。これは用語を perlunitut にあわせた,というのと,こないだのような単純な「二項対立」ではお話をすすめられないので,あえて用語をかえた,というのがあります。

Encode モジュールで変換しているのに,両者の違いが「Perl にとって判別不能」というのが「そんなバカな」と思われるかもしれませんが,実際そういうインプリメントになっています(下記参照)。

UTF8 flag について*4

ほんとうは UTF8 flag についてあまり書きたくないのですが*5,「存在しないかのように振る舞え」というお達しであり「存在そのもの」を知ることを禁止されているわけではなさそうなので,簡単に説明します。

実は Perl の「テキスト文字列」には下記の2通りの内部表象があります。

f:id:dayflower:20080625134418p:image:w400

単純に "ABC" などの文字列リテラルを表記すると,通常は「内部 Latin-1 文字列」となります。いっぽう,

  • "\x{2665}", chr(9829) など U+0100 以上の Unicode Character が文字列リテラルに含まれる場合
  • ソースコードuse utf8; で書いてあり,文字列内に U+0100 以上の文字が含まれる場合)
  • Encode::decode() によって some encoding byte stream を「テキスト文字列」に decode() した場合
  • UTF8 flag on の文字列と結合した場合

には「内部 UTF-8 文字列」となります。この文字列は UTF8 flag という内部フラグが on になっています。

UTF8 flag の状態は utf8::is_utf8() 関数などによって調べることができます。

# * UTF-8 でソースコードを記述しているとします *

use strict;
use warnings;
use Encode;

sub utf8_flag_of { utf8::is_utf8(shift) ? 'UTF8 on' : 'UTF8 off' }

print utf8_flag_of( "ABC" ), "\n";
# >>> "UTF8 off"

print utf8_flag_of( "ABC\x{2665}" ), "\n";
# >>> "UTF8 on"

print utf8_flag_of( "ABC" . chr(2665) ), "\n";
# >>> "UTF8 on"

no utf8;
print utf8_flag_of( "日本語" ), "\n";
# >>> "UTF8 off"
# あくまで UTF-8 encoding byte stream なので UTF8 flag off

print utf8_flag_of( decode('UTF-8', "日本語") ), "\n";
# >>> "UTF8 on"

use utf8;
print utf8_flag_of( "日本語" ), "\n";
# >>> "UTF8 on"
# use utf8; すると UTF8 on な文字列とみなしてくれる

perluniadvice のマナーをまもってプログラミングしていれば,みずからが utf8::is_utf8() などの関数を使う必要はないはずです。

で,上記のプログラムを読めばわかると思いますが,

は,同じように表記して同じように格納されてますよね。こういうこともあって,「Perl にはテキスト文字列かバイト列か判別不能」なのです。「テキスト文字列」や「バイト列」は,あくまで,あなたの「意図」に基づく状態(意味論)なのです。だから「意図」をもって「分離」することが大事なんですね。

UTF8 flag については,以前もっと詳しい話を書きました*6ので興味があればどうぞ。

ソースコードUTF-8 エンコーディングで書かれているときだけ use utf8;

utf8::* 系の関数use utf8; をしなくても使えます。(おそらく)欧米人はソースコードISO-8859-1 で書く習性があると思うんですが,そのような場合に use utf8; してしまうと文字列リテラルが誤読されてしまうので無闇に use utf8; を使うな,という意味だと思います。日本人には(さいわい)縁のない話。日本人が書く場合,マルチバイト文字列を含むコードは UTF-8 で書くでしょうから,文字列リテラルUnicode 文字列として認識してくれる use utf8; は福音。

Do not use this pragma for anything else than telling Perl that your script is written in UTF-8. The utility functions described below are directly usable without use utf8;.

Because it is not possible to reliably tell UTF-8 from native 8 bit encodings, you need either a Byte Order Mark at the beginning of your source code, or use utf8; , to instruct perl.

utf8 - perldoc.perl.org

なななんと BOM つけると use utf8; と等価になるんですか!と思って Perl 5.8.8 で試したけどだめでした*7Perl 5.10 でどなたか試してくれるとうれしいです。perldelta には書いてない feature?

use bytes;

use bytes; すると(そのスコープでは)length() 等の文字列操作関数が,バイト単位になります。たとえば漢字を与えると length() が3を返します(UTF-8 encoding では3バイトになるので)。詳しくは bytes - perldoc.perl.org を参照してください。

これは内部エンコーディングUTF-8 であることを連想させる悪癖。かつ,内部 Latin-1 文字列の場合,1文字=1バイトのままになります(UTF-8 だと U+0080U+00FFの範囲で2バイトのはず)。などなど,状態によって挙動が変わり直感的でないのでよろしくないです。

ということで,これ系はすべて使うな,と。

use encoding; を使うな

これ残念ながら理由がわかりませんでした*8。誰か教えて。不完全なところがあるからかな。それとも use utf8; で書くべきところを use encoding 'utf8'; って書く癖がついちゃうからかな。

2008-06-27 追記:

2008年06月26日 id:vkgtaro perl, unicode 「use encoding; を使うな」は、レキシカルじゃないとか、そもそも下位互換のために用意したとかって話を YAPC 2006 で dan さんが言ってた気がする。今はスコープ単位で動くんだっけ?

http://b.hatena.ne.jp/vkgtaro/20080626#bookmark-9074627

おー。情報ありがとうございます。たしかにしくみ上レキシカルじゃなさそう。ちら見したところ,すくなくとも PerlIO の部分はレキシカルにならないと思います。エンコーディング変換部分は Filter::Util::Call ベースっぽいのでひょっとするとレキシカルに動くかも。うーん検証しなきゃですね。

utf8::upgrade() について

さきに書いたとおり Perl の「テキスト文字列」には下記の2通りの内部表象があります。

utf8::upgrade() という関数

内部 Latin-1 encoding 文字列を内部 UTF-8 encoding 文字列に変換する

ものです。

f:id:dayflower:20080625134431p:image:w360

  1. 「内部 Latin-1 文字列」を与えると,「内部 UTF-8 文字列」に変換してくれます
  2. もともと「内部 UTF-8 文字列」なものは,そのままにしておいてくれます
  3. UTF-8 encoding バイナリ文字列(バイト列)」を与えると,おかしなことになります(1番と比較すると理由はわかると思います)

さて uc() などの前に utf8::upgrade() しなくてはいけない理由とは。サンプルコードで見てみます。

use strict;
use warnings;

binmode \*STDOUT, ':utf8';      # わたしの terminal は UTF-8 なので

# \x{e9} means LATIN SMALL LETTER E WITH ACUTE
my $str = "H\x{e9}llo";

print $str, "\n";
print uc($str), "\n";

というコードを素直に実行すると,

% perl uc.pl

Héllo
HéLLO

のようになります。「é」が「É」になってない!Latin-1 (ISO-8859-1) や Unicode Character 的には「é」の大文字は「É」であってほしいですよね。

実は UTF8 flag off なテキスト文字列の場合,デフォルトでは US-ASCII の範囲*9のみ「大文字」「小文字」「アルファベット文字」などの「文字種別判別」が行われます*10

一つの抜け道としては,use locale; プラグマを用いてロケールに基づく「文字種別判別」を行ってもらうことです。

uc EXPR
uc

Returns an uppercased version of EXPR. This is the internal function implementing the "\U" escape in double-quoted strings. Respects current LC_CTYPE locale if "use locale" in force. See perllocale and perlunicode for more details about locale and Unicode support. It does not attempt to do titlecase mapping on initial letters. See "ucfirst" for that.

If EXPR is omitted, uses $_.

uc - perldoc.perl.org

とありますので,システムロケールを *.ISO-8859-1 にしつつ use locale; して実行してみましょう。以下の実行は CentOS 5.1 で行いました*11

% LANG=en_US.ISO-8859-1 perl -Mlocale uc.pl

Héllo
HÉLLO

おー。「É」になりました。

もう一つの(より推奨される)方法は,utf8::upgrade() を用いて内部 UTF8 flag on な文字列に変換することです。このときは,「文字種別判断」は Unicode Character に基づきます。

use strict;
use warnings;

binmode \*STDOUT, ':utf8';

my $str = "H\x{e9}llo";

# utf8::upgrade は in-place upgrade
# また上で述べたように use utf8; する必要がないです
utf8::upgrade($str);

print $str, "\n";
print uc($str), "\n";

これを実行すると,

% perl upgrade.pl

Héllo
HÉLLO

やはり「É」が表示されるようになりました。

実装を浅くみてみます。uc() ではなく「文字種別判断」の部分ですが,

/******* snip snip snip *******/

/* デフォルト実装 */

#define isALPHA(c)      (isUPPER(c) || isLOWER(c))
                /* snip */
#   define isUPPER(c)   ((c) >= 'A' && (c) <= 'Z')
#   define isLOWER(c)   ((c) >= 'a' && (c) <= 'z')

/******* snip snip snip *******/

/* setlocale() サポートアーキテクチャでの実装 */

#    define isALPHA_LC(c)       isalpha((unsigned char)(c))
#    define isSPACE_LC(c)       isspace((unsigned char)(c))
#    define isDIGIT_LC(c)       isdigit((unsigned char)(c))
#    define isUPPER_LC(c)       isupper((unsigned char)(c))
#    define isLOWER_LC(c)       islower((unsigned char)(c))

/******* snip snip snip *******/

/* Unicode Character 用実装 */

#define isALPHA_uni(c)          is_uni_alpha(c)
#define isSPACE_uni(c)          is_uni_space(c)
#define isDIGIT_uni(c)          is_uni_digit(c)
#define isUPPER_uni(c)          is_uni_upper(c)
#define isLOWER_uni(c)          is_uni_lower(c)

/******* snip snip snip *******/

#define isALPHA_utf8(p)         is_utf8_alpha(p)
#define isSPACE_utf8(p)         is_utf8_space(p)
#define isDIGIT_utf8(p)         is_utf8_digit(p)
#define isUPPER_utf8(p)         is_utf8_upper(p)
#define isLOWER_utf8(p)         is_utf8_lower(p)

/******* snip snip snip *******/
handy.h

ロケール無視バージョンの isUPPER() 等は,ハードコーディングされています。このために US-ASCII の範囲のみ upper case になっていたんですね。今回は uc() の挙動のみたしかめましたが,「文字種別判断」にまつわる機能は上記の項目のように lc()\w マッチングなどもあります。


さて,utf8::downgrade() という反対の挙動をする関数がありますが,これは

(○)内部 UTF-8 encoding 文字列を内部 Latin-1 encoding 文字列に変換する

ものであり,

(×)内部 UTF-8 encoding 文字列UTF-8 encoding バイト列に変換する

ではないことに注意が必要です。U+0100 以上の文字を含む文字列を変換しようとすると怒られます。上のアドバイスにもあるとおり,utf8::downgrade() は普通使う必要がないし,もし使ってうまくいくのであればそこ以前のコーディングが間違っている可能性が大です。

内部 Latin-1 文字列と内部 UTF-8 文字列が混在する可能性

あなたが「外界」からの入力を必ず decode() していれば,内部 UTF-8 文字列になるので基本的には影響しません。ですが,CPAN モジュールを使ったり,素直なコーディングをしていても,ごちゃごちゃになる可能性はあります。

utf8::is_utf8 considered harmful - Bulknews::Subtech - subtech と同様 HTML::Entities を使った例ですが,

use strict;
use warnings;
use HTML::Entities;

binmode \*STDOUT, ':utf8';

my $s1 = decode_entities("H&eacute;llo");
print $s1, "\n";
print utf8::is_utf8($s1) ? 'UTF8 on' : 'UTF8 off', "\n";

my $s2 = decode_entities("H&eacute;llo&hearts;");
print $s2, "\n";
print utf8::is_utf8($s2) ? 'UTF8 on' : 'UTF8 off', "\n";

この実行結果は,

Héllo
UTF8 off

Héllo♥
UTF8 on

のようになります。同じ関数を同じように呼んだだけですが,$s1 は Latin-1(U+0000U+00FF)の範囲の文字で構成されるため全体が内部 Latin-1 文字列(UTF8 off)となり,$s2 は内部 UTF-8 文字列(UTF8 on)となりました。

HTML::Entities が悪いとは一概にはいえません。下記のような単純なコーディング*12でも,

use strict;
use warnings;

binmode \*STDOUT, ':utf8';

my $s1 = chr(0x0048) . chr(0x00e9) . chr(0x0079);
print $s1, "\n";
print utf8::is_utf8($s1) ? 'UTF8 on' : 'UTF8 off', "\n";
# >>> "UTF8 off"

my $s2 = chr(0x0048) . chr(0x00e9) . chr(0x0079) . chr(0x2665);
print $s2, "\n";
print utf8::is_utf8($s2) ? 'UTF8 on' : 'UTF8 off', "\n";
# >>> "UTF8 on"

前者は内部 Latin-1 文字列,後者は内部 UTF-8 文字列となります。

ここコーディング指針を決めるうえでなかなか悩むところです。

  • 外部モジュールからの戻り値utf8::upgrade() をかませる
  • uc() 等呼び出す前だけ utf8::upgrade() をかませる

の2者の指針があると思います。前者のほうが(decode() 等と)統一感を保てていい気がするんですが,上記のように実直にコーディングしていても混在する可能性もありますし,となると後者かなぁとか。文字列を結合すると UTF8 flag に(言葉は悪いですが)「汚染」されていくのであんまり深く考える必要はないかもしれませんが。

pack(), unpack()

テキスト処理がからむプログラムで使うことはまずないし,個人的にもあまり使わない関数なので省略しようと思いましたが,一応サンプルコードだけおいておきますね。

use strict;
use warnings;

sub say    { print join q{}, @_, "\n" }
sub a2hex4 { join q{ }, map { sprintf '%04X', $_ } @_ }
sub a2hex2 { join q{ }, map { sprintf '%02x', $_ } @_ }
sub str2a  { map { ord } split q{}, shift }
sub utf8_flag_of { utf8::is_utf8(shift) ? 'UTF8 on' : 'UTF8 off' }

# "日本語" as Unicode String
my $uStr = "\x{65E5}\x{672C}\x{8A9E}";
# "日本語" in UTF-8 as byte stream
my $bStr = join q{}, map { chr hex } qw( e6 97 a5 e6 9c ac e8 aa 9e );

# Unicode Character Array
my @uaCh = str2a( $uStr );
# UTF-8 Byte Array
my @baCh = str2a( $bStr );


say a2hex4( unpack('U*', $uStr) );
# >>> "65E5 672C 8A9E"
# Unicode 文字列の Character Array 化

say a2hex2( unpack('C*', $bStr) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# バイト列の Byte Array 化

say a2hex4( str2a( pack('U*', @uaCh) ) );
# >>> "65E5 672C 8A9E"
# (Character Array の Unicode 文字列化)

say a2hex2( str2a( pack('C*', @baCh) ) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# (Byte Array のバイト列化)


######## 以下非推奨機能

say a2hex2( unpack('C*', $uStr) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# (Unicode 文字列が UTF-8 byte stream に自動変換して出力される)
# (素直に自力で Encode::encode('UTF-8', $uStr) してから使いましょうね)

say a2hex4( unpack('U*', $bStr) );
# >>> "00E6 0097 00A5 00E6 009C 00AC 00E8 00AA 009E"
# (UTF-8 byte stream の各文字が Latin-1 byte stream としてみなされてしまった)
# (素直に自力で Encode::decode('UTF-8', $bStr) してから使いましょうね)

say a2hex4( unpack('U*', "H\x{e9}llo") );
# >>> "0048 00E9 006C 006C 006F"
# (いっぽうこちらはわかっていて使うなら問題ない)
# (Latin-1 文字列の各 Unicode Character を取得している)
# (コードとしては上と同じだが,まさに「意味論」的問題です)


say utf8_flag_of( pack('C*', @baCh) );
# >>> "UTF8 off"
# (Byte Array→バイト列,なので当然 UTF8 flag off だが……)

say a2hex4( str2a( pack('U0C*', @baCh) ) );
# >>> "65E5 672C 8A9E"
say utf8_flag_of( pack('U0C*', @baCh) );
# >>> "UTF8 on"
# (UTF-8 文字列に自動変換され Unicode 文字列となり UTF8 flag on となる)


say utf8_flag_of( pack('U*', @uaCh) );
# >>> "UTF8 on"
# (Character Array→Unicode 文字列,で UTF8 flag on だが……)

say a2hex2( str2a( pack('C0U*', @uaCh) ) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
say utf8_flag_of( pack('C0U*', @uaCh) );
# >>> "UTF8 off"
# (UTF-8 byte stream に自動変換され UTF8 flag off となる)

# (こいつらも素直に自力で事後に encode() / decode() しましょうね)

str2a() とか車輪の再発明もいいところですが*13,意味合いをはっきりさせるためにもあえて再発明してます。

後半の U0C*, C0U* については注釈が必要かも。

pack()テンプレートU を使うと,結果は UTF8 flag 付き文字列になります。このことを利用して,あたまに U0 をもってくると,UTF8 flag 付き文字列返すモードになります。そのあと C* で byte array を食わせてやれば,UTF-8 byte stream array を UTF8 flag 付き Unicode 文字列に変換できる,ということです。なかば裏技的用法ですな。

If the pattern begins with a "U", the resulting string will be treated as UTF-8-encoded Unicode. You can force UTF-8 encoding on in a string with an initial "U0", and the bytes that follow will be interpreted as Unicode characters. If you don’t want this to happen, you can begin your pattern with "C0" (or anything else) to force Perl not to UTF-8 encode your string, and then follow this with a "U*" somewhere in your pattern.

pack - perldoc.perl.org

なお Perl 5.10 では,unpack() における U0C0 テンプレートが,出力結果ではなく入力結果オリエンテッドになっています。これは Perl 5.8 のときと,挙動が逆になっています。

To be consistent with pack(), the C0 in unpack() templates indicates that the data is to be processed in character mode, i.e. character by character; on the contrary, U0 in unpack() indicates UTF-8 mode, where the packed string is processed in its UTF-8-encoded Unicode form on a byte by byte basis. This is reversed with regard to perl 5.8.X, but now consistent between pack() and unpack().

perldelta - perldoc.perl.org

なんにせよ,これらの feature は現在では非推奨ということで。

つーか,pack()unpack()テンプレートC だけ使い,そこからの「入出力」に Encode::decode(), Encode::encode() を使うようにすればいいんじゃないかと思います。あと文字列を文字ごとに分解して文字コード群を取得したい場合には unpack('U*') は便利かな。

*1:(わたしが)用語定義もしてないし同義語をごちゃごちゃまぜこんだり(Latin-1 と ISO-8859-1 とか)してますしね。ごめんなさい。

*2 図解: Perl と Unicode 文字列 - daily dayflower

*3:こないだの記事は図版だけは気合いれて描きましたが,読み返してみると,文章が納得いくクオリティじゃないですね。

*4:ここは特に文章練れてないです。理解できなくても,おそらく文章のせいです。悲しまないで。

*5:ボロが出そうだし,一応この機能を意識しないでプログラミングしましょうということになってるし。

*6 UTF8 フラグあれこれ - daily dayflower Perl (5.8) での文字列の内部表象について返信 - daily dayflower

*7:自信がないので追試希望。

*8:素敵な feature だと思うんですが。わたしは使わないけど。

*9U+0000U+007F

*10:たぶん後方互換性のため。

*11Ubuntu 8.04 にはデフォルトで en_US.ISO-8859-1 などの ISO-8859-1 エンコーディングロケールインストールされていませんでしたので CentOS 5.1 を使いました。

*12:もっと単純には "H\x{e9}y\x{2665}" でいいんですが,なんというかロジックを介在させて「らしく」見せました。

*13:それこそ unpack() のやってくれることですから。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/dayflower/20080625/1214374293