まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2006/07/28(金)マニュアルから知らなかった関数

perlの関数メモ

知らなかった関数から抜粋。もっと早く読んでおけばよかったなぁ。

lc/uc

最初が小文字変換、後者が大文字変換。要するに次と等価。

$x=lc($x) : $x =~ tr/A-Z/a-z/;
$x=uc($x) : $x =~ tr/a-z/A-Z/;

study SCALAR

何回も文字列に対するパターンマッチを行なうアプリケーションで、そのような文字列 SCALAR を予め学習しておきます。(中略)別のスカラを study した場合には、以前に学習した内容は「忘却」されてしまいます。

(この study の仕組みは、まず、検索される文字列内のすべての文字のリンクされたリストが作られ、たとえば、すべての 'k' がどこにあるかがわかるようになります。各おのの検索文字列から、C プログラムや英語のテキストから作られた、頻度の統計情報に基づいて、もっともめずらしい文字が選ばれます。 この「めずらしい」文字を含む場所だけが調べられるのです。)

日本語の場合……難しいところですが、同じ(短めの)文字列に対して予めリンクリストを作っておけば、次からのパターンマッチが高速化されるようです。実際には計測して速いかどうか確認してから使えと書いてあります。

ループ関連

redo

redo コマンドは、条件を再評価しないで、ループブロックの始めからもう一度実行を開始します。 continue ブロックがあっても、実行されません

ラベル指定 last/next/redo

last などのコマンドは、ラベルによって抜けるループを指定できるようです。多段ループから抜けたいときいつも

my $z;
foreach my $x (1..9) {
  my $flag;
  foreach my $y (1..9) {
    if ($x*$y>50) { $z=$x*$y; $flag=1; last; }
  }
  if ($flag) { last; }
}

と書いてたのですが、ラベル指定を使えば

my $z;
OUT_LOOP:
foreach my $x (1..9) {
  foreach my $y (1..9) {
    if ($x*$y>50) { $z=$x*$y; last OUT_LOOP; }
  }
}

と書けばよかったようです。

正規表現/パターンマッチ

$&

$cookie_val =~ s/(\W)/ '%' . unpack('H2', $1)/eg;

とかやって URI エンコードをするのは有名ですか、わざわざ $1 のフレーズホルダーを使わなくても、

$cookie_val =~ s/\W/ '%' . unpack('H2', $&)/eg;

とすればよかったようです。$&というのはマッチした文字列そのものを示す特殊変数で、マッチ部より手前と後ろを示す $`, $' を使えば、

$cookie_val == "$`$&$'"

はマッチングが成功すれば常に成り立つことを意味してます。

:$&は正規表現の動作速度を低下させる恐れがあります。URIエンコード程度ならば$1を使いましょう(コメントでの指摘ありがとうございま)。→参考

m//;

m//; はマッチングをとる演算子で、何も付けずに//;したときと一緒。つまりは

if ($str =~ /<(\w+)>(.*?)<\/\w+>/) { print "$1 = $2\n"; }

としたときは暗黙に m が指定されているということです。/ がセパレーターなので\/エスケープしなければなりません。しかし置換表現ならば、

$str =~ s#<(\w+)>(.*?)</\w+>#print "$1=$2\n"#eg;
$str =~ s|<(\w+)>(.*?)</\w+>|print "$1=$2\n"|eg;
$str =~ s!<(\w+)>(.*?)</\w+>!print "$1=$2\n"!eg;
$str =~ s[<(\w+)>(.*?)</\w+>][print "$1=$2\n"]eg;
$str =~ s{<(\w+)>(.*?)</\w+>}{print "$1=$2\n"}eg;

などとセパレーターを変更することで、/をエスケープする必要がないわけです。でもマッチングを取るときは

if ($str =~ |<(\w+)>(.*?)<\/\w+>|) { print "$1 = $2\n"; }

とかできないで不便だなぁと思ってたのですが、つまり次みたいにすればよかったようです。

if ($str =~ m|<(\w+)>(.*?)<\/\w+>|) { print "$1 = $2\n"; }

q//;

q//; は正規表現をコンパイルした状態で保存する演算子です。正規表現をあたかもオブジェクトのように扱えます。/o オプションを使えば正規表現をコンパイルして保持できるのですが、2度とその場所の正規表現を変更できなくなるので使えませんでした。*1

@urlsを正規表現が格納された配列、@aryがマッチング処理をする配列だとします。

my $match;
LABEL: foreach my $line (@ary) {
  foreach(@urls) {
    if ($line =~ /$_/) { $match=1; last LABEL; }
  }
}

とすると、中のif文を実行する度に正規表現がコンパイルされ、@ary が大きいときに大変なロスとなります。

# 先にコンパイルしておく
foreach(@urls) { $_ = qr/$_/; }
my $match;
LABEL: foreach my $line (@ary) {
  foreach(@urls) {
    if ($line =~ /$_/) { $match=1; last LABEL; }
  }
}

とすることで、正規表現のコンパイルが1度のみとなり大きな速度向上が見込めます。ちなみにこのときのコンパイルされた正規表現式はref($str)に対して'Regexp'を返します。

my $reg = qr#<(\w+)>(.*?)</\w+>#;
print "$reg\n", ref($reg), "\n";

実行結果

(?-xism:<(\w+)>(.*?)</\w+>)
Regexp

pos SCALAR

対象の変数に対して、前回の m//g が終了した場所のオフセットを返します。また、代入することでオフセットを変えることも可能です。

と書かれています。/g 付きの正規表現は、連続マッチングを取るオプションですが、前回マッチングを取った場所より後ろが次のマッチング開始位置になります。無限ループなどにならない、至極当然の方式なのですが、極希にこの仕様が困ったことになります。

my $str = <<TEXT;
>>
1
<<
>>
2
<<

>>
3
<<
TEXT

という文字列から「>>のみの行で始まり<<のみの行で終わる」ブロックを抽出することを考えます。こののみの行ってのが厄介です。

$str = "\n$str";  # 前処理*2
$str =~ s/\n>>\n(.*?)\n<<\n/print "$1 "; "\n"/esg;

とすれば、うまく行きそうに見えます*3。ですが実際には、

1 3 

と見えて表示されません。というのも、1つめのブロックとマッチする最後の"\n"と、2つめのブロックをマッチするときの最初の"\n"が同じものを示しているためです。ここで pos 関数の出番です。

while($str =~ m|\n>>\n(.*?)\n<<\n|g) {
	print "$1(pos:", pos ($str), ")  ";
	pos($str) = length($`);
	$str = $` . "\n" . $';
}

とすれば、

1(pos:9)  2(pos:9)  3(pos:10)

となり正しく認識出来ます。

追記:もっとスマートな方法

前回マッチした部分とマッチする "\G" という要素を使えば、もっとスマートな方法で実現可能でした。

my $str = "aaa0bbb0ccc0ddd1eee0fff0ggg";
$str =~ s/(?:\G|0)(\w\w\w)0/
	print "$1\n";
	/eg;

実行結果は、

aaa [0]
bbb [4]
ccc [8]
fff [19]

また、(?:...|...)は$1などへの割り当てない部分正規表現で、マッチ部の割り当てという無駄な処理が減るので処理が効率化できます*4。また\Gを使うことで ^\w\w\w とマッチする効果があり、前処理として\nを追加する必要がなくなります。

.......ソース書きなおそ(笑

*1 : クロージャと /o オプションを組み合わせればいいかなーとか最初思ってたのですが、コンパイルしてしまう方法の方がよっぽどスマートでした

*2 : ^を使った正規表現式を書けばいいのですが、正規表現の式が2倍になるより、予め前処理した方が効率的なのでこうしています

*3 : /e は置換文を実行するオプション。/gは連続置換するオプション。/sは改行を含めてマッチングするオプション。

*4 : 知らなかった