camel

久々の勝手に添削。今回はこちら。

正規表現がらみなので、Perl以外でも有用。

添削箇所は、こちら。

40行で作るPerl用テンプレートエンジン
sub convert {
    return unless defined(my $str = shift);
    $str =~ s{&}{&}gso;
    $str =~ s{<}{&lt;}gso;
    $str =~ s{>}{&gt;}gso;
    $str =~ s{\"}{&quot;}gso;
    $str;
}

これの最初のsubstitutionが&amp;ではなくて&ではないかというのはさておき、こういった場合、何度も正規表現をかけて少しずつというのは、効率の面だけではなくバグを呼ぶという点を考えてもいい手とは言えない。

例えば、これを以下のように書いてしまった場合、どうなるだろうか。

sub convert {
    return unless defined(my $str = shift);
    $str =~ s{<}{&lt;}gso;
    $str =~ s{>}{&gt;}gso;
    $str =~ s{\"}{&quot;}gso;
    $str =~ s{&}{&amp;}gso;
    $str;
}

この場合、先に&gt;となったものが、&amp;gt;となるリスクが避けられない。

こういう場合は、文字クラスとhashのコンボで攻めるのが正しい。

my %escaped = ( '&' => 'amp', '<' => 'lt', '>' => 'gt', '"' => 'quot' );
sub escape {
    my $str = shift or return;
    $str =~ s{([&<>"])(?!amp;)}{'&' . $escaped{$1} . ';'}msxgeo;
    $str;
}

将来性を考えると、こうするとさらによさそうだ。

my %escaped = ( '&' => 'amp', '<' => 'lt', '>' => 'gt', '"' => 'quot' );
my $cclass2escape = '[' . join('', keys %escaped) . ']';
sub escape {
    my $str = shift or return;
    $str =~ s{($cclass2escape)(?!amp;)}{'&' . $escaped{$1} . ';'}msxgeo;
    $str;
}

(?!amp;)がいらない、ということであれば、正規表現を使わない方法もある。

my %escaped = ( '&' => 'amp', '<' => 'lt', '>' => 'gt', '"' => 'quot' );
sub escape{
    my $str = shift or return;
    my $result = '';
    $result .= $escaped{$_} ? '&' . $escaped{$_} . ';' : $_
        for (split //, $str);
    $result;
}

もっとも、互換性を考えればCPANのHTML::EntitiesHTML::Entities::encode_entities()を使うべきという意見もあるが、この場合はCPANに頼るのは趣旨に反するだろう。

この問題、Web上で実に多く目にするので、改めて添削した次第。

Dan the Perl Monger

追記:

はてなブックマーク - t-murachiのブックマーク / 2007年10月30日
s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/eg

惜しい。$&はペナルティが大きいので使用は避けるべき。

s/([<>&"])/'&'.{qw(< lt > gt & amp " quot)}->{$1}.';'/eg;

かな。2bytes長いけど。

0行で作るPerl用テンプレートエンジン : ひろ式めもちょう
テンプレートタグの開始文字列は「@{[ do {」、終了文字列が「} ]}」です。

究極www これだから Perl Mongers という生き物はwww