入力は可能な限り早く decode すべき

最近、perl の文字の扱いではまることはなかったが久々にはまった。 単語リストが書かれた word.txt を読み込んで、もにょもにょやろうとしている下記プログラムのどこがダメか。

# test.pl
use 5.14.0;
use warnings;
use Encode 'decode_utf8';

open my $fh, "<", "word.txt" or die "word.txt: $!";
my %word;
while (my $line = <$fh>) {
    $line =~ s/\s+$//; # 行末の空白や改行を除去
    $word{ decode_utf8 $line }++;
}

# do something with %word
use Data::Dumper;
print Dumper \%word;

word.txt が

> cat word.txt
駅
原因

のような場合、

> perl test.pl
$VAR1 = {
          "\x{539f}\x{fffd}\x{fffd}" => 1,
          "\x{fffd}\x{fffd}" => 1
        };

となる。ここで "\x{fffd}" は壊れた unicode 文字の置き換えとして使われる文字である。

なぜこうなってしまったかというと 「駅」の utf8 byte 列は "\xe9\xa7\x85" であり、 $line =~ s/\s$// で "\x85" (NEXT LINE) が除去されてしまったから。

ちなみにこの挙動は perl のバージョンによって変わり、

  • perl 5.12 以下では起こらない。
  • perl 5.14 以上では use feature 'unicode_strings' もしくは use 5.12.0 以上を指定したとき。

のようだった。

( 5.12 はおそらくバグってる。cf: https://gist.github.com/shoichikaji/70b714c7a79b59897bd6 )

つまるところ、入力はできる限り早く decode するようにすれば、このようなことは起こらない。 今回の場合は open layer に指定するのが一番。

# test.pl
use 5.14.0;
use warnings;

open my $fh, "<:encoding(UTF-8)", "word.txt" or die "word.txt: $!";
my %word;
while (my $line = <$fh>) {
    $line =~ s/\s+$//;
    $word{ $line }++;
}

SEE ALSO

( perl のバージョンをいろいろ変えてみるべし)