2006-09-13

(追記あり。)

問い:
別のところで定義されている文字 $A, $B があったとする。文字列 $s 中にある $A をすべて $B に置き換えたい。Perl でこれを処理するにはどう書けばよいでしょう。

$A, $B というのはようするにグローバル変数です。

さて、$A, $B が文字なので、文字を入れ替える tr 演算子を使うだろう、と考えるわけです。

$s =‾ tr/$A/$B/;

ブー。これはうまくいかない。tr 演算子は変数展開をしない。なんなんだこれは。$s =‾ tr/X/Y/; と書いて XXXYYY にすることはできるけど、$X = "X"; $Y = "Y"; tr/$X/$Y/; はできない。どうするのかというと、こうしろと。

eval "¥$s =‾ tr/$A/$B/;";

ありえん。eval て。個人的には eval というのは最後の手段的なものだと思ってましたが。デバッグできないし。しかもこれは正しく動かない。$A‘/’$B‘Y’ だったらどうなりますか。eval されるのは “$s =‾ tr///Y/;” というわけでエラーになります。

なので事前に文字をエスケープする必要があります。$A, $B はグローバル変数なので勝手に置換してしまうわけにはいかないので、ローカルにコピーして使います。

my $a;
my $b;
($a = $A) =‾ s/(¥¥|¥/)/¥¥$1/g;
($b = $B) =‾ s/(¥¥|¥/)/¥¥$1/g;
eval "¥$s =‾ tr/$a/$b/;";

これでいちおう正解(のはず)。たのしいね。

こんなのやってられないので正規表現置換することにします。s 演算子は変数展開もしてくれるし。

$s =‾ s/$A/$B/g;

もちろんこれもアウト。今回は $A ないし $B‘¥’ だと正規表現のメタ文字として解釈されておかしなことになります。というわけでエスケープ。今回も $A, $B はローカルにコピーします。

my $a;
my $b;
($a = $A) =‾ s/¥¥/¥¥¥¥/g;
($b = $B) =‾ s/¥¥/¥¥¥¥/g;
$s =‾ s/$a/$b/g;

これで正解(のはず)。ぜんぜん行数減ってない。

追記。

上のコードは間違っていました。$B‘¥’ のときに間違った結果になります。正しくは以下のようにする必要がある。

my $a;
($a = $A) =‾ s/¥¥/¥¥¥¥/g;
$s =‾ s/$a/$B/g;
$s;

エスケープ処理するのは $A のほうだけでいい。置換文字列のほうは正規表現じゃないからか。なんか正規表現リテラルがあるとかえって混乱するな……。

‘X’‘¥’ に置換。 ‘¥’‘X’ に置換。
直接指定 変数を経由 直接指定 変数を経由
Perl
$s =‾ s/X/¥¥/;
$a = "X";
$b = "¥¥";
$s =‾ s/$a/$b/;
$s =‾ s/¥¥/X/;
$a = "¥¥¥¥";
$b = "X";
$s =‾ s/$a/$b/;
Python
re.sub('X', '¥¥', s)
a = 'X'
b = '¥¥'
re.sub(a, b, s)
re.sub('¥¥¥¥', 'X', s)
a = '¥¥¥¥'
b = 'X'
re.sub(a, b, s)
TX-C(笑)
regular(s, "X", "¥¥", 0);
a = "X";
b = "¥¥";
regular(s, a, b, 0);
regular(s, "¥¥¥¥", "X", 0);
a = "¥¥¥¥";
b = "X";
regular(s, a, b, 0);

↑これでよい? 強調部分の非対称性が罠か。

さて、Perl で正規表現のエスケープに使うメタ文字、¥Q, ¥E中村さんから教えてもらいました。

$s =‾ s/¥Q$A¥E/$B/g;

ありがとうございました。これがどうも Perl での「いちばん正しい」書きかたみたいですね。

じつはもうひとつ、1行で書けるのも考えました。

join '', map { $_ eq $A ? $B : $_ } split //, $s;

でもこれはかなり遅いみたいです。それにべつにわかりやすいわけでもありませんし。

ところでこの処理を Python で書くとこうなります。(べつに Python でなくてもまともな文字列操作メソッドがある言語ならおんなじでしょうけど。)

s.replace(A, B)

あと Perl でもっと簡単に書ける方法をご存知の方は教えていただけるとありがたいです(というか単純な replace はないのでしょうか)。