July 20, 2004

Perl の open プラグマと UTF8 フラグの微妙な関係

[ Perl ]
ところで、「Perl 5.8 Unicode で再びはまり中」の件なのですが、解決なさったのでしょうか。use open IO=>':utf8';すればいいと思うのですが。

とコメントをいただきました。以下、この件に関して。('Perl 5.8 Unicode で再びはまり中' も参照のこと)

Perl 5.8 では文字列に対して UTF8 フラグという概念があります。詳しい話は 'Perl 5.8 以降においての Unicode 文字列の扱い方' にまとめてあります。フラグが立っている文字列は文字として扱われるため、たとえば length() により数えた文字の長さがバイト数ではなく文字数として得られたりします。

このときフラグが立っている文字列と、そうでない文字列を連結したりで一緒に扱った場合、文字化けを起こします。従って、ファイルを open したりする場合に、そのファイルから読み取った文字列にフラグが立っているか、立っていないかですとか、XML::Parser を通した文字列にフラグが...といった具合で、常にフラグの存在を意識しながらプログラミングする必要があります。

スクリプトからファイルを open するとして、そのファイルに記述されたテキストに対して明示的にフラグを立てることは、open プラグマあるいは PerlIO レイヤを使うことで可能です。

例えば以下のようにファイルを open() し、その結果得られた文字列を Devel::Peek で dump してみます。

#!/usr/local/bin/perl
 
use strict;
use warnings;
use Devel::Peek;
  
local $/;
open my $fh, "sample.txt";
my $text = <$fh>;
close $fh;
 
Dump $text;

結果、以下のように変数内部のステータスがわかります。

$ perl open.pl
SV = PV(0x83a941c) at 0x83b3c30
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK)
  PV = 0x83b9c78 "this is a sample"\0
  CUR = 16
  LEN = 80

さて、先のスクリプトで明示的にフラグを立てるよう open プラグマ、すなわち use open で指定します。

#!/usr/local/bin/perl
 
use strict;
use warnings;
use Devel::Peek;
use open IO => ':utf8';
...

スクリプトを実行すると、

$ perl open.pl
SV = PV(0x95d441c) at 0x95dec60
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK,UTF8)
  PV = 0x95dab38 "this is a sample"\0 [UTF8 "this is a sample"]
  CUR = 16
  LEN = 80

と、UTF8 フラグが立っていることがわかります。この open プラグマですが、

The open pragma serves as one of the interfaces to declare default "layers" (also known as "disciplines") for all I/O. Any two-argument open(), readpipe() (aka qx//) and similar operators found within the lexical scope of this pragma will use the declared defaults.

とありますとおり、デフォルトの PerlIO レイヤを指定するもので、先のスクリプトでは utf8 レイヤをデフォルトに設定したということになります。open されたファイルは utf8 レイヤをはさむので UTF8 フラグが立った文字列が得られるというわけです。

しかし、ここで一点問題になるのがこの一文における 'operators found within the lexical scope of this pragma' の部分。「このプラグマによるレキシカルスコープの範囲内で...」、 つまり open プラグマはあくまでプラグマが作用するスコープが限定されているということです。

実際、

#!/usr/local/bin/perl
 
use strict;
use warnings;
use Devel::Peek;
use FileHandle;
use open IO => ':utf8';
 
local $/;
my $fh = FileHandle->new('sample.txt');
my $text = $fh->getline;
$fh->close;
 
Dump $text;

とファイルの open() を抽象化した FileHandle モジュール に任せた場合、

$ perl filehandle.pl
SV = PV(0x820f52c) at 0x8237f58
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK)
  PV = 0x8250b50 "this is a sample"\0
  CUR = 16
  LEN = 17

と UTF8 フラグが立ちません。open() を行っている箇所が外部モジュールなので、open プラグマのスコープの範囲外であり IO レイヤ指定が有効になりません。

さらに検証すべく、open を自前で外部モジュール化して試してみます。

package NDO::File;
 
use strict;
use warnings;
 
sub new {
    my $class = shift;
    my $self = bless {}, $class;
    $self->_init(@_);
    $self;
}
 
sub _init {
    my $self = shift;
    my $file = shift or
	die 'usage: NDO::File->new("filename")';
    open my $fh, $file or
	die 'cannot open $file: $!'
    $self->{_fh} = $fh;
}
 
sub getline {
    my $fh = shift->{_fh};
    return <$fh>;
}
 
sub getlines {
    my $fh = shift->{_fh};
    local $/;
    return <$fh>;
}
 
sub DESTOROY {
    my $self = shift;
    if ($self->{_fh}) {
	close $self->{_fh};
    }
}
 
1;

このモジュールを使ってデバッグしてみます。

#!/usr/local/bin/perl
 
use strict;
use warnings;
use NDO::File;
use Devel::Peek;
use open IO => 'utf8';
  
my $file = NDO::File->new('sample.txt');
my $text = $file->getlines;
 
Dump $text;

実行結果は以下。

$ perl mymod.pl
SV = PV(0x86675d8) at 0x8671c3c
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK)
  PV = 0x8675aa8 "this is a sample"\0
  CUR = 16
  LEN = 17

やはり、open プラグマのスコープ範囲外でファイルを open しているので UTF8 フラグは立ちません。NDO/File.pm 内で open プラグマを指定した場合、つまり

package NDO::File;
 
use strict;
use warnings;
use open IO => ':utf8';
...

とした場合には、

$ perl mymod.pl
SV = PV(0x8afec64) at 0x8af4c3c
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK,UTF8)
  PV = 0x8b32488 "this is a sample"\0 [UTF8 "this is a sample"]
  CUR = 16
  LEN = 17

と、もちろんフラグが立ちます。

このように open プラグマは外部モジュールで open されたファイルにまでは作用しないので、'Perl 5.8 Unicode で再びはまり中' で書いたような、HTML::TemplateTemplate-Toolkit などがそれぞれ open() を行っているため云々という問題には、open プラグマでは対応できないようです。

テンプレートインスタンスには(ファイル名ではなく) PerlIO レイヤによりフラグを立てて open したファイルハンドルを渡す、あるいは渡す文字列のフラグを事前に落としておくといった若干バッドな方法で対応する必要がありそうです。ちなみに僕は Template-Toolkit の場合 utf8 フラグを落とす FILTER プラグインを書いて、それで対処しています。(プラグインで対処するというアイデアは blog.bulknews.net の中の人に教えてもらいました。)

Posted by naoya at July 20, 2004 09:50 AM | トラックバック (0)  b_entry.gif
トラックバック [0件]
TrackBack URL: http://mt.bloghackers.net/mt/suck-tbspams.cgi/1135
コメント [0件]
コメントする









名前、アドレスを登録しますか?