Perl-5.8 覚え書き

Last Updated at $Date: 2005/12/04 04:00:59 $.

このページは,Perl-5.8.2 を使う上で困ったことなどの覚え書きです.


日本語を含むスクリプトを書く

Perl-5.8.x で日本語を扱うスクリプトを書く場合,大きく2通りの方法があります.

  1. 日本語 EUC でスクリプトを書く.
  2. UTF-8 でスクリプトを書く.

どちらの方法でも, (1)ファイル入出力, (2)データベースアクセス, (3)プロセス間通信の3つに気をつけて, 明示的にバイト列を文字列に変換したり,また逆に文字列をバイト列に変換する必要があります.

実際には,これら3つは全て「ファイルハンドル」として抽象化されるので, まとめて扱うことができますが.

日本語 EUC でスクリプトを書く

基本になるのは,encoding プラグマです.

use encoding "euc-jp";
print "こんにちわ\n";

use encoding "euc-jp" という指定は, (1)標準入出力の文字コードとして日本語 EUC を使い, (2)スクリプトは日本語 EUC で記述されていると見なす,という意味を持ちます. また,encoding プラグマによる指定は,グローバルな効果を持ちます.

したがって,この指定だけでは標準エラー出力には正しく出力されません.

use encoding "euc-jp";
warn "文字化けするはずです\n";

この問題を避けるためには,binmode 関数を使って, 標準エラー出力の文字コードを明示的に設定しておきます.

use encoding "euc-jp";
binmode STDERR, ":encoding(euc-jp)";
warn "正常に出力されましたか?\n";

詳しくは,後の説明を参照してください. とりあえず,use encoding ... binmode ... はイディオムとして覚えておきましょう.

encoding プラグマはグローバルな効果を持ちますから,以下の点に注意が必要です.

・特定のスコープだけ encoding プラグマを無効化することはできません.

関数 foo の内部だけは,単なるバイト列として扱って欲しいと考えて, 以下のような指定をしても動きません.

sub foo {
    no encoding;
    $str = "日本語EUCのバイト列‥‥とは解釈されない";
}
・他の文字コードで記述されたモジュールを利用できません.

ISO-2022-JP や Shift-JIS など他の文字コードで記述されているモジュールも 日本語 EUC で記述されていると一括して見なされてしまいます. したがって,それらのモジュール中で使われている文字は,正常に解釈されないでしょう. ただし,UTF-8 で記述されていて,かつ, utf8 プラグマが指定されているモジュールは利用できます.

use utf8;
# ... UTF-8 で記述されたモジュール
1;
encoding プラグマを複数回指定することはできません.

複数の encoding プラグマが指定された場合は,最後のプラグマだけが意味を持ちます.

UTF-8 でスクリプトを書く

基本になるのは,utf8 プラグマです.

# 以下のスクリプトは UTF-8 で保存すること.
use utf8;
print "UTF-8 でこんにちわ\n";

このスクリプトは,標準入出力との通信にも UTF-8 を使います. システム全体を Unicode 化したという少数の人はこれで良いでしょうが, まだ大多数の人は日本語 EUC を使っているでしょう. その場合は,以下のように, 標準入出力および標準エラー出力には日本語 EUC を使うように指定してください.

# 以下のスクリプトは UTF-8 で保存すること.
use utf8;
binmode STDOUT, ":encoding(euc-jp)";
binmode STDERR, ":encoding(euc-jp)";
binmode STDIN, ":encoding(euc-jp)";
print "UTF-8 でこんにちわ\n";

日本語を含むファイルを開く

基本

組み込み関数 open の書式が拡張されて, 読み込み・書き込みの encoding が指定できるようになっています. この拡張書式を利用するのが,最も基本的な方法です.

open( F, "<:encoding(euc-jp)", "filename" ) or die;
print <F>;
close F;

詳しい情報は,perldoc -f open または perldoc perlopentut してみると得られます.

binmode 関数を使う

既に open 済みのファイルハンドルを対象として, encoding を指定(変更)する場合は,binmode 関数を使います.

binmode STDOUT, ":encoding(euc-jp)";

open プラグマを使う

open を呼び出すたびに指定するのが面倒だ, という場合は以下のように open プラグマで指定します.

use open IO => ":encoding(euc-jp)";
open( F, "< filename" ) or die;
print <F>;
close F;

open プラグマによる指定は pipe の場合にも有効になります. 以下の例では,ISO-2022-JP に encode された文字列が kcc に渡されます.

use encoding "euc-jp";
use open OUT => ":encoding(iso-2022-jp)";
open( P, "| kcc -c" ) or die;
print P "漢字を含む文\n";

拡張された open 関数の呼出しの場合(= 引数が3個以上指定されている場合)には, open プラグマによる指定は有効にはなりません.

# このスクリプトは期待通りに動作しない.
use encoding "euc-jp";
use open IO => ":encoding(euc-jp)";
open( F, "<", "filename" ) or die;
while( <F> ){
    m/正規表現/ and  print;
}

無名のファイルハンドル

固定的なファイルハンドル名を利用していると, ファイルハンドル名の衝突が気になる, という場合は次のように書きます.

open( my $fh, "< filename" ) or die;
print <$fh>

なお,変数 $fh のスコープは関数 open の属するブロックと一致しています.

use strict;
if( open( my $fh, "not-exist-file" ) ){
    print <$fh>;
} else {
    print $fh;
}
# ここに print $fh と書くと,compile error になる.

この実験結果から分かる通り,open に失敗した場合でも $fh の内容は空ではないので,以下のスクリプトは動作しません.

# このスクリプトは期待通りに動作しない.
open( my $fh, "not-exist-file" );
if( $fh ){
    # ここに処理が移ってしまう.
    print <$fh>;
} else {
    print "Cannot open file\n";
}

Perl-5.6 と同じように,IO::File モジュールまたは FileHandle モジュールを用いることもできます.

use IO::File;
my $fh = IO::File->new( $0, "<" ) or die;

# 文字コードを指定する場合は binmode 関数を使う
binmode( $fh, ":encoding(euc-jp)" );

# ただし,open の拡張された書式で文字コードを指定しようとすると,エラーになる
my $fh2 = IO::File->new( $0, "<:encoding(euc-jp)" ) or die;

連想配列で日本語を使う

Perl-5.8 の内部では,全ての文字は Unicode で表現されています. したがって,連想配列のキーや値としても,日本語を普通に使うことができます.

use encoding "euc-jp";
my %hash;
$hash{"キー"} = "値";
while( my( $key, $value ) = each %hash ){
    print "$key:$value\n";
}

ところが,DB_File クラスや GDBM_File クラスと結び付けられた(tie された)連想配列を使おうとすると, 保存したつもりの文字列が得られず,不可思議な動作をするようになります.

use DB_File;
use encoding "euc-jp";
tie( %hash, 'DB_File', "filename", O_CREAT ) or die;
$hash{"キー"} = "値";
while( my( $key, $value ) = each %hash ){
    print "$key:$value\n";
}
# このスクリプトを実行しても,まともな文字列は得られません.

以下のように,明示的に decode すれば,元々の文字列を回復することができます.

use DB_File;
use Encode qw/ decode_utf8 /;
use encoding "euc-jp";
tie( %hash, 'DB_File', "filename", O_CREAT ) or die;
$hash{"キー"} = "値";
while( my( $key, $value ) = each %hash ){
    printf( "%s:%s\n", &decode_utf8($key), &decode_utf8($value) );
}

データベースと言っても,ファイルとの入出力を行っていることに変わりはありませんから, ファイルを開く場合と同様にきちんと encode/decode を行う必要があるわけです. DB_File モジュールの場合は以下のように, キーと値を読み書きするときのフィルタを設定すると,期待通りに動くようになります.

use DB_File;
use Encode qw/ encode decode /;
use encoding "euc-jp";
    
$db = tie( %hash, 'DB_File', "filename", O_CREAT ) or die;
$db->filter_store_key  ( sub { $_ = encode("euc-jp", $_) } );
$db->filter_store_value( sub { $_ = encode("euc-jp", $_) } );
$db->filter_fetch_key  ( sub { $_ = decode("euc-jp", $_) } );
$db->filter_fetch_value( sub { $_ = decode("euc-jp", $_) } );

$hash{"キー"} = "値";
while( my( $key, $value ) = each %hash ){
    print "$key:$value\n";
}

このようなフィルタが定義されていないモジュールを使う必要がある場合には, 簡単なラッパーを書けば解決できます. GDBM_File モジュールのラッパーの例を以下に示します.


日本語を含む正規表現

以下のような内容のスクリプトを,日本語 EUC で保存して実行してみます.

my $str = "漢字";
$str =~ s/岨//;
print "$str\n";

何も指定しなければ,次のような結果が得られるでしょう.

$ perl sample.perl
旗

この結果は,多バイト文字の文字境界を考慮せずに正規表現検索が行われてしまっていることを示しています.

Perl に多バイト文字の文字境界を考慮させるためには,先に説明した通り, 正しい方法で多バイト文字を含むスクリプトを書く必要があります.

Unicode 規格で定義されている文字種の名前を利用すると, 以下のような正規表現を書くことができます.

平仮名に一致する正規表現
$str =~ m/(\p{Hiragana}+)/;
片仮名に一致する正規表現
$str =~ m/(\p{Katakana}+)/;
句読点に一致する正規表現
$str =~ m/(\p{Punctuation}+)/;
漢字に一致する正規表現
$str =~ m/(\p{Han}+)/;

詳しくは,perldoc perljp してみて下さい.


文字コードの自動判定

Encode::Guess モジュールを使います. 使い方には, (1)文字コードを推定する関数 guess_encoding を明示的に呼び出す方法と, (2)自動推定を行うことを指定する特別な文字コード Guess を指定する方法, の2通りがあります.

第1の方法の実例として,簡単な nkf コマンドを perl で実装してみると,以下のようになります.

#!/usr/bin/perl
use Encode qw/ from_to /;
use Encode::Guess;
use Getopt::Long qw/ :config no_ignore_case /;
use open IO => ":bytes";
use strict;
my $in;
my $out = "euc-jp";
&GetOptions( 'JIS'      => sub { $in = "7bit-jis"; },
             'EUC'      => sub { $in = "euc-jp"; },
             'SHIFTJIS' => sub { $in = "shiftjis"; },
             'jis'      => sub { $out = "7bit-jis"; },
             'euc'      => sub { $out = "euc-jp"; },
             'shiftjis' => sub { $out = "shiftjis"; } );
my $data = join( undef, <> );
unless( $in ){
    my $enc = guess_encoding( $data, qw/ euc-jp shiftjis 7bit-jis / );
    if( ref $enc ){
        $in = $enc->name;
    } else {
        die "Cannot estimate encoding: $enc\n";
    }
}
&from_to( $data, $in, $out );
print $data;

文字コードを自動推定する guess_encoding 関数は,引数として, 推定対象となるデータと文字コードの推定範囲を取ります.

第2の方法,すなわち特別な文字コード Guess を指定する場合は, 文字コードの推定範囲を適当な方法で先に指定しておく必要があります. もっとも簡単な方法は,以下のように Encode::Guess モジュールの引数として指定する方法です.

use Encode qw/ decode encode /;
use Encode::Guess qw/ euc-jp shiftjis 7bit-jis /;
print encode( "euc-jp", decode( "Guess", join( undef, <> ) ) );

以下のように,set_suspects 関数を用いて指定することもできます.

use Encode::Guess;
Encode::Guess->set_suspects( qw/ euc-jp shiftjis 7bit-jis / );

その他の情報は, もっと詳しいページを見てください.


古い Perl でも実行できるように書く

encoding プラグマは,Perl-5.6 や Jperl には存在しないので, 普通に encoding プラグマを使ったスクリプトを実行しようとすると, 以下のようなエラーになります.

Can't locate encoding.pm in @INC.
BEGIN failed--compilation aborted at sample.perl line 3.

以下のように,BEGIN ブロックを利用して, Perl のバージョン検査を行ってから require & import を行うようにすると, encoding プラグマの存在しない Perl でも動くスクリプトが書けます.

use English qw/ $PERL_VERSION /;
BEGIN {
    if( $PERL_VERSION > 5.008 ){
	require encoding;
	encoding->import( "euc-jp" );
    }
}

日本語の正規表現を使っているなどの理由で, Perl-5.8.x または Jperl だけを使いたい場合は以下のように書きます.

use English qw/ $PERL_VERSION /;
BEGIN {
    if( $PERL_VERSION > 5.008 ){
	require encoding;
	encoding->import( "euc-jp" );
    } elsif( split( //, "字" ) == 1 ){
	require I18N::Japanese;
	I18N::Japanese->import();
    } else {
	die "Your Perl does not support Japanese characters.";
    }
}

未解決の問題

日本語を含むコンテンツを HTML::TreeBuilder で処理するには?

日本語部分が Unicode の文字実体参照に変換されてしまう.

use strict;
use encoding "euc-jp";
use Encode qw/ decode encode_utf8 /;
use HTML::TreeBuilder;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new();
my $response = $ua->get( "http://namazu.org/~tsuchiya/" );
if( $response->is_success ){
    my $tree = HTML::TreeBuilder->new;
    $tree->parse( encode_utf8( decode( "iso-2022-jp", $response->content ) ) );
    # print $tree->as_HTML;
    print decode_utf8( $tree->as_text );
}

HTML::Parser モジュールの handler を適切に設定すればいいのかもしれない.

NDO::Weblog の記述によれば,既知の問題であるらしい.とりあえず,encode_utf8 を呼んで,先に UTF-8 なバイト列にしてから parse しなければならないようだ. この方法で,明示的に encode/decode することにより,as_text() メソッドでの出力は出来るようになった.as_HTML() を利用したり, 透過的に処理を行う方法がまだ分からない.


[Top] / [私の使っている Perl Scripts] / [Perl-5.8 覚え書き]