つれづれの正規表現 (第二巻)

これは第一巻の続きである。 つまり、正規表現を使いこなせないという掲示板の声に応える形で 書きはじめたものである。 第一巻の冒頭で書いたように、私はこれを実用に供するように書こうとは 思ってはいない。正規表現に関した読みもの程度に考えてもらった方がよい。 それが証拠に正規表現の解説などまったく無視して書いた部分もある。

例によって、 このテキストを読まれる際には何等かの正規表現のリファレンスなどを 手もとに置いて読み進まれることを希望する。繰り返すが、このテキストには 正規表現の一覧表などはない。また、このテキストで扱う 正規表現は Vine Linux 1.1 にインストールされている perl 5.00404 の 正規表現である。スクリプトはすべてこの perl で動作を確認している。 日本語化された jperl などを使用されている方は正規表現の 解釈に於いて動作が違う場合もあるので注意されたい。

さて、第一巻を読んでみてばかばかしいと感じた人は少なからずいると思う。 ことに私がやたらに RFC を引用するのでへき易としている人は多いかも知れない。 しかし、こういったテーマを扱う際に仕様や規格にあたるというのは 実に面白い作業だ。 仕様はいわば遊びのルールである。 ルールがあればこそ、ルールの許す範囲内で何が出来るかを考える楽しみが生まれる。 ルールのない遊びはつまらない。何でも出来るからだ。 何でも出来ると言うことは、何が出来るかを考える余地がないということでもある。 何でも出来る自由と言うのは正規表現で言えば

.*

にすぎない。これは見るからにつまらない。 しかし、色々な制約がついた状況では、

[1-9-]\d*

などと正規表現は多彩にかつ複雑になる。

断わっておくが、私は正規表現の話をしている。 人間の自由と制約について論じる資格など、 もとよりこの私にあろうはずもない。

ユーザ名

多くの人はプロバイダと契約してインターネットに接続しているはずだ。 プロバイダといっても ppp で接続する様なものに限定している。 オンラインサインアップ時には、あなたはユーザ名とパスワードを 決めなければならなかったはずだ。 課金はそのユーザ名として何分接続したかで徴収されているはずだ。 また、電子メールアドレスも使えるようなところなら、 そのユーザ名があなたへ電子メールを送る際の宛先の重要な部分になる。

あるいは、あなたは web hosting サービスのアカウントを持っているかも知れ ない。やはりここでもユーザ名は必要だ。 ファイルをサーバにアップロードするときにはユーザ名とパスワードの入力の 手続きが必ず必要である。

あなたが大学生以上で恵まれた環境にいるのなら、実習用の UNIX マシンの アカウントの交付を受けているかも知れない。これにも、ユーザ名とパスワードが ある。ただし、実習用のアカウントは無味乾燥なことにアルファベット のあとに学籍番号が続いたものである。 大学の研究室や会社の UNIX マシンにアカウントがあるのなら、 tanaka, nakamura, manabu などの、もっと親近感のあるあなたの名前に 近いユーザ名だろう。 もちろん、管理者が許せば自分の名前とは無関係であっても差し支えない。 hoge でも foobar でもよい。 しかし、学校や職場でつかうユーザ名はほとんど実名に近いことが多い。

もしも、自宅のパソコンならなんでもありだ。それこそ kevin でも 構わないし、r00tzer0 でも構わない。ちなみに私は ageha を使っている。 え? root があなたのユーザ名だって? 自分のパソコンなのだから何をしても構わないが、先々のことを考えると 一般的なユーザを登録してそれでログインした方が良いかも知れない。そうだ、 それをまず説明することにしよう。 そのときにはユーザの名前を考えないといけない。そうだそれを書こう!

そこで、この章ではユーザ名の命名規則について考えてみたい。 しかし、命名規則といっても、あなたが自分のパソコンの上で r00tzer0 やら kevin やらのユーザ名を使っても良いかどうかと言った問題とは 別次元の問題を考える。 なお以下でいうユーザ名というのはログイン名と同じである。即ち、

[ageha@vine RFC]$ telnet localhost
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

Vine Linux 1.1 (Rheingau)
Kernel 2.0.36 on an i586
login: 

と『login: 』というプロンプトが表示された際に入力すべきものである。

一般ユーザの登録

昔からユーザ名は 8 文字以内となっていた。 いや、正確にはそういうふうに思っていた。 しかし、この根拠はなんだろうか?

Vine Linux 1.1 のオンラインマニュアルのうち主なものにあたってみた。 useradd(8), passwd(1), passwd(5) しかし、8 文字という制限については どこにも書かれていなかった。HOWTO といわれるドキュメント類にも 少なくとも日本語訳されたものには目を通してみた。 一般ユーザを登録せよ、root のままで Linux box を使ってはいけない。 そんなことは書いてあるが、ユーザ名の長さは何文字までで、文字としては 何が使えるかは書いてはいない。正直言って、これでは一般ユーザなど 登録することなど出来るわけがないではないか?

これはどう考えても正常な事態とは言えない。 今まで私も一般ユーザを登録してそれでログインするべきだと思っていた。 雑誌やインストール本の口調で人に自分の考えを押しつけることはしなかったが、 少なくとも自分の場合にはそうするべきだと思ったし、現にそうしている。 実際に、私はこの Linux box で一般ユーザを登録してそれでログインしている。 しかし、インストールされたドキュメント類を見ても登録しなければならない ユーザ名についての規則が書かれていないのだ。今まで私は『たまたま』システムが 許容するユーザ名を選択していたために一般ユーザを登録できていたに過ぎない。 ましてや、昨日今日 Windows 98 などを使っていた人が規則のはっきりしていな いユーザ名で一般ユーザを管理者権限で登録するなど出来るわけがない。 いや、出来る方がどうかしているといってもいいはずだ。 にもかかわらず、root でログインするな。の大合唱である。 この無責任なありさまを、どうかしていると言ってはいけないのだろうか?

ソースを見なければわからないが、少なくとも今のところ、 ageha はシステムに許容されたユーザ名であることはわかった。 一方、a なんてユーザ名はどうだろうか? Vine Linux の場合には useradd と言うコマンドを使えば ユーザが簡単に登録できるし、ホームディレクトリを作成して 初期設定ファイルの雛型をコピーしてくれる。 Vine Linux の場合には簡単で単純にシステム管理者権限で

[root@vine /etc]# useradd a

などとすればよい。 ただし、Vine Linux ではパスワードの設定はしてくれないのでそのまま管理者権限で パスワードを決める必要がある。

[root@vine /etc]# passwd a
New UNIX password: 
Retype new UNIX password: 
passwd: all authentication tokens updated successfully
[root@vine /etc]# 

一人でパソコンの Linux を使っている場合ならこれでいいが、 ユーザが何人かいる場合だと適当なパスワードを決めて 紙に書いて渡す。人数が多ければ、紙は厳封した封筒にいれてユーザに渡し、 同時にパスワードの変更の仕方と直ちにパスワードを変更するように 書いたメモもいれておく。こういった場合にはこちらで設定したパスワードを そのまま使い続けようとする不精者もいるので、パスワードの寿命を 1 週間程 度にしておくと良い。当然、パスワードの寿命が設定してあることは添付の 紙にも書いておく。 ユーザの人数が少なければ、その場でパスワードの変更の仕方を教えて 実際にその場で変更させるのが間違いがない。 ここでひとつ注意。ユーザがいくらあなたに信頼を寄せているからと言って、 絶対にユーザの希望をいれる形で初期パスワードを決めてはいけない。 これは root 権限ではいかなるパスワードも設定できてしまうためだ。 多くの場合、そのようにユーザの希望をいれる形にするとユーザは英単語の ようなパスワードを言い出して来る。しかし、多くのシステムでは ユーザが自分でパスワードを変更する際にはアルファベットと数字が混ざった 適当な長さのものでないと受け付けないようになっている。

ところで、上のように a などといったふざけたユーザは登録できた。

[root@vine /etc]# finger a
Login: a                                Name: 
Directory: /home/a                      Shell: /bin/bash
Never logged in.
No mail.
No Plan.

では、yakushimaru なんていうのはどうだろう? 11 文字ある。

[root@vine /etc]# finger yakushimaru
Login: yakushimaru                      Name: 
Directory: /home/yakushimaru            Shell: /bin/bash
Never logged in.
No mail.
No Plan.

驚くべきことにこれは登録できた。で、色々やってみた。

[root@vine /etc]# useradd abcdefghijklmnopqrstu            
[root@vine /etc]# useradd abcdefghijklmnopqrstuv
[root@vine /etc]# useradd abcdefghijklmnopqrstuvw
[root@vine /etc]# useradd abcdefghijklmnopqrstuvwx
[root@vine /etc]# useradd abcdefghijklmnopqrstuvwxy
[root@vine /etc]# useradd abcdefghijklmnopqrstuvwxyz
[root@vine /etc]# useradd abcdefghijklmnopqrstuvwxyzabcdefg
useradd: invalid user name `abcdefghijklmnopqrstuvwxyzabcdefg'
[root@vine /etc]# useradd abcdefghijklmnopqrstuvwxyzabcdef 
[root@vine /etc]#

つまり 32 文字までなら Vine Linux の場合にはユーザ名として使えそうだ。 次に

[root@vine /etc]# useradd 1
useradd: invalid user name `1'
[root@vine /etc]# useradd 1ageha
useradd: invalid user name `1ageha'

数字で始まる場合はどうもいけないようである。一方

[root@vine /etc]# finger kuro-ageha ayumi.hamasaki
Login: kuro-ageha                       Name: 
Directory: /home/kuro-ageha             Shell: /bin/bash
Never logged in.
No mail.
No Plan.

Login: ayumi.hamasaki                   Name: 
Directory: /home/ayumi.hamasaki         Shell: /bin/bash
Never logged in.
No mail.
No Plan.

記号を使っても、ハイフンとかピリオドは許容されるようである。 なおこんな遊びをしていると変なユーザが沢山たまってしまう。 登録できるかどうかだけを確認したら削除すると良い。 削除するには userdel コマンドを使う。ただし、ホームディレクトリまでは 削除してくれないので、次のようにすると良い。

[root@vine /etc]# userdel -r kuro-ageha

これでも /etc/group を見ると変なグループが沢山登録されている。 かならずチェックして削除しておいた方がいい。

ユーザ名の制限

さて、はっきりさせたくてインストール CD-ROM にソースコードを求めたが見つからな かった。仕方がないので FreeBSD 3.2-RELEASE のソースを見ることにした。 ここのところ FreeBSD は使わず Vine Linux ばかり使っているせいか だいぶ勘が鈍っているようだ。 FreeBSD には useradd なんてコマンドはなかった。それから userdel なんて コマンドもなかった。 あるのは adduser と言うコマンドと rmuser というコマンドである。 これが確か伝統的な名前であったと思う。 こういったコマンドの相違。 UNIX を使う人間は常にこれに悩まされる。コマンドの相違どころか、 設定ファイルの相違、インクルードファイルの相違、 システムコールの仕様の相違…。きりがない。 UNIX を経験したことのない人はひとくくりに『UNIX』という単一の オペレーティングシステムがあると思っている。 しかし、『UNIX』などという単一のオペレーティングシステムなんか ありはしない。あったとすれば、AT&T の Bell 研究所で PDP-7 の上に 産声をあげた、まさに初期の AT&T UNIX だけかもしれない。 もちろん、UNIX を使う我々だってひとくくりに『UNIX』とは言う。 しかし、それは他のオペレーティングシステムを意識したときだけである。 いや、FreeBSD から見れば Linux は『他のオペレーティングシステム』だし、 Linux から見れば FreeBSD だって『他のオペレーティングシステム』だ。 更に言えば、両者(いずれも BSD 系統)から見れば Solaris は異端の SysV とい うことになる。 細かい話になるが FreeBSD の陣営から言えば Red Hat Linux は BSD の系統と 呼んではいけないものなのかもしれない。 起動スクリプトが rcrc.local ではなくて SysV ライクな方式だ。 それに、ソケットの実装も違う。 もしも、キャメルブックを持っているのなら最後に『用語集』というのが ある。そこの UNIX というところを見てもらいたい。このような説明がある :

互換性を大きく欠いた何種類かの構文を持つ巨大で常に進化している言語。 この言語では、誰でも好きなものを自分の好き勝手に定義することが可能で、 また実際に誰もがそのようにしている。 この言語の話し手は、たやすく自分寄りにねじ曲げることができるという 経験を基に、この言語を習得するのは容易であると考えている。 しかし、方言の違いによって、部族間の意志の疎通はほぼ不可能に近く、 旅行者はしばしばカタコト言語風のサブセットしか使うことができない。 UNIX シェルプログラマはその技を身につけるのに何年もの歳月を 費さなければならないと、一般に信じられている。 多くの人々はこれを見切りをつけて、エスペラント風の言語 Perl でやりとり するようになりつつある。
古代には、UNIX という言葉は、ホコリをかぶっていた PDP7 コンピュータを 使うために、Bell 研究所の 2, 3 人が書いたちょっとしたコードのことも 意味していた。

今でも、いや、今のほうが、この説明はよくあたっている。

さて、話がだいぶ脱線した。FreeBSD の adduserrmuser はとても幸いなこ とに perl スクリプトで書いてある。このスクリプトの一部分を見てみよう。

# return username
sub new_users_name {
    local($name);

    while(1) {
        $name = &confirm_list("Enter username", 1, "a-z0-9_-", "");
        if (length($name) > 16) {
            warn "Username is longer than 16 chars\a\n";
            next;
        }
        last if (&new_users_name_valid($name) eq $name);
    }
    return $name;
}

sub new_users_name_valid {
    local($name) = @_;

    if ($name !~ /^[a-z0-9_][a-z0-9_\-]*$/ || $name eq "a-z0-9_-") {
        warn "Wrong username. " .
        "Please use only lowercase characters or digits\a\n";
        return 0;
    } elsif ($username{$name}) {
        warn "Username ``$name'' already exists!\a\n"; return 0;
    }
    return $name;
}

このコードを見れば明らかである。 まず、ユーザ名は 16 文字以下ということになっている。 さらに使える文字は正規表現を見ればすぐにわかる

^[a-z0-9_][a-z0-9_\-]*$

これを見る限り、 先頭の文字は小文字のアルファベットまたは数字またはアンダースコア であり、二文字目以降はそれらにハイフンを加えたものが許される。 もしも長さもこめて正規表現で書き表せば

^[a-z0-9_][a-z0-9_\-]{0,15}$

となる。 すると『1』だとか『______』なんていうユーザ名も許されることになる。 本当だろうか? また、FreeBSD の場合 Linux で使えるピリオドが使えない。 FreeBSD のオンラインマニュアルだと passwd(5) に使える文字の種類について のこの規則が書いてある。

The login name must never begin with a hyphen (``-''); also, it is strongly suggested that neither upper-case characters nor dots (``.'') be part of the name, as this tends to confuse mailers.

ユーザ名の長さについては adduser(8) のマニュアルページに書いてある。 こういっては Linux ユーザには悪いが FreeBSD の方が ドキュメントは優れている。 HOWTO を漁っても出てこなかった説明が、 マニュアルページを見ただけで、なぜ 16 文字なのかまで含めて書いて ある。しかも、変更の仕方と注意も書いてある。この記述は FreeBSD の FAQ に もあった。 また特定のシステムではっきりしたユーザ名の長さの基準を知るには utmp.h を 見ると良いこともこのことからわかる。 さらに私がユーザ名が 8 文字だと思い込んだ理由もこれでわかった。 今まで私が使っていた環境では NIS が動いていた。NIS が動いていれば ユーザ名は 8 文字以下にならざるを得ないことも書いてある。

The NIS protocol mandates an 8-character username. If you need a longer login name for e-mail addresses, you can define an alias in /etc/aliases .

最後に adduser(8) のマニュアルページではこのようなとどめが書いてあること を指摘しておく。

The reasons for this limit are "Historical". Given that people have traditionally wanted to break this limit for aesthetic reasons, it's never been of great importance to break such a basic fundamental parameter in UNIX.

この制限は歴史的なものである。 こういうときまって人々は『美しくない』などと言い立ててその制限を とりはらおうとしたがった。 しかし、このようなかくも基本的な UNIX のパラメータを破壊するような変更を 加えられる御大層な理由などありはしなかった。

URI

第一巻でカール掲示板の自動リンク機能について見てみた。 重複になるが、その機能を実現しているコードは

    if ($link eq "1") {
      $value =~ s/(https?|ftp|news):\/\/([\w|\!\#\$\%\&\'\(\)\=\-\^\`\\\|\@\~\[\{\]\}\;\+\:\*\,\.\?\/]+)/<a href=\"$1:\/\/$2\">$1:\/\/$2<\/a>/g;
    }

だった。 しかし、第一巻で指摘した通りこのコードでは実際にはあり得もしない URI にたいしてもリンクを張ってしまう。 それをここでは改善することを考える。 もちろん、これは実用的な観点からそれを考えようと言うのではない。 既にカール掲示板のスクリプトのままで実用的である。 それに、web hosting サービスのサーバでこの掲示板スクリプトを使うという 背景を考えると自動リンクの機能のためだけにこれ以上スクリプトの 動作を重くするわけにはいかない。 事実 CPU の使用時間が 5 秒を超える CGI は 動作させることを認めていないというサービスもある。 それに、そもそも実在したとしてもアドレスの移動で URI が意味を なさなくなる場合もある。ただ、URI を扱うことは 正規表現を考える上で面白い題材である。

あえて実用的な側面を指摘すると、URI を厳密にチェックすることで 掲示板に対する嫌がらせや事故を防ぐことが出来る。 基本的に掲示板はテキストベースで十分なので HTML のタグが使える必要は ないと思うが中には、意図せずそのような攻撃の余地をつくってしまう場合がある。 例えば、自分のページの URI を 入力できる欄が設けてあって、 そこに例えば http://www.hogehoge.co.jp/~user/ などと入力すると 投稿した内容とともに

<a href="http://www.hogehoge.co.jp/~user/">http://www.hogehoge.co.jp/~user/</a>

などとページへのリンクが張られる掲示板がある。 最近は無理だと思うが、自分の URI の代わりに

http://"><img src="http://www.bad.site.net/evil.jpg">

などと入力すると

<a href="http://"><img src="http://www.bad.site.net/evil.jpg">">
http://"><img src="http://www.bad.site.net/evil.jpg"></a>

と機械的に展開してしまうものがあったらしい。 この機械的な展開のために、結果的に不愉快な画像が張り付けられてしまう。 当然こういったものは正しい URI かどうかのチェックを行えば 避けられるはずである。まともな URI なら不等号もダブルクォーテーションも 含まないからだ。

ホスト名

ついでにホスト名とドメイン名について考えてみる。 これは第一巻で考え残した URI の表記を考える上でも必要だ。

これはそんなに大変ではない。 現在言われているホスト名やドメイン名の根拠になっているの はおそらく RFC952 でそれによると

なお RFC1123 のセクション 2.1 を見ると最初の文字は数字でも構わないように 変更したとあるが、ちょっとこれはわからない。 参照した規約が古すぎるのかも知れないが、とにかくこのルールを正規表現で 書くと

[a-z](([a-z0-9-])*[a-z0-9])?

となる。ただ上の正規表現には長さの規則までは反映できていない。 長さの規則までをいれると

[a-z]([a-z0-9-]{0,22}[a-z0-9])?

こんな感じになる。もちろん数字の部分を

[a-z]([a-z\d-]{0,22}[a-z\d])?

としてもいい。 それで、ピリオドを区切り記号として連ねたものがインターネットで ユニークにきまるホスト名になる。いや、正確には IP アドレスに きちんと対応したマシン名と言うべきか。

([a-z]([a-z\d-]{0,22}[a-z\d])?)(\.([a-z]([a-z\d-]{0,22}[a-z\d])?))*

となる。この正規表現を見ると 最初のホスト名だけの

[a-z]([a-z\d-]{0,22}[a-z\d])?

までが含まれる。しかし、IP アドレスに対応したマシン名という観点からだと localhost があるから、これでも一応矛盾しない。 localhost だけでちゃんと 127.0.0.1 に対応している。 現に web サーバにアクセスできるの で URI のホスト名部分という点ではこれで十分かも知れない。

名前表記にしたときには必ずしも区切りのピリオドの個数は IP アドレスのそれ と対応はしない。localhost がその代表例だし、sah1.ye なんてホストもある。 これはイエメンの電話会社の web サーバで、実際に アクセスしてページを見ることができる。

IP アドレス

ここでついでに IP アドレスを表すことを考えよう。 誰しも思うのは

\d+\.\d+\.\d+\.\d+

だろうが、これではまずいことは第一巻で指摘した。 とりあえず、第一巻では

\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

でとめたが、これでも少々まずい。もちろん、パケットのヘッダから 読み出した IP アドレスを相手にするのなら最初の正規表現でも 問題はないが、人間が入力したものや途中で改竄が予想されるような データに含まれている IP アドレスならまずい場合もある。

2 番目の正規表現がまずいのは

999.999.999.999

なんていうものにもマッチしてしまうからだ。さらに、

207.012.034.001

なんていうものにもマッチしてしまう。 上の場合は

207.12.34.1

と表記されるところである。 ここでのポイントはピリオドで区切られた数字がきちんと範囲内に 収まっているかどうかということとちゃんと十進数を表しているか ということにつきる。

数を表す

ばかばかしい

\d+

で十分ではないかと、思う人は問題の本質がわかっていない。 実際問題として上の表記ですむ場合もあるが、そうでない場合もある。 例えば、第一巻で扱ったアクセスログファイルの時刻のフィールドの時間は

\d{2}

であった。つまり、1 時はこの表記法に従えば 01 と書かれることになる。 しかし、例えば 2 桁以下の自然数を学校の数学や算数で扱うように表すとした 場合には上の表記法ではいけない。 いや、既に 8 進数との混同が生じるのでコンピュータがらみであっても この表記法は問題がある。

そこでまず、99 以下の自然数を表す正規表現を考えてみることにする。 9 以下の自然数なら話は楽である。

[1-9]

ですむ。次に 2 桁の数の場合だが

[1-9](\d)?

となるだろう。999 以下なら

[1-9](\d){0,2}

となるはずだ。ここまでは数分考えれば誰でも出来る。

次に 199 以下の自然数ならどうなるだろうか? この程度なら例えば

(1\d{2}|[1-9]\d?)

ぐらいでも良い。

1\d{2}

が、100 から 199 まで表してくれるし、残りが 99 以下の数を 表してくれる。

では, 254 以下の自然数ならどうなるだろうか? 列挙式でやるのなら

(25[0-4]|2[0-4]\d|1\d{2}|[1-9]\d?)

となる。 もっとも、数の範囲ということであれば、普通の自然数が表せた段階で 数値としてのチェックをするとかを考えた方が自然だし、 能率的である。つまり、各行に数の書いてあるファイルがあってその中から 254 以下の自然数のみをプリントしたい場合には

#!/usr/bin/perl 

while( <> ){
  print if( /^(25[0-4]|2[0-4]\d|1\d{2}|[1-9]\d?)$/ );
}

というスクリプトを用いる代わりに

#!/usr/bin/perl 

while( <> ){
  chop;
  print "$_\n" if( /^[1-9]\d*$/ && $_ <= 254 );
}

とした方が自然である。 ともかく、強引に正規表現で数値の範囲を表すことにして それで IP アドレスを表すことにすると

(25[0-4]|2[0-4]\d|1\d{2}|\d\d?)\.(25[0-4]|2[0-4]\d|1\d{2}|\d\d?)\.(25[0-4]|2[0-4]\d|1\d{2}|\d\d?)\.(25[0-4]|2[0-4]\d|1\d{2}|[1-9]\d?)

となる。とはいっても、これにマッチするアドレスが本当に存在する ホストかどうかはわからない。nslookup をしても引けない場合もあるし、 pingtraceroute も効かない場合もある。 例えば FreeBSD 3.4-RELEASE だとホストを traceroute をかけても 不可視にする機能がある。ping もフィルターをかけて通さないように出来る。 与えられた一見 IP アドレスに見える文字列が本当に実在するホストを 指しているかどうかははっきりとは言えないわけである。 つまり、この点から言えばパケットのヘッダから読み出せる以上の 確定的なことは何一つ言えないということである。

HTTP URL

いよいよ本題に入る。 その前に、ちょっとしたコーディングスタイルの問題について触れる。 ちょっと複雑な正規表現を扱うとすぐに長くなってしまうことはいままでも たびたび経験した。横スクロールできるブラウザやエディタを使っているのなら いいが、それでもコード全体の中で正規表現を見れないと言うことは 思わぬミスを誘発する恐れがある。 こう言った場合には、少しずつ部分にわけて扱うのがプログラミングの 基本的な態度である。幸いなことに perl には正規表現の中に変数を 埋め込めるので、少しずつ正規表現を組み立てていくことが可能である。 これが sed だと、まともに正規表現を書かなければならず大変である。

具体的に説明する。例えば、abcd を検索するのに

#!/usr/bin/perl

$pattern = 'abcd';

while( <> ){
  print if /$pattern/o;
}

とすることも perl では出来る。これを利用しよう。ここで

/$pattern/o

とある。これだけをちょっと説明しよう。変数を使うと基本的に 実行中に変数が変化するものと見なされてパターンマッチを行うごとに 正規表現の解釈を見直す。これは本当に変数の内容が変化するのなら 必要なことだが、上のような場合にはその都度正規表現の内容を見直す 必要はない。perl はもちろんこれをこれをある程度まで自動的に行うが はっきりとその旨伝えているのが、/ / のあとの o である。 これは最初に一回だけ正規表現を解釈したらあとはそのままそれで パターンマッチを行うことを意味する。 これで、パフォーマンスはかなり変わるので正規表現を短くするためだけに 変数を使うのならこれを是非指定した方が良い。

さて、この機能を使うと HTTP URL は

http://$hostport(/$hpath(\?$querys)?)?

となる。これによって、適切に $hostport, $hpath, $querys が指定できれば

http://www.hogehoge.co.jp
http://www.hogehoge.co.jp:3128/cgi-bin/bbs.cgi
http://www.hogehoge.co.jp/cgi-bin/bbs.cgi?name=foo&email=var

等といったことが表せる。 $hostport

$host(:\d+)?

で表せる。なお、ポート番号の範囲は考えないことにした。 $hostFQD 表記されたホスト名か IP アドレスである。 $host は前の部分を使えば良いから

([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*

でよい。前にホスト名を考えた場合には長さもこめて考えたが今回は 長さは考えないことにする。ただし、HTTP URL の場合には IP アドレスも可能なので、まとめると $host

((([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)|(\d+\.\d+\.\d+\.\d+))(:\d+)?

である。すると結局

$hostport = '((([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)|(\d+\.\d+\.\d+\.\d+))(:\d+)?';

となる。次が $hpath である。

$hsegment(/$hsegment)*

$hsegment というのは要するにファイル名やディレクトリ名だが これに使える文字は

[a-zA-Z0-9;:@&=$_.+!*'(),-]

という文字クラスの文字かないしは %AF などといった表記のものである。 これらをまとめると

([a-zA-Z0-9;:@&=$_.+!*'(),-]|(%[\da-fA-F][\da-fA-F]))

となる。結局 $hsegment

$hsegment = q#([a-zA-Z0-9;:@&=$_.+!*'(),-]|(%[\da-fA-F][\da-fA-F]))*#;

となる。 最後の $querys$hsegment と同じである。 この場合には解釈できるか、正しいかということは問題にならない。例えば、

http://localhost/cgi-bin/bbs.cgi?aaa&bbb=&=ccc

なども認められる。 以上をまとめて具体的なスクリプトにすると

#!/usr/bin/perl

$hostport = '((([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)|(\d+\.\d+\.\d+\.\d+))(:\d+)?';
$hsegment = q#([a-zA-Z0-9;:@&=$_.+!*'(),-]|(%[\da-fA-F][\da-fA-F]))*#;
$querys = "$hsegment";
$hpath = "$hsegment(/$hsegment)*";
$httpurl = "http://$hostport(/$hpath(\\?$querys)?)?";

while( <> ){
  s#$httpurl#<a href="$&">$&</a>#goi;
  print;
}

となる。これはファイル中の HTTP URL とおぼしき文字列に 全部リンクを張る。なお、上のスクリプトの中の $& というのは マッチした文字列に対応する。あと、大文字小文字を区別する必要はないので そのフラグをつけている。 実際にはそのようにすると、$hsegment

$hsegment = q#([\w;:@&=$.+!*'(),-]|(%[\dA-F][\dA-F]))*#;

などと少し短くなる。

/PATTERN/ について

ここで /PATTERN/ だとか s/PATTERN1/PATTERN2/ だとかいったものに ついてちょっと微妙な部分を考えてみる。 つまり、// の間には固定された正規表現ばかりではなくて perl の変数も入れて使えると言うことを指摘した。つまり、 前のサンプルにあったように

#!/usr/bin/perl

$pattern = 'abcd';

while( <> ){
  print if /$pattern/o;
}

などといった使い方が出来るということである。 同様に、abcdefgh に置換するには

#!/usr/bin/perl

$pattern = 'abcd';
$replace = 'efgh';

while( <> ){
  s/$pattern/$replace/og;
  print;
}

といった使い方が可能である。 これは、つまり // の間はダブルクォーテーションで囲まれた 文字列と同じ扱いを受けると言うことを意味する。 問題になるのは文字クラス指定などでドル記号などを含む場合である。 perl の 予約変数と衝突する可能性がある。例えば、

[&%$'()-] 

などという正規表現の場合 $' が予約変数なのでこれが変数展開されて 意図した結果にならないことがある。例えば、

#!/usr/bin/perl

while(<>){
  print if /^[&%$'()-]+$/;
}

というスクリプトに

abcdef
()()()()()
$$$$$$
'''''

というデータファイルを読ませると結果は

[ageha@vine re]$ perl t3.pl t3
()()()()()

となってしまう。もしも、ドルマークとシングルクォートを きちんと認識させるとしたらバックスラッシュを施して

[&%\$'()-] 

等とするか、最初から変数を使うことにして

#!/usr/bin/perl

$pat = q#[&%$'()-]+#;

while(<>){
  print if /^$pat$/;
}

とする。通常はシングルクォートで括ればドルマークは文字通り 扱われるが、上の場合にはさらにシングルクォートも入っているので 一般シングルクォートの

q#  #

を使った。これが上の URI へリンクをはるスクリプト中で

$hsegment = q#([\w;:@&=$.+!*'(),-]|(%[\dA-F][\dA-F]))*#;

などと q#PATTERN# を使っている理由である。

それから、

s/PATTERN1/PATTERN2/;

に関連して知っておくと便利なことを 2, 3 書いておこう。 通常は上のような使い方をするが、置換演算子は括弧類を使って

s{PATTERN1}[PATTERN2];

などとも書ける。当然、

s<PATTERN1>(PATTERN2);

なんて書き方も可能である。さらにこのような書き方をした場合には

s{PATTERN1}  # Comment1
 [PATTERN2]; # Comment2

などとコメントも入れられる。

拡張正規表現

ここでちょっとだけ大事な話をする。 いい加減に正規表現が見づらくなると間違いをしやすくなるし、 しかも、間違いがどこになるのか発見するのが大変になる。 こうなって来ると正規表現を書くのも、一種のプログラムを書くのと 同じになって来る。 一般のプログラミング言語なら行に対する制限も厳しくはないし、 またコメントもいれることが出来る。それは正規表現では無理なのだろうか?

perl の正規表現に限ればそれは可能である。 perl には拡張正規表現の機能がある。 これは perl4 には実装されておらず、 perl5 から使える。 その話をしてみよう。 詳しいことは perlre マニュアルページにある。

拡張正規表現では基本的には空白文字と # の扱いが変わって来る。 まず、すべての空白文字は無視される。ただし、文字クラスの中ではそのまま 解釈される。どうしても空白として解釈させたければバックスラッシュで エスケープさせる。perl の構文におけるコメントの 役割を果たす。これも文字通り解釈させる場合にはバックスラッシュを使う。

例をあげる。 第一巻で扱った httpd のアクセスログを扱うスクリプトは 次のように書いても良い。

#!/usr/bin/perl

while( <> )
{
  if(
     # 以下は拡張正規表現の例
     # 空白およびタブ・改行は無視されるので空白は \ としてエスケープする。
     m{
       (\S+)\        # リモートホスト
       (\S+)\        # ログイン名
       (\S+)\        # ユーザ名
       \[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2}\ [+-]\d{4})\]\     # アクセス時刻
       "(.*)"\       # リクエストヘッダの一行目
       (\d{3})\      # ステータス
       (\d+|-)       # 転送バイト数
     }x
     )
  {
    ($rhost, $rlog, $ruser, $time, $req, $stat, $byte)
      = ($1, $2, $3, $4, $5, $6, $7);

    print <<"records";
HOST: $rhost
RLOG: $rlog
RUSER: $ruser
TIME: $time
REQ: $req
STATUS: $stat
BYTES: $byte
-------------------------
records
   }
}

上のように拡張正規表現にはコメントも使えるし、正規表現を複数行に わけて記述できる。これは、場合によってはコードが見やすくなるので便利だ。 注意することは空白をいれるなら上のようにバックスラッシュでエスケープ しないといけないということである。 さらに、拡張正規表現であることを示すために

m/ /x

などと x フラグを使用することも必要になる。

同様に、上の HTTP URL の場合だと

#!/usr/bin/perl

while( <> ){
     s {
       http://          # スキーム名
         # ホスト名の部分
         (
          # 文字でホスト名を FQD 表示した場合
          (([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)
           |
          # IP アドレス
          (\d+\.\d+\.\d+\.\d+)
         )
         (:\d+)?  # ポート番号 -- オプショナル
         ((
          # 以下はパス名の部分
          /([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          (/([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*)*
          )
          (
          # 以下はクエリー
          \?
          ([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          )?)?
    }{<a href="$&">$&</a>}gix;
  
  print;
}

となる。 なお、この拡張正規表現の経緯については Jeffrey E. F. Friedl の『正規表現』 に詳しい(同書 p.230 参照のこと)。 perl のニュースグループで Jeffrey が長い正規表現を人にわかりやすく 説明するために適当に改行をいれ、コメントをつけた記事を投稿した。 それを見ていた Lary Wall が perl にこの機能を実装したらしい。 実際に、空白改行を無視して構わない場合にはこの機能は本当に ありがたい。長い正規表現をわかりやすく記述できる。 ただし、この機能はむろんほかの正規表現を認識する UNIX コマンドでは使えな いし、perl4 でも使えない。

マッチした部分の取り出し

マッチした部分を取り出すことを考える。 基本的には $1 などを使えば取り出せる。上の

#!/usr/bin/perl

while( <> )
{
  if(
     # 以下は拡張正規表現の例
     # 空白およびタブ・改行は無視されるので空白は \ としてエスケープする。
     m{
       (\S+)\        # リモートホスト  $1
       (\S+)\        # ログイン名  $2
       (\S+)\        # ユーザ名 $3
       \[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2}\ [+-]\d{4})\]\   # アクセス時刻 $4
       "(.*)"\       # リクエストヘッダの一行目 $5
       (\d{3})\      # ステータス $6
       (\d+|-)       # 転送バイト数 $7
     }x
     )
  {
    ($rhost, $rlog, $ruser, $time, $req, $stat, $byte)
      = ($1, $2, $3, $4, $5, $6, $7);

    print <<"records";
HOST: $rhost
RLOG: $rlog
RUSER: $ruser
TIME: $time
REQ: $req
STATUS: $stat
BYTES: $byte
-------------------------
records
   }
}

というスクリプトで $1 から $2 までを使っているが、これがそうである。 番号は括弧に対応している。コメントをみれば対応がわかるだろう。 やはりこういう場合には拡張正規表現はありがたい。 さらに、マッチした文字列を取り出すのに、 perl に詳しい人なら、第一巻から出ているこのコードをいかにも 不細工なコードと思うはずだ。 このコードは次のようにも書ける。

#!/usr/bin/perl

while( <> )
{
  if(
     ($rhost, $rlog, $ruser, $time, $req, $stat, $byte) = 
     # 以下は拡張正規表現の例
     # 空白およびタブ・改行は無視されるので空白は \ としてエスケープする。
     m{
       (\S+)\        # リモートホスト $1
       (\S+)\        # ログイン名 $2
       (\S+)\        # ユーザ名 $3
       \[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2}\ [+-]\d{4})\]\     # アクセス時刻 $4
       "(.*)"\       # リクエストヘッダの一行目 $5
       (\d{3})\      # ステータス $6
       (\d+|-)       # 転送バイト数 $7
     }x
     )
  {
    print <<"records";
HOST: $rhost
RLOG: $rlog
RUSER: $ruser
TIME: $time
REQ: $req
STATUS: $stat
BYTES: $byte
-------------------------
records
   }
}

まず、

($str1, $str2) = m{(PATTERN1)(PATTERN2)}

において m{ .... } はマッチすれば 配列コンテキストでは 配列 ($1, $2, ..) を返す。 こんどは条件文としてスカラーコンテキストで評価されるため 配列の個数を返す。マッチしていれば当然 0 ではない。 したがって、マッチすれば条件が真になる。 これは、慣れれば読みやすいコードである。 もちろん、書き方は色々あるから、自分の好きな書き方を すれば良い。

さて、文字列からマッチした部分を取り出す場合には上のようにすれば良いことが わかった。括弧の対応に応じて $1 だとかを用いれば良い。 問題は括弧が入れ子になっている場合である。 それを考えるために、次のようなデータを用意した。

aaa[bbb[ccc]ddd]eee
[aaa[bbb]ccc]
[aaa]

また、次のスクリプトを考える。

#!/usr/bin/perl

while( <> ){
  if ( /(.*)(\[(.*)(\[(.*)\])(.*)\])(.*)/  ){
    print "\$1 = $1, \$2 = $2, \$3 = $3, \$4 = $4, \$5 = $5, \$6 = $6, \$7 = $7\n";
  }
}

どこがどう取り出されるかを試すものである。括弧は 7 対ある。何が何に 対応するかわかるだろうか? 実行した結果は

[ageha@vine re]$ perl p1.pl p
$1 = aaa, $2 = [bbb[ccc]ddd], $3 = bbb, $4 = [ccc], $5 = ccc, $6 = ddd, $7 = eee
$1 = , $2 = [aaa[bbb]ccc], $3 = aaa, $4 = [bbb], $5 = bbb, $6 = ccc, $7 = 

である。左の括弧は 1 番目なので $1 になる。 次の括弧は 2 番目なので $2 である。ここまでは良い。 3 番目は 2 番目の括弧の中の 1 番目である。 4 番目は 2 番目の括弧の中の 2 番目さらに、 2 番目の括弧の中の 2 番目の括弧の中の括弧は 5 番目である。 2 番目の括弧中の 3 番目の括弧は 6 番目となる。 拡張正規表現を使ってわかりやすく書くと

#!/usr/bin/perl

while( <> ){
  if (
      m{
        (.*)  # $1
        (\[   # $2
         (.*) # $3
         (    # $4
          \[
          (.*)# $5
          \]
         )
         (.*) # $6
         \])
        (.*)  # $7
        }x
      ){
    print "\$1 = $1, \$2 = $2, \$3 = $3, \$4 = $4, \$5 = $5, \$6 = $6, \$7 = $7\n";
  }
}

となる。それでは、ちょっと難しい問題を考えてみよう。 http URL を表すスクリプト

#!/usr/bin/perl

while( <> ){
     s {
       http://          # スキーム名
         # ホスト名の部分
         (
          # 文字でホスト名を FQD 表示した場合
          (([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)
           |
          # IP アドレス
          (\d+\.\d+\.\d+\.\d+)
         )
         (:\d+)?  # ポート番号 -- オプショナル
         ((
          # 以下はパス名の部分
          /([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          (/([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*)*
          )
          (
          # 以下はクエリー
          \?
          ([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          )?)?
    }{<a href="$&">$&</a>}gix;
  
  print;
}

このなかでホスト名または IP アドレスは何を使うと抜き出せるだろうか? またポート番号は? 答えは次のスクリプトを実行してみればわかる。

#!/usr/bin/perl

while( <> ){
     if(
        s {
       http://          # スキーム名
         # ホスト名の部分
         (
          # 文字でホスト名を FQD 表示した場合
          (([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)
           |
          # IP アドレス
          (\d+\.\d+\.\d+\.\d+)
         )
         (:\d+)?  # ポート番号 -- オプショナル
         ((
          # 以下はパス名の部分
          /([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          (/([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*)*
          )
          (
          # 以下はクエリー
          \?
          ([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*
          )?)?
         }{<a href="$&">$&</a>}gx
       ){
          print "host = $2\n";
          print "ipaddr = $8\n";
          print "port = $9\n";
          print "path = $11\n";
          print "query = $17\n";
       }
  
  print;
}

perl までしか使えなかったが、 現在の perl5 はもとより perl4 の最近のバージョンでなら $10 以上も 使うことが出来る。

余計な括弧

上のように何かを取り出そうとするときに括弧は必要になる。 また、

([a-zA-Z0-9;:@&=_.+!*'(),\$-]|(%[\da-fA-F][\da-fA-F]))*

などの指定のようにグループ化するために必要な括弧もある。 なるべくなら括弧はあった方が正規表現が見やすくなるので良いが、 実際は結合の強さが決まっているので不要な括弧もある。

例えば、上の正規表現だと

([a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*

でもよい。この括弧をはずとどうなるだろうか次のスクリプトを試してみよう。

#!/usr/bin/perl

open(F, "u") || die "open: $!";
@arr = <F>;
close(F);

$btime = time;
for( $i = 0 ; $i < 1000000 ; $i++ ){
  foreach( @arr ){
        m {
       http://          # スキーム名
         # ホスト名の部分
         (
          # 文字でホスト名を FQD 表示した場合
          (([a-z]([a-z\d-]*[a-z\d])?)(\.([a-z]([a-z\d-]*[a-z\d])?))*)
           |
          # IP アドレス
          (\d+\.\d+\.\d+\.\d+)
         )
         (:\d+)?  # ポート番号 -- オプショナル
         ((
          # 以下はパス名の部分
          /([a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*
          (/([a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*)*
          )
          (
          # 以下はクエリー
          \?
          ([a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*
          )?)?
         }gx;
  
#  print;
  }
}
$etime = time;
$dtime = $etime - $btime;

print "\nTime $dtime seconds.\n";

これは Jeffrey Friedl のベンチマークを真似たものである。 このスクリプトでは上で指摘した部分の括弧をはずしてある。 同様に、括弧をはずす前のスクリプトも同じようにして 1000000 回 ループで回してある。データファイルの u というのは

http://localhost/cgi-bin/che.cgi
http://127.0.0.1/index.html
http://bahnhof.virtualave.net/cgi-bin/che.cgi
http://www.ecc.u-tokyo.ac.jp/cgi-bin/hoge.cgi?name=foo&email=root@localhost
http://www.hogehoge.co.jp/bbs.cgi?aaa=%AF%EF%20&bbb=abc+def&ccc=%32%85%76
http://[][][][]{}{}{}{}/@@@@@?????$$$$$/,,,,..../*****.cgi
http://www.foo.var.com:3128/some/path/bbs.cgi?name=hoge&email=hogehoge

というもので、サイズは 387 バイト。これを 1000000 回まわすのだから、 単純に行換算で 387 MB のファイルを走査したことになる。 何回かやってみたところ同じデータで概ね括弧をはずす前が 300 秒 括弧をはずしたあとが 280 秒かかり差は 20 秒である。 絶対的な時間を書いても CPU のパワーの違いなどで他の環境では この数字そのものはあまり参考にはならないが、この差がもはや無視できる 差ではないことは容易にわかるはずだ。上のような簡単な構造の ファイルですらこれだけの差がつく。 perl の場合には括弧をつけるとその分、あとで使われるのではないかというこ とでいちいちコピーする。括弧が 20 対あればそれだけコピーを内部でとってい る。単純に 20 対の括弧の中に 1 バイトずつあったとしても上のファイルが 7 行あるから一回のループでコピーするのは 140 バイトである。それが 1000000 回あるから 140 MB 分のデータのコピーをメモリ上で行ったことになる。

とは言っても、どうしてもグルーピングしないといけない場合もある。 perl5 にはこのような場合のために余計なコピーを作らずに グルーピングするための正規表現がある。 例えば、

([a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*

はグルーピングだけ出来れば十分である。 そのために

(?:    )

というグルーピング専用の括弧がある。 これを使えば上の正規表現は

(?:[a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*

となる。これを使って最後のスクリプトを書き換えてみる。

#!/usr/bin/perl

open(F, "u") || die "open: $!";
@arr = <F>;
close(F);

$btime = time;
for( $i = 0 ; $i < 1000000 ; $i++ ){
  foreach( @arr ){
        m {
       http://          # スキーム名
         # ホスト名の部分
         (?:
          # 文字でホスト名を FQD 表示した場合
          [a-z](?:[a-z\d-]*[a-z\d])?(?:\.[a-z](?:[a-z\d-]*[a-z\d])?)*
           |
          # IP アドレス
          \d+\.\d+\.\d+\.\d+
         )
         (?::\d+)?  # ポート番号 -- オプショナル
         (?:(?:
          # 以下はパス名の部分
          /(?:[a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*
          (?:/(?:[a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*)*
          )
          (?:
          # 以下はクエリー
          \?
          (?:[a-zA-Z0-9;:@&=_.+!*'(),\$-]|%[\da-fA-F][\da-fA-F])*
          )?)?
         }gx;
  }
}
$etime = time;
$dtime = $etime - $btime;

print "\nTime $dtime seconds.\n";

出来る限り不要な括弧は取り除き、グルーピングでやむを得ず使うものには (?: ) を使った。 同じファイルを処理して同じ回数だけループで回したら 結果は 200 秒であった。最初のに比べて 100 秒の差がついたことになる。 この 100 秒の差はもう圧倒的といっても良い。 走査対象のファイルが複雑だったりした場合にはこれ以上の 差がつくかも知れない。もちろん、置換などを伴う場合にはもっと ひどいことになり得る。 380MB のファイルを走査して違いが 100 秒なら大したことはない という考えもあるが、使う計算機の資源も大違いになる。 一人で独占して使えるコンピュータなら何をしようと構わないが、 複雑な正規表現の入ったスクリプトを web hosting サービスのサーバで 動かされた日には他のユーザも管理者もたまったものではない。 上のような複雑な URI 走査スクリプトを、ログファイルのサイズが 一日 300 MB を軽く超えるような大規模な web サーバ上のアクセスログファイ ルにかけた場合には、もう犯罪行為といってもよい。 自分の必要性にあわせた最善のチューニングをスクリプトに 施さないといけない。

その点ではカール掲示板のコード

#!/usr/bin/perl

open(F, "u") || die "open: $!";
@arr = <F>;
close(F);

$btime = time;
for( $i = 0 ; $i < 1000000 ; $i++ ){
  foreach( @arr ){
    m/(https?|ftp|news):\/\/([\w|\!\#\$\%\&\'\(\)\=\-\^\`\\\|\@\~\[\{\]\}\;\+\:\*\,\.\?\/]+)/g;
  }
}
$etime = time;
$dtime = $etime - $btime;

print "\nTime $dtime seconds.\n";

は優秀である。同じ条件で 180 秒しかかからなかった。 これは CPU の占有時間も短いことを意味する。

$Id: re2.html,v 1.3 2000/05/07 21:10:03 ageha Exp $