index
関数
目次
|
|
トップへ
sub my_flock { my %lfh = (dir => './lockdir/', basename => 'lockfile', timeout => 60, trytime => 10, @_); $lfh{path} = $lfh{dir} . $lfh{basename}; for (my $i = 0; $i < $lfh{trytime}; $i++, sleep 1) { return \%lfh if (rename($lfh{path}, $lfh{current} = $lfh{path} . time)); } opendir(LOCKDIR, $lfh{dir}); my @filelist = readdir(LOCKDIR); closedir(LOCKDIR); foreach (@filelist) { if (/^$lfh{basename}(\d+)/) { return \%lfh if (time - $1 > $lfh{timeout} and rename($lfh{dir} . $_, $lfh{current} = $lfh{path} . time)); last; } } undef; } sub my_funlock { rename($_[0]->{current}, $_[0]->{path}); } # ロックする(タイムアウトあり) $lfh = my_flock() or die 'Busy!'; # アンロックする my_funlock($lfh);複数のプロセスが同時にある
1つ のファイルを読み書きする可能性 がある場合,排他制御をしなければなりません.排他制 御をする方法はいくつかありますが,このスクリプトは次の方針に基づいています.
- どんなプラットフォームでも使えること
- 異常なロック状態を回避できること
排他制御をする方法として
や flock
関数を使う方法がありますが,これらの関数は プラットフォームによってはサポートされていません.したがって,1 を満たすため にはこれらの方法を使うことはできません.それ以外の方法としては, symlink
関数を使う方法と mkdir
関数を使う方法が考えられます.次に 2 につい てですが,異常なロック状態とは,あるプロセスがロックした状態のまま何らかの原 因で死んだ場合に,ロックが解除されずに残ってしまった状態のことです. rename
関数flock
を使っている場合は,ロック状態でプロセスが死んだとき自動的にロックが解除されますので,異常なロック状態は起こ りません.しかし,symlink
やmkdir
,rename
などを使う場合にはスクリプト側での 対処が必要になります.具体的にどのように対処するかですが,ロック状態がある一定の時間を経過して いた場合には異常と判断し,他のプロセスがロック状態を解除してもよいことにしま す.実はここに落とし穴が存在します.排他制御をする方法としてなぜ
symlink
やmkdir
,rename
を使うのか? それはこれらの関数が,ロックできるかどうかのテストと実際に ロックする操作を同時に行なうことができる atomic な関数であるからです. 話を戻して,異常なロック状態を解除するときのことを考えます.たとえば,mkdir
を使ったロックの方法において,異常なロック状態のときにロッ クを解除するには,次のようなスクリプトになります.rmdir($lockdir) if (time - (stat($lockdir))[9] > 60);ロック状態が
60秒 以上経過していた場合にはロックを解除すると いうスクリプトですが,これがsymlink
やmkdir
,rename
のときと違って,ロックを解除するかどうかの判断と実際にロッ クを解除する操作を同時に行なっているわけではないということが問題となります. 具体的に何がまずいのかというと,正常なロック状態も解除して しまうことがあるということです.それは次のような場合です.
プロセスA プロセスB プロセスC 異常と判断 異常と判断 ↓ ↓ ↓ ↓ ロック解除 ↓ ↓ ↓ ↓ ロック ↓ ロック解除 ↓ 複数のプロセスでロック状態が異常であると判断し,そのうちの
1つ がロックを解除したことにより,別のプロセスがロックしたにもか かわらず,先ほどロック状態が異常であると判断したプロセスによってこの正常なロッ クを解除されてしまう可能性があります.この方法の問題点は,異常なロック状態を解除する操作が正常なロック状態をも 解除できてしまうことにあります.逆に言えば,異常なロック状態を解除する操作に よって正常なロック状態を解除できなければ問題ないわけです.そのためにはどうす ればよいのか? 答えはロック状態が常に変化していけば よいということです.そして,これを実現するのに都合がよいのが
rename
による方法になります.最初のスクリプトで説明しますと,ロックファイルが lockfile という 名前のときがロックが解除されている状態で,lockfile987654321 のよう に後ろに作成時刻がついた状態がロック状態になります.こうすることで 先ほどの例で,
プロセスB によってプロセスC のロック が解除されてしまったという状況を回避することができます. なぜなら,プロセスC によってrename
されたロックファ イルの名前はすでにプロセスB が知っている名前とは違っているから です.最初のスクリプトでは一旦ロックを解除するのではなく,異常なロック状態を 解除しつつ,新たなロック状態へと移行させています.スクリプトの注意点としては,あらかじめロック用のディレクトリとファイルを 用意しておくこと,ディレクトリに書き込み属性をつけておくこと,
dir
の値には最後に/
などのデリミタをつけておくこ とです.のように呼び出すことでパラメータを変更できます.また, $lfh = my_flock(basename => 'lockfileA');
my_flock()
はロックに失敗(タイムアウト)するとundef
を返します. ロックするまでブロックしたい場合には次のように書きます.# ロックする(タイムアウトなし) 1 while (not defined($lfh = my_flock()));最後に,ファイルを読み込み,それを加工した上で書き込む場合の安全な排他制 御の手順を書いておきます.
- ロックする
- ファイルを読み込む
- 一時ファイルに書き込む
- 一時ファイルを元ファイルにリネームする
- アンロックする
トップへ# ファイル $file の中身を逆順に表示する $bufsize = 1024; open(FILE, "< $file"); binmode(FILE); $size = (-s FILE) / $bufsize; $pos += $size <=> ($pos = int($size)); while ($pos--) { seek(FILE, $bufsize * $pos, 0); read(FILE, $buf, $bufsize); $buf .= $buf_tmp; ($buf_tmp, @lines) = $buf =~ /[^\x0D\x0A]*\x0D?\x0A?/g; pop(@lines); foreach (reverse @lines) { print $_; print "\n" if $_ !~ /[\x0D\x0A]$/; } } close(FILE); print $buf_tmp;このスクリプトはファイルを
ずつ 読み込んで逆順に表示するので,ファイル全体を一度に読み込む方法に 比べて少ないメモリで実行させることができます. $bufsize
バイト
への代入文にある $size
はファイルテスト演算子の -s
1つ でファイルサイズを返します.にはファイルサイズを $pos
で割って切り上げた値が代入されます.切り上げに関しては 「数字を切り上げる」を参照してください. $bufsize
は while
ブロックに分けてファイルを読み込んで処理するということをやっています. $pos
回
には $buf
ずつ 読み込んだファイルの一部が代入されます.ファイルの中身を逆順に表示する ためには,まずは $bufsize
バイトの中身を行ごとに分ける必要が あります.それを行なっているのが $buf
の部分になります. この正規表現は,改行コード以外の文字が $buf =~ /[^\x0D\x0A]*\x0D?\x0A?/g;
0文字 以上続き,その後の 改行コードまでを表わしています.つまり,これで1行 分を 取り出しているわけです.改行コード以外の文字が0文字 以上で あるので空行にもマッチします.また,改行コードの部分の正規表現は改行コードが \x0D?\x0A?
でも \x0D\x0A
でも \x0D
でもよいことはもちろ んのこと,ファイルの最後が改行で終わっていない行だった場合にもマッチします. ここまでの話ですでにお気づきの人もいるかもしれませんが,この \x0A
1行 分にマッチする正規表現は,実は空文字列にも マッチします.そして,それは必ずの最後で マッチさせる文字が何もない状態で一度だけ起こります.したがって,この無意味な 空文字列を削除するために,次の行で $buf
しています. pop(@lines);
の中身を行ごとに分けるには $buf
を使って, split
関数とすればいいのでは ないかと思うかもしれませんが,この方法では split(/\x0D\x0A|\x0D|\x0A/, $buf);
の 最後に空行があった場合にまずいことになります. $buf
の split
関数第 3引数 を省略すると,split
した結果の最後が空文字列であった 場合には自動的に削除されます.つまり,最後に空行が連続する文字列のようなものを "foo\nbar\n\n\n"
split
するとしか残らないため,本来 ('foo', 'bar')
となってほしかった最後の空行がなくなってしまいます. ('foo', 'bar', '', '')
そこで最後の空文字列を自動的に削除させないために,
第 3引数 にのように負数を指定すればいいのではないかと思うかも しれませんが,これでもまだうまくいきません.例えば, split(/\x0D\x0A|\x0D|\x0A/, $buf, -1);
を "foo\nbar\n"
split
すると,今度はのように最後の改行コードの後ろの空文字列が削除されずに残ってしまいます. そこで,これに対処するために最後が空文字列であった場合には削除するように, ('foo', 'bar', '')
とする手があります. しかし,これを行なうことによって,ちょうど pop(@lines) if $lines[-1] eq '';
ずつ区切った前後が改行コードであった場合には 必要な空行まで削除してしまいます.そのためさらに, $bufsize
read
の後にを入れる必要があります. これで正規表現を使った方法とほぼ同じ動作をするようになります. ただ,私がベンチマークをとって調べたところ,正規表現を使った方法の 方が速かったためそちらを採用しました. $buf_tmp = "\n" if $buf_tmp eq '';
トップへ# ファイル $file の最後の最大 $n行だけ表示する $bufsize = 1024; open(FILE, "< $file"); binmode(FILE); $size = (-s FILE) / $bufsize; $pos += $size <=> ($pos = int($size)); while ($pos--) { seek(FILE, $bufsize * $pos, 0); read(FILE, $buf, $bufsize); $buf .= $buf_tmp; ($buf_tmp, @lines) = $buf =~ /[^\x0D\x0A]*\x0D?\x0A?/g; pop(@lines); unshift(@tail, @lines); last if @tail >= $n; } close(FILE); unshift(@tail, $buf_tmp); @tail = @tail[-$n .. -1] if @tail > $n; foreach (@tail) { print $_; }このスクリプトの基本は 「ファイルの中身を逆順に表示する」のスクリプトと 同じです.スクリプトの詳細についてはそちらを参照してください.違いとしては
を取り出すことができた時点ですぐに $n
行を抜けるようにしているところです. while
ブロック実際に表示する直前では
の大きさを調べ,もし @tail
よりも大きければ最後の $n
だけを 配列スライスで取り出して代入し直しています. $n
個は範囲演算子と言い, リストコンテキストで実行した場合は範囲演算子の前の 値から後ろの値までのリストを返します.つまり,この場合は ..
というリストになります. 配列の添え字が負数だった場合には後ろから数えた場所になるので,この場合は 配列の最後の (-$n, -$n+1,..., -2, -1)
分ということになります. $n
個
トップへ# ファイル $file から 1行ランダムに選択する srand; open(FILE, "< $file"); rand($.) < 1 and $line = $_ while <FILE>; close(FILE); print $line;このスクリプトではファイル全体をメモリに読み込まない ので少ないメモリで実行させることができます.また, ファイルの行数があらかじめわかっている必要もありません.
ファイル全体に対して
を回すわけですが, while
文1行 ずつ読み込んで実行される部分がwhile
よりも左側の部分です.この部分は2つ の式のand
を取っています.論理演算子 は左側が真の場合に限り右側が評価されます.つまり,この部分はand
を使って次のように書いたものと同じ意味になります. if
文if (rand($.) < 1) { $line = $_; }
特殊変数 は最後に読み込んだファイルの行番号を返します.したがって,この条件が 成立する確率は$.
になります.たとえば, 1/$.
1行目 のときは1/1 ,2行目 のときは1/2 ,3行目 のときは1/3 の確率というようになります. これでなぜランダムに1行 選択できるのかという問題は数学の 問題です.簡単に書きますと,全部で3行 のファイルだった場合に,1行目 が選択されるのは,1行目 で条件が真となり,2行目 と3行目 では条件が偽となる必要があります. したがって,確率は1/1 * (1 - 1/2) * (1 - 1/3) = 1/3 となり, ちゃんと行数で割った確率になります.2行目 が選択されるのは, 条件が2行目 で真で3行目 で偽の場合です.2行目 で真になればそれ以前の条件は無関係だというのはいい ですよね? その結果,確率はやはり1/2 * (1 - 1/3) = 1/3 となり, 行数で割った確率になります.
トップへ# ディレクトリ $dir のサイズ $size を求める use File::Find; find(sub {$size += -s if -f}, $dir); print $size, "bytes\n";このスクリプトは
ディレクトリ 以下の すべてのファイルのファイルサイズの合計を求めています.あるディレクトリ以下の すべてのファイルまたはディレクトリに対して何か処理したい 場合には$dir
標準モジュール のFile::Find
を使うのが簡単です. この関数は find
関数第 2引数 で与えたディレクトリに対して,ファイル またはディレクトリを幅優先で探索し,見つかった ファイルまたはディレクトリをに $_
1つ 代入しては第 1引数 で与えた関数を実行します. 正確には第 1引数 に は関数へのリファレンスを与えます. このスクリプトでは無名関数へのリファレンスを 与えています.これは次のように書いても同じです.# ディレクトリ $dir のサイズ $size を求める(わかりやすく) use File::Find; find(\&wanted, $dir); print $size, "bytes\n"; sub wanted { $size += -s $_ if -f $_; }
はファイルテスト演算子の -s
1つ で ファイルサイズを返します.はディレクトリやシンボリックリンクなどではなく普通のファイルのときに 真となります.幅優先ではなく深さ優先で 処理したい場合には -f
を使います. finddepth
関数
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str の中のタグを削除した $result を作る # $tag_regex と $tag_regex_ は別途参照 $text_regex = q{[^<]*}; $result = ''; while ($str =~ /($text_regex)($tag_regex)?/gso) { last if $1 eq '' and $2 eq ''; $result .= $1; $tag_tmp = $2; if ($tag_tmp =~ /^<(XMP|PLAINTEXT|SCRIPT)(?![0-9A-Za-z])/i) { $str =~ /(.*?)(?:<\/$1(?![0-9A-Za-z])$tag_regex_|$)/gsi; ($text_tmp = $1) =~ s/</</g; $text_tmp =~ s/>/>/g; $result .= $text_tmp; } }このスクリプトの基本は 「自動で URI(URL) のリンクを張る」のスクリプトと 同じです.詳しくはそちらを参照してください.
および $tag_regex
については 「 $tag_regex_
HTMLタグ の正規表現」のスクリプトを 正規表現として使います.また,には $str
HTML文書 全体を入れておきます. 注意が必要な点としましては,XMPタグ やPLAINTEXTタグ を削除した場合には, それまでその中で無効だったタグが有効になってしまう可能性があることです. そのため,XMPタグ やPLAINTEXTタグ を削除するときには, その中の<
をに, <
>
をに変換しています. >
SCRIPTタグ についても同様です.次のようにしてタグの開始 < と終了 > にだけ注目してタグを削除する方法では うまくいかない場合があります.
# $str の中のタグを削除した $result を作る(不完全) ($result = $str) =~ s/<[^>]*>//g;具体的には次のような不具合があります.
<!-- <FOO> --> のようなコメントの<!-- <FOO> を削除してしまう.<FOO BAR=">"> のようにダブルクォートで囲んだ中に > があると,そこをタグの終了と間違って<FOO BAR="> を削除してしまう.<XMP><FOO></XMP> のようにXMPタグ やPLAINTEXTタグ ,SCRIPTタグ の中の一見タグに見える<FOO> も削除してしまう.最初のスクリプトではこのような場合にもうまくいくようになっています. ただし,
HTML文書 として正しく書かれている場合を想定して いますので,< に対応する > がないときなどは予期せぬ動作をすることに なります.もし
BRタグ やAタグ など特定のタグだけは 削除したくない場合には,の後に, 次のようにして $tag_tmp = $2;
を $tag_tmp
に加えるようにすればできます. $result
$result .= $tag_tmp if $tag_tmp =~ /^<\/?(BR|A)(?![0-9A-Za-z])/i;逆に
FONTタグ やIMGタグ など特定のタグだけ 削除したい場合には,の後に, 次のようにして $tag_tmp = $2;
を $tag_tmp
に加えるようにすればできます. $result
$result .= $tag_tmp if $tag_tmp !~ /^<\/?(FONT|IMG)(?![0-9A-Za-z])/i;
モジュール のHTML::TokeParser
,または get_text
メソッドや, striphtml を使っても同じようなことができます. get_trimmed_text
メソッド
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str の中の URI(URL) にリンクを張った $result を作る # $tag_regex と $tag_regex_ は別途参照 # $http_URL_regex と $ftp_URL_regex および $mail_regex は別途参照 $text_regex = q{[^<]*}; $result = ''; $skip = 0; while ($str =~ /($text_regex)($tag_regex)?/gso) { last if $1 eq '' and $2 eq ''; $text_tmp = $1; $tag_tmp = $2; if ($skip) { $result .= $text_tmp . $tag_tmp; $skip = 0 if $tag_tmp =~ /^<\/[aA](?![0-9A-Za-z])/; } else { $text_tmp =~ s{($http_URL_regex|$ftp_URL_regex|($mail_regex))} {my($org, $mail) = ($1, $2); (my $tmp = $org) =~ s/"/"/g; '<A HREF="' . ($mail ne '' ? 'mailto:' : '') . "$tmp\">$org</A>"}ego; $result .= $text_tmp . $tag_tmp; $skip = 1 if $tag_tmp =~ /^<[aA](?![0-9A-Za-z])/; if ($tag_tmp =~ /^<(XMP|PLAINTEXT|SCRIPT)(?![0-9A-Za-z])/i) { $str =~ /(.*?(?:<\/$1(?![0-9A-Za-z])$tag_regex_|$))/gsi; $result .= $1; } } }
については 「 $http_URL_regex
http URL の正規表現」,については 「 $ftp_URL_regex
ftp URL の正規表現」,については「メールアドレスの正規表現」の最後に書いてある スクリプトを正規表現として使います.また, $mail_regex
および $tag_regex
については 「 $tag_regex_
HTMLタグ の正規表現」のスクリプトを 正規表現として使います.また,には $str
HTML文書 全体を入れておきます. このスクリプトは以下の項目に 当てはまらないhttp URL とftp URL およびメールアドレスについてリンクを張ります.
- タグ(コメント)の内部である.
Aタグ でリンクが張ってある.XMPタグ ,PLAINTEXTタグ ,SCRIPTタグ の有効範囲内である.このスクリプトの説明を簡単にします.
に対して,テキスト部分とタグ部分をそれぞれ $str
1つ ずつ探してをまわします.タグ部分は特に処理する必要は ないのでそのままです. while
文は $skip
Aタグ でリンクを張り始めたときに 1 になります.このときは テキスト部分を特に処理することなくそのままにします.Aタグ が 閉じたときにを 0 に戻します. $skip
Aタグ でリンクを張っていないとき,テキスト部分にhttp URL かftp URL またはメールアドレスを見つけた 場合にはリンクを張ります.もし,タグ部分が
XMPタグ ,または,PLAINTEXTタグ だった場合には,次に対応する閉じタグまで無条件に スキップします.無条件というのは,にある条件でテキスト部分とタグ部分を取り出すことができなくなるため, 閉じタグだけに注目するということです.なぜなら,これらのタグの有効範囲内では 他のタグが無効になり, そのまま表示されるからです.逆に言えば,これらのタグの有効範囲内では タグに見えてもタグではなく,普通のテキストと同じように扱わなくてはならないと いうことです.ただし,この部分に while
文http URL やftp URL ,メールアドレスがある場合でもリンクは張りません. もし張ったとしても,それはそのまま表示されてしまい意味がないからです.SCRIPTタグ についても同様です.
に対するパターンマッチが行なわれている $str
2ヶ所 とも に修飾子 がつけられていることに注目してください.g
修飾子 をつけたパターンマッチをスカラーコンテキストで 行なうと,前回どこまでパターンマッチを行なったかを保存しておいて,次回 その続きから検索を始めてくれます.このスクリプトでは基本的にテキスト部分と タグ部分をg
1つ ずつ探してをまわしているのですが, while
文XMPタグ ,PLAINTEXTタグ ,SCRIPTタグ のときだけは 別処理をする必要があります.その処理終了後に戻ったときには,その続きからパターンマッチをしてもらう 必要があります. このようなときに, while
文に対するどちらのパターンマッチに おいても $str
修飾子 がつけられていますので, どちらの場合も都合よく続きからパターンマッチを始めることができるわけです.g
置換によってリンクを張る処理ですが,単純に次のように行なったのでは
2つ の理由からまずいことになります.$text_tmp =~ s/($http_URL_regex)/<A HREF="$1">$1<\/A>/go; $text_tmp =~ s/($ftp_URL_regex)/<A HREF="$1">$1<\/A>/go; $text_tmp =~ s/($mail_regex)/<A HREF="mailto:$1">$1<\/A>/go;
1つ 目の理由は,タグの中ではダブルクォートで囲む都合上, マッチしたものがダブルクォートを含んでいるとまずいことになるということです. そこで,ダブルクォートで囲む部分については,マッチしたものに含まれる ダブルクォートを" に変換するという処理が 必要になります.
2つ 目の理由ですが,置換の処理がhttp URL ,ftp URL ,メールアドレスのそれぞれについて独立して 行なわれているということです.これらは互いに他の正規表現にマッチする部分を 含むことができます.具体的な例で言いますと,次のようなものが挙げられます.http://www.din.or.jp/~ohzaki/?ftp://ftp.din.or.jp/+foobar@example.com ftp://ftp.din.or.jp/foobar@example.com "http://www.din.or.jp/~ohzaki/?ftp://ftp.din.or.jp/"@example.com上から順に
http URL ,ftp URL ,メールアドレス となっています.これらを独立して置換処理した場合,メールアドレスの一部をhttp URL として置換してしまったり,http URL の一部をftp URL として置換してしまうというようなことが 起こってしまいます.どちらがどちらに含まれるのかわからないので,置換処理の 順番でどうこうできる問題ではありません.幸いなことに,先頭部分が他の 正規表現にマッチすることはありませんので,これらの置換処理を1つ の正規表現としてまとめて,1回 の置換処理で 行なうことにより,うまくリンクを張ることができます.
トップへ# 半角スペース $space = '\x20'; # 全角スペース $Zspace = '(?:\xA1\xA1)'; # EUC-JP $Zspace_sjis = '(?:\x81\x40)'; # SJIS # 全角数字 [0-9] $Zdigit = '(?:\xA3[\xB0-\xB9])'; # EUC-JP $Zdigit_sjis = '(?:\x82[\x4F-\x58])'; # SJIS # 全角大文字 [A-Z] $Zuletter = '(?:\xA3[\xC1-\xDA])'; # EUC-JP $Zuletter_sjis = '(?:\x82[\x60-\x79])'; # SJIS # 全角小文字 [a-z] $Zlletter = '(?:\xA3[\xE1-\xFA])'; # EUC-JP $Zlletter_sjis = '(?:\x82[\x81-\x9A])'; # SJIS # 全角アルファベット [A-Za-z] $Zalphabet = '(?:\xA3[\xC1-\xDA\xE1-\xFA])'; # EUC-JP $Zalphabet_sjis = '(?:\x82[\x60-\x79\x81-\x9A])'; # SJIS # 全角ひらがな [ぁ-ん] $Zhiragana = '(?:\xA4[\xA1-\xF3])'; # EUC-JP $Zhiragana_sjis = '(?:\x82[\x9F-\xF1])'; # SJIS # 全角ひらがな(拡張) [ぁ-ん゛゜ゝゞ] $ZhiraganaExt = '(?:\xA4[\xA1-\xF3]|\xA1[\xAB\xAC\xB5\xB6])'; # EUC-JP $ZhiraganaExt_sjis = '(?:\x82[\x9F-\xF1]|\x81[\x4A\x4B\x54\x55])'; # SJIS # 全角カタカナ [ァ-ヶ] $Zkatakana = '(?:\xA5[\xA1-\xF6])'; # EUC-JP $Zkatakana_sjis = '(?:\x83[\x40-\x96])'; # SJIS # 全角カタカナ(拡張) [ァ-ヶ・ーヽヾ] $ZkatakanaExt = '(?:\xA5[\xA1-\xF6]|\xA1[\xA6\xBC\xB3\xB4])'; # EUC-JP $ZkatakanaExt_sjis = '(?:\x83[\x40-\x96]|\x81[\x45\x5B\x52\x53])'; # SJIS # 半角カタカナ [ヲ-゜] $Hkatakana = '(?:\x8E[\xA6-\xDF])'; # EUC-JP $Hkatakana_sjis = '[\xA6-\xDF]'; # SJIS # EUC-JP文字 $ascii = '[\x00-\x7F]'; # 1バイト EUC-JP文字 $twoBytes = '(?:[\x8E\xA1-\xFE][\xA1-\xFE])'; # 2バイト EUC-JP文字 $threeBytes = '(?:\x8F[\xA1-\xFE][\xA1-\xFE])'; # 3バイト EUC-JP文字 $character = "(?:$ascii|$twoBytes|$threeBytes)"; # EUC-JP文字 # EUC-JP文字(機種依存文字・未定義領域・3バイト文字を含まない) $character_strict = '(?:[\x00-\x7F]|' # ASCII . '\x8E[\xA1-\xDF]|' # 半角カタカナ . '[\xA1\xB0-\xCE\xD0-\xF3][\xA1-\xFE]|' # 1,16-46,48-83区 . '\xA2[\xA1-\xAE\xBA-\xC1\xCA-\xD0\xDC-\xEA\xF2-\xF9\xFE]|' # 2区 . '\xA3[\xB0-\xB9\xC1-\xDA\xE1-\xFA]|' # 3区 . '\xA4[\xA1-\xF3]|' # 4区 . '\xA5[\xA1-\xF6]|' # 5区 . '\xA6[\xA1-\xB8\xC1-\xD8]|' # 6区 . '\xA7[\xA1-\xC1\xD1-\xF1]|' # 7区 . '\xA8[\xA1-\xC0]|' # 8区 . '\xCF[\xA1-\xD3]|' # 47区 . '\xF4[\xA1-\xA6])'; # 84区 # EUC-JP未定義文字(機種依存文字・3バイト文字を含む) $character_undef = '(?:[\xA9-\xAF\xF5-\xFE][\xA1-\xFE]|' # 9-15,85-94区 . '\x8E[\xE0-\xFE]|' # 半角カタカナ . '\xA2[\xAF-\xB9\xC2-\xC9\xD1-\xDB\xEB-\xF1\xFA-\xFD]|' # 2区 . '\xA3[\XA1-\xAF\xBA-\xC0\xDB-\xE0\xFB-\xFE]|' # 3区 . '\xA4[\xF4-\xFE]|' # 4区 . '\xA5[\xF7-\xFE]|' # 5区 . '\xA6[\xB9-\xC0\xD9-\xFE]|' # 6区 . '\xA7[\xC2-\xD0\xF2-\xFE]|' # 7区 . '\xA8[\xC1-\xFE]|' # 8区 . '\xCF[\xD4-\xFE]|' # 47区 . '\xF4[\xA7-\xFE]|' # 84区 . '\x8F[\xA1-\xFE][\xA1-\xFE])'; # 3バイト文字 # SJIS文字 $oneByte_sjis = '[\x00-\x7F\xA1-\xDF]'; # 1バイト SJIS文字 $twoBytes_sjis = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])'; # 2バイト SJIS文字 $character_sjis = "(?:$oneByte_sjis|$twoBytes_sjis)"; # SJIS文字 # SJIS文字(機種依存文字・未定義領域を含まない) $character_sjis_strict = '(?:[\x00-\x7F\xA1-\xDF]|' # ASCII,半角カタカナ . '[\x89-\x97\x99-\x9F\xE0-\xE9][\x40-\x7E\x80-\xFC]|' # 17-46,49-82区 . '\x81[\x40-\x7E\x80-\xAC\xB8-\xBF\xC8-\xCE\xDA-\xE8\xF0-\xF7\xFC]|' # 1,2区 . '\x82[\x4F-\x58\x60-\x79\x81-\x9A\x9F-\xF1]|' # 3,4区 . '\x83[\x40-\x7E\x80-\x96\x9F-\xB6\xBF-\xD6]|' # 5,6区 . '\x84[\x40-\x60\x70-\x7E\x80-\x91\x9F-\xBE]|' # 7,8区 . '\x88[\x9F-\xFC]|' # 15,16区 . '\x98[\x40-\x72\x9F-\xFC]|' # 47,48区 . '\xEA[\x40-\x7E\x80-\xA4])'; # 83,84区 # SJIS未定義文字(機種依存文字を含む) $character_sjis_undef = '(?:[\x85-\x87\xEB-\xFC][\x40-\x7E\x80-\xFC]|' # 9-14,85-120区 . '\x81[\xAD-\xB7\xC0-\xC7\xCF-\xD9\xE9-\xEF\xF8-\xFB]|' # 1,2区 . '\x82[\x40-\x4E\x59-\x5F\x7A-\x7E\x80\x9B-\x9E\xF2-\xFC]|' # 3,4区 . '\x83[\x97-\x9E\xB7-\xBE\xD7-\xFC]|' # 5,6区 . '\x84[\x61-\x6F\x92-\x9E\xBF-\xFC]|' # 7,8区 . '\x88[\x40-\x7E\x80-\x9E]|' # 15,16区 . '\x98[\x73-\x7E\x80-\x9E]|' # 47,48区 . '\xEA[\xA5-\xFC])'; # 83,84区 # ドコモ絵文字 $iPictograph_base = '(?:\xF8[\x9F-\xFC]|' # 基本絵文字(SJIS) . '\xF9[\x40-\x49\x50-\x52\x55-\x57\x5B-\x5E\x72-\x7E\x80-\xB0])'; $iPictograph_ext = '(?:\xF9[\xB1-\xFC])'; # 拡張絵文字(SJIS) $iPictograph = '(?:$iPictograph_base|$iPictograph_ext)'; # iモード対応 絵文字(SJIS)日本語の扱いについては「日本語を扱う」を参照.
個々の機種依存文字についてはここでは扱わないこととする. なぜなら,機種依存文字は各ベンダ・文字コードごとに非常に多くの種類が存在し, そのすべてを把握することは不可能なためである. 以下のリンク先の文書の外字(ユーザ定義とベンダ定義)の欄が機種依存文字に 該当する.
トップへ# HTMLタグの正規表現 $tag_regex $tag_regex_ = q{[^"'<>]*(?:"[^"]*"[^"'<>]*|'[^']*'[^"'<>]*)*(?:>|(?=<)|$(?!\n))}; #'}}}} $comment_tag_regex = '<!(?:--[^-]*-(?:[^-]+-)*?-(?:[^>-]*(?:-[^>-]+)*?)??)*(?:>|$(?!\n)|--.*$)'; $tag_regex = qq{$comment_tag_regex|<$tag_regex_};このスクリプトの
がコメントタグの正規表現で, $comment_tag_regex
がコメントタグ以外の普通の タグの < 以降の正規表現になります. $tag_regex_
最初に普通のタグの正規表現について説明します.普通のタグの中身の 正規表現として最初に思いつくのは
です. しかし,これではダブルクォートやシングルクォートで囲まれた中に [^>]*
>
があった場合にまずいことになります. そこで,ダブルクォートやシングルクォートについて考えます.ダブルクォートで囲まれている部分の正規表現は
と書くことができます. シングルクォートで囲まれている部分についても同様です. これでダブルクォートやシングルクォートで囲まれている内側には "[^"]*"
>
を含むことができます.それ以外のダブルクォートでもシングルクォートでも 囲まれていない部分は今度こそだから,結局 [^>]
でいいか,というとそううまくはいきません. (?:[^>]|"[^"]*"|'[^']')*
ではダブルクォートや シングルクォートまで含んでしまうため,せっかく用意したダブルクォートや シングルクォートで囲まれている部分の正規表現が使われることなくそのまま マッチングが進んで,ダブルクォートやシングルクォートの 中の [^>]
>
をタグの終わりと間違えてマッチが成功してしまいます.これを回避するには,
のように最初にダブルクォートかシングルクォートで囲まれているかどうかを 調べる方法があります.しかし,これは明らかに遅いです. なぜかというと,ダブルクォートやシングルクォートで囲まれていない部分の場合, (?:"[^"]*"|'[^']*'|[^>])*
1文字 ごとにダブルクォートとシングルクォートのマッチングが 失敗してからでないとにマッチしないためです. そこで次のように [^>]
とするとすべてがうまくいきます. [^"'>]
$tag_regex_ = q{(?:[^"'>]|"[^"]*"|'[^']*')*}; #'}}}次に閉じないタグのことを考えます.閉じないタグとは
のように <P<B>
>
を省略してあるものです.このときを正しくタグとして認識するためには,タグの中身は <P
ではなく [^>]*
としなければならないことになります. また,タグの最後は必ず [^<>]*
>
で終わるとは限らないので,とする必要があります. これは (?:>|(?=<)|$(?!\n))
>
で終わる普通のタグか,または, その次の文字がタグの開始文字である<
であるか,または, 文字列の最後である場合を表しています.については後で詳しく説明します.結局,これをまとめると次のようになります. $(?!\n)
$tag_regex_ = q{(?:[^"'<>]|"[^"]*"|'[^']*')*(?:>|(?=<)|$(?!\n))}; #'}}}これを
Jeffrey E. F. Friedl 氏原著による 「詳説 正規表現」で 「ループ展開」として書かれている 手法で実行速度を速くしたものが最初のスクリプトの 正規表現です.簡単なベンチマークをとってみたところ約 1.5倍 ほど 速かったです.次にコメントタグの正規表現の説明をします.コメントタグについては, まずは水無月 ばけらさんによる 「 SGMLの注釈宣言」を一読することをお勧めします.
コメントタグ,すなわち,注釈宣言は
--コメントの中身-- というコメントだけから構成されています. コメントタグは複数のコメントを持つことができ, コメントの後ろには空白文字のみあってもかまいません. また,コメントの中身やコメントの数が0個 であってもかまいません. ただし,<! とコメントの間に空白文字があることは 許されていないので,<! の直後にはコメントか 閉じ括弧の > しか来てはいけないことになります.以上のことから, 正常なコメントタグの正規表現は次のようになります.# 正常なコメントタグの正規表現 $comment_tag_regex $comment_tag_regex = q{<!(?:--(?:(?!--).)*--\s*)*>};この正常なコメントタグの正規表現をもとに,閉じないコメントタグだった場合と, コメントの後ろに空白文字以外の文字があって不正であるコメントタグだった場合にも 対応した正規表現が最初のスクリプトになります.
最後の
の選択はそれぞれ, コメントタグが閉じていた場合,コメントの後に (?:>|$(?!\n)|--.*$)
>
がなく閉じて いなかった場合,コメントの終わりのがなくコメントの中身が最後まで続いている場合を表わしています. --
ですが,ただの $(?!\n)
$
でもよいのではないかと疑問に思われる人もいるかと 思いますが,と $(?!\n)
$
では 少し意味が違います.たとえば,のとき, $str = "test\n";
はマッチしますが, m/^test$/
はマッチしません. なぜなら, m/^test$(?!\n)/
$
は文字列の最後に改行があった場合には, 改行の直前でもマッチするからです.もし,にはマッチしてほしいが, 'test'
にはマッチしてほしくないというときに,ただの "test\n"
$
では困るわけです.コメントタグの正規表現ではのような場合にマッチしてもらっては困るのでこのような正規表現になっています. "<!\n"
perl5.005 以降ならばを $(?!\n)
とすることができます. \z
は \z
$
やと違って本当の意味で文字列の最後にマッチします. \Z
実は次のようにコメントタグの正規表現を書いても同じことができます. 理解するにはまずこちらの正規表現をもとに考えた方がよいかもしれません.
# コメントタグの正規表現(遅い) $comment_tag_regex = '<!(?:--(?:(?!--).)*--(?:(?!--)[^>])*)*(?:>|$(?!\n)|--.*$)';この正規表現では,コメントの中身を表わす正規表現として
としています.これの意味は,次に (?:(?!--).)*
が来ないような何か --
1文字 の繰り返しということです.つまり,-
が単独で現れた場合には問題ないわけで,と続けて現れる --
-
は駄目だということになります. これでコメントの中身にはが絶対に現れないことが 保証されます.コメントの中身を表わす正規表現としてはこれで正しいのですが, --
1文字 ごとにでないことを チェックしているのでこのままでは実行速度が遅いです. --
そこで普通のタグのときと同様に「ループ展開」の 手法を用いることとします.まず,
が来ない何か --
1文字 の繰り返しを表わすを少し違う考え方で表現し直します.この正規表現は (?:(?!--).)*
が含まれない部分ということですので,まず, --
-
以外の 文字ならば問題ないことはすぐにわかると思います.仮に-
が来たとしてもその次の文字が-
以外の文字であればその場合もまた大丈夫です. ということは,は (?:(?!--).)*
と変形することができます. これに対して「ループ展開」の手法を用いると, (?:[^-]|-[^-])*
となり,結局 [^-]*(?:-[^-][^-]*)*
となります. [^-]*(?:-[^-]+)*
これでコメント部分の正規表現は
となりました. 簡単なベンチマークをとったところ 約 --[^-]*(?:-[^-]+)*--
2倍 ほど速くなりました. しかし,まだ最初のスクリプトとは少し違っています. このコメント部分の正規表現には非決定性なところが あります.それは-
が来たときに,その時点では それがコメントの中身なのか,コメントの終了を表わすの最初の --
1文字 なのかわかりませんが, 正規表現でもやはりマッチする可能性がある場所が2ヶ所 に なっているということです.つまり,の (?:-[^-]+)*
直後の (?:
-
にマッチするかもしれないし,直後の (?:-[^-]+)*
-
にマッチするかもしれないのです.このような非決定性はバックトラック発生時に 多くの負担を強いることになります.そこで,を変形し,非決定性を排除すると [^-]*(?:-[^-]+)*--
となります.これで [^-]*-(?:[^-]+-)*-
-
が来たときにマッチする正規表現の部分は直後の [^-]*
-
の1ヶ所 となります.ここまでの変形でかなり最初のスクリプトに近づきましたが,まだ
1ヶ所 違っています.それはと (?:[^-]+-)*
,つまり, (?:[^-]+-)*?
*
との違いです.一般に *?
*
とを変えたらマッチするものも 変わってしまいます.しかし,今回の場合は *?
*
でもでも必ず同じ結果となります. 必ず同じ結果となることがわかっているので,実行速度が速い方を考えます. 一般にコメントタグというものは *?
<-- これはコメントタグです --> というようなものが ほとんどでしょう.つまり,コメントタグの中身として-
を含んでいるものの出現頻度は,含んでいないものの出現頻度よりも 低いということです.もし,コメントタグの中身に-
を含んでいた 場合はの部分を通過することになります. しかし,実際には含んでいないことの方が多いわけですから, (?:[^-]+-)
の部分をチェックするのは無駄な ことになります.そこで, (?:[^-]+-)
*
をと することでこの無駄を可能な限り排除することができます. *?
次に
の部分について考えます. ここもコメントタグの中身の部分と同様にまずは 「ループ展開」の手法を用いて (?:(?!--)[^>])*
と変形します.更に, (?:[^>-]*(?:-[^>-]+)*
*
をとすることができますので, *?
と変形するところまでは同じです. (?:[^>-]*(?:-[^>-]+)*?
最初のスクリプトでは更に全体を
というように (?: regex)??
をつけた形にしています. これは,コメントタグの中身と違って,一般にコメントの終了を表わす ??
の後ろには何か文字が入ることなく直後に > で閉じられているものの出現頻度が高いと思われるためです.言い換えると, --
がマッチすることはほとんどない,つまり,マッチさせようとすると無駄に終わる ことが多いと思われるため,この部分の正規表現全体に (?:[^>-]*(?:-[^>-]+)*?
をつけて,可能な限りチェックさせないように しています. ??
トップへ# $uri が正しい URI か判定する $digit = q{[0-9]}; $upalpha = q{[A-Z]}; $lowalpha = q{[a-z]}; $alpha = qq{(?:$lowalpha|$upalpha)}; $alphanum = qq{(?:$alpha|$digit)}; $hex = qq{(?:$digit|[A-Fa-f])}; $escaped = qq{%$hex$hex}; $mark = q{[-_.!~*'()]}; $unreserved = qq{(?:$alphanum|$mark)}; $reserved = q{[;/?:@&=+$,]}; $uric = qq{(?:$reserved|$unreserved|$escaped)}; $fragment = qq{$uric*}; $query = qq{$uric*}; $pchar = qq{(?:$unreserved|$escaped|} . q{[:@&=+$,])}; $param = qq{$pchar*}; $segment = qq{$pchar*(?:;$param)*}; $path_segments = qq{$segment(?:/$segment)*}; $abs_path = qq{/$path_segments}; $uric_no_slash = qq{(?:$unreserved|$escaped|} . q{[;?:@&=+$,])}; $opaque_part = qq{$uric_no_slash$uric*}; $path = qq{(?:$abs_path|$opaque_part)?}; $port = qq{$digit*}; $IPv4address = qq{$digit+\\.$digit+\\.$digit+\\.$digit+}; $toplabel = qq{(?:$alpha|$alpha(?:$alphanum|-)*$alphanum)}; $domainlabel = qq{(?:$alphanum|$alphanum(?:$alphanum|-)*$alphanum)}; $hostname = qq{(?:$domainlabel\\.)*$toplabel\\.?}; $host = qq{(?:$hostname|$IPv4address)}; $hostport = qq{$host(?::$port)?}; $userinfo = qq{(?:$unreserved|$escaped|} . q{[;:&=+$,])*}; $server = qq{(?:(?:$userinfo\@)?$hostport)?}; $reg_name = qq{(?:$unreserved|$escaped|} . q{[$,;:@&=+])+}; $authority = qq{(?:$server|$reg_name)}; $scheme = qq{$alpha(?:$alpha|$digit|[-+.])*}; $rel_segment = qq{(?:$unreserved|$escaped|} . q{[;@&=+$,])+}; $rel_path = qq{$rel_segment(?:$abs_path)?}; $net_path = qq{//$authority(?:$abs_path)?}; $hier_part = qq{(?:$net_path|$abs_path)(?:\\?$query)?}; $relativeURI = qq{(?:$net_path|$abs_path|$rel_path)(?:\\?$query)?}; $absoluteURI = qq{$scheme:(?:$hier_part|$opaque_part)}; $URI_reference = qq{(?:$absoluteURI|$relativeURI)?(?:#$fragment)?}; $pattern = $URI_reference; print "ok\n" if $uri =~ /^$pattern$/o;URI については
RFC 2396 ( 日本語訳 )に書かれています.それを機械的に素直に正規表現にした ものが上のスクリプトです.これから求めた URI References の正規表現は 次のようになりました.(?:(?:[a-z]|[A-Z])(?:(?:[a-z]|[A-Z])|[0-9]|[-+.])*:(?:(?://(?:(?:( ?:(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f]) (?:[0-9]|[A-Fa-f])|[;:&=+$,])*@)?(?:(?:(?:(?:(?:[a-z]|[A-Z])|[0-9] )|(?:(?:[a-z]|[A-Z])|[0-9])(?:(?:(?:[a-z]|[A-Z])|[0-9])|-)*(?:(?:[ a-z]|[A-Z])|[0-9]))\.)*(?:(?:[a-z]|[A-Z])|(?:[a-z]|[A-Z])(?:(?:(?: [a-z]|[A-Z])|[0-9])|-)*(?:(?:[a-z]|[A-Z])|[0-9]))\.?|[0-9]+\.[0-9] +\.[0-9]+\.[0-9]+)(?::[0-9]*)?)?|(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[ -_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[$,;:@&=+])+)(?: /(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])( ?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[ -_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*(?: /(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])( ?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[ -_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*)*) ?|/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f] )(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9]) |[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*( ?:/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f] )(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9]) |[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*) *)(?:\?(?:[;/?:@&=+$,]|(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|% (?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]))*)?|(?:(?:(?:(?:[a-z]|[A-Z])| [0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[;?:@&=+ $,])(?:[;/?:@&=+$,]|(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?: [0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]))*)|(?://(?:(?:(?:(?:(?:(?:(?:[a- z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f] )|[;:&=+$,])*@)?(?:(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|(?:(?:[a-z]|[A- Z])|[0-9])(?:(?:(?:[a-z]|[A-Z])|[0-9])|-)*(?:(?:[a-z]|[A-Z])|[0-9] ))\.)*(?:(?:[a-z]|[A-Z])|(?:[a-z]|[A-Z])(?:(?:(?:[a-z]|[A-Z])|[0-9 ])|-)*(?:(?:[a-z]|[A-Z])|[0-9]))\.?|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ )(?::[0-9]*)?)?|(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[ 0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[$,;:@&=+])+)(?:/(?:(?:(?:(?:[a-z ]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]) |[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[ 0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*(?:/(?:(?:(?:(?:[a-z ]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]) |[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[ 0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*)*)?|/(?:(?:(?:(?:[a -z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f ])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(? :[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*(?:/(?:(?:(?:(?:[a -z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f ])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(? :[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*)*)*|(?:(?:(?:(?:[a- z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f] )|[;@&=+$,])+(?:/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?: [0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z] |[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])| [:@&=+$,])*)*(?:/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?: [0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z] |[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])| [:@&=+$,])*)*)*)?)(?:\?(?:[;/?:@&=+$,]|(?:(?:(?:[a-z]|[A-Z])|[0-9] )|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]))*)?)?(?:#(?:[ ;/?:@&=+$,]|(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A -Fa-f])(?:[0-9]|[A-Fa-f]))*)?この正規表現はあまりにも一般的すぎて,ほとんどの入力に対してマッチして しまいます.
RFC 2396 はもともと URI の一般形を定義した ものであるので,この正規表現を使うことはほとんどないと言っていいでしょう.
トップへ# $http が正しい http URL か判定する $digit = q{[0-9]}; $upalpha = q{[A-Z]}; $lowalpha = q{[a-z]}; $alpha = qq{(?:$lowalpha|$upalpha)}; $alphanum = qq{(?:$alpha|$digit)}; $hex = qq{(?:$digit|[A-Fa-f])}; $escaped = qq{%$hex$hex}; $mark = q{[-_.!~*'()]}; $unreserved = qq{(?:$alphanum|$mark)}; $reserved = q{[;/?:@&=+$,]}; $uric = qq{(?:$reserved|$unreserved|$escaped)}; $query = qq{$uric*}; $pchar = qq{(?:$unreserved|$escaped|} . q{[:@&=+$,])}; $param = qq{$pchar*}; $segment = qq{$pchar*(?:;$param)*}; $path_segments = qq{$segment(?:/$segment)*}; $abs_path = qq{/$path_segments}; $port = qq{$digit*}; $IPv4address = qq{$digit+\\.$digit+\\.$digit+\\.$digit+}; $toplabel = qq{(?:$alpha|$alpha(?:$alphanum|-)*$alphanum)}; $domainlabel = qq{(?:$alphanum|$alphanum(?:$alphanum|-)*$alphanum)}; $hostname = qq{(?:$domainlabel\\.)*$toplabel\\.?}; $host = qq{(?:$hostname|$IPv4address)}; $http_URL = qq{http://$host(?::$port)?(?:$abs_path(?:\\?$query)?)?}; $pattern = $http_URL; print "ok\n" if $http =~ /^$pattern$/;
http URL についてはRFC 2616 の3.2.2 http URL に書かれています. このスクリプトは, 「URI(URL) の正規表現」で書いた URI(URL) の 正規表現のスクリプトを修正し,http URL の正規表現にしたものです.このスクリプトから求めたhttp URL の正規表現は次のようになりました.http://(?:(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|(?:(?:[a-z]|[A-Z])|[0-9] )(?:(?:(?:[a-z]|[A-Z])|[0-9])|-)*(?:(?:[a-z]|[A-Z])|[0-9]))\.)*(?: (?:[a-z]|[A-Z])|(?:[a-z]|[A-Z])(?:(?:(?:[a-z]|[A-Z])|[0-9])|-)*(?: (?:[a-z]|[A-Z])|[0-9]))\.?|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?::[0-9 ]*)?(?:/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A- Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[ 0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,] )*)*(?:/(?:(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'()])|%(?:[0-9]|[A- Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,])*(?:;(?:(?:(?:(?:[a-z]|[A-Z])|[ 0-9])|[-_.!~*'()])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f])|[:@&=+$,] )*)*)*(?:\?(?:[;/?:@&=+$,]|(?:(?:(?:[a-z]|[A-Z])|[0-9])|[-_.!~*'() ])|%(?:[0-9]|[A-Fa-f])(?:[0-9]|[A-Fa-f]))*)?)?この
http URL の正規表現は,文字クラス同士の選択が まとめられていないので無駄が多いことがわかります.そこで,文字クラスを なるべくまとめるように以下のように一部改良します.# $http が正しい http URL か判定する(文字クラス改良版) $alpha = q{[a-zA-Z]}; $alphanum = q{[a-zA-Z0-9]}; $hex = q{[0-9A-Fa-f]}; $uric = q{(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]} . qq{|$escaped)}; $pchar = q{(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]} . qq{|$escaped)}; $toplabel = qq{(?:$alpha|$alpha} . q{[-a-zA-Z0-9]*} . qq{$alphanum)}; $domainlabel = qq{(?:$alphanum|$alphanum} . q{[-a-zA-Z0-9]*} . qq{$alphanum)};このスクリプトから求めた
http URL の正規表現は次のように なりました.http://(?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][-a-zA-Z0-9]*[a-zA-Z0-9])\. )*(?:[a-zA-Z]|[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9])\.?|[0-9]+\.[0-9]+\ .[0-9]+\.[0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0 -9A-Fa-f][0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa -f][0-9A-Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][ 0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-F a-f])*)*)*(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A -Fa-f])*)?)?この正規表現は前述しましたが,
RFC 2616 の3.2.2 http URL に書かれています.RFC 2616 には HTTPプロトコルに関することが書かれており,3.2.2 http URL に書かれているhttp URL も,HTTPプロトコルの中での話になります. 一般に,HTML のリンクに使用されるものは,純粋に HTTPプロトコルの中で使用されるhttp URL ではなく, scheme が http であるURI References です.たとえば
http://user:passwd@www.din.or.jp/~ohzaki/perl.htm#URI はURI References ですが,user:passwd@ の部分,すなわち,userinfo や,#URI の部分,すなわち,Fragment Identifier は HTTPプロトコルの中で使用されるhttp URL としては不正なものとなります.しかし,HTML のリンクとしては問題ありません.なぜなら,クライアント(ブラウザ)が HTTPプロトコルで通信する際にはそれらを削除しているからです.余談ですが,RFC 2396 ( 日本語訳 ) の第 4章 にはFragment Identifier は URI の一部ではないと書かれています.Fragment Identifier はuser agent によって解釈される付加的参照情報だそうです.次に,scheme が http である
URI References を考えます. そこで再び「URI(URL) の正規表現」で書いた URI(URL) の 正規表現のスクリプトを修正して作ります.その際, HTTPプロトコルの中で使用されるhttp URL を構築するのに必要な情報を必ず含んでいれば, それ以外に冗長な情報を含んでいてもよいとします.必要な情報とは,host,port, abs_path,query です.また,scheme は当然 http ですが,この際, Secure Hyper Text Tranasfer Protocol(S-HTTP)と呼ばれる プロトコルを使うshttp: や Secure Sockets Layer(SSL) というプロトコルを使うhttps: にも対応するようにしておきます. 修正した結果は,以下のように一部を修正することになりました.$server = qq{(?:$userinfo\@)?$hostport}; $authority = qq{$server}; $scheme = q{(?:https?|shttp)}; $hier_part = qq{$net_path(?:\\?$query)?}; $absoluteURI = qq{$scheme:$hier_part}; $URI_reference = qq{$absoluteURI(?:#$fragment)?};これに先ほどと同じように文字クラスをまとめる改良として,以下のように一部 を修正しました.
$alpha = q{[a-zA-Z]}; $alphanum = q{[a-zA-Z0-9]}; $hex = q{[0-9A-Fa-f]}; $unreserved = q{[-_.!~*'()a-zA-Z0-9]}; $uric = q{(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]} . qq{|$escaped)}; $pchar = q{(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]} . qq{|$escaped)}; $toplabel = qq{(?:$alpha|$alpha} . q{[-a-zA-Z0-9]*} . qq{$alphanum)}; $domainlabel = qq{(?:$alphanum|$alphanum} . q{[-a-zA-Z0-9]*} . qq{$alphanum)}; $userinfo = q{(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|} . qq{$escaped)*};このようにして求めた正規表現は次のようになりました.
(?:https?|shttp)://(?:(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|%[0-9A-Fa-f][ 0-9A-Fa-f])*@)?(?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][-a-zA-Z0-9]*[a-zA- Z0-9])\.)*(?:[a-zA-Z]|[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9])\.?|[0-9]+\ .[0-9]+\.[0-9]+\.[0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&= +$,]|%[0-9A-Fa-f][0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|% [0-9A-Fa-f][0-9A-Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9 A-Fa-f][0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f ][0-9A-Fa-f])*)*)*)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A- Fa-f][0-9A-Fa-f])*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-F a-f][0-9A-Fa-f])*)?この正規表現を使えば,
が, scheme が http である $http
URI References かどうか判定することはできます. ところが,ある文字列の 中からhttp URL を抽出する目的でこの正規表現を使っても うまくいきません.たとえば,次のような スクリプトを実行するとうまくいかないことがわかります.# $str から http URI References を抽出する $str = "このページの URI は http://www.din.or.jp/~ohzaki/perl.htm です"; $pattern = $URI_reference; while ($str =~ /($pattern)/g) { print $1, "\n"; } 実行結果(失敗例) http://www.din.or.jなぜこのような結果になってしまったのでしょうか.それは Perl のパターン マッチエンジンが非決定性有限オートマトン NFAs(Nondeterministic Finite Automata) だからです. 次のようなスクリプトを考えてみてください.
print "数字 1文字 or 数字で始まり数字か小文字が続くもの\n"; $str = '123abc'; @patterns = ('(?:\d|\d[0-9a-z]+)', '(?:\d[0-9a-z]*)'); foreach $pattern (@patterns) { print " 文字列 $str パターン $pattern "; print '結果 ' . join('/', $str =~ /$pattern/g) . "\n"; } print "\n数字 1文字 or 最初が数字か小文字で,次が小文字のもの\n"; $str = '1a'; @patterns = ('(?:\d|[\da-z][a-z])', '(?:[\da-z][a-z]|\d)'); foreach $pattern (@patterns) { print " 文字列 $str パターン $pattern "; print '結果 ' . join('/', $str =~ /$pattern/g) . "\n"; } 実行結果 数字 1文字 or 最初が数字で,その後数字か小文字が続くもの 文字列 123abc パターン (?:\d|\d[0-9a-z]+) 結果 1/2/3 文字列 123abc パターン (?:\d[0-9a-z]*) 結果 123abc 数字 1文字 or 最初が数字か小文字で,次が小文字のもの 文字列 1a パターン (?:\d|[\da-z][a-z]) 結果 1 文字列 1a パターン (?:[\da-z][a-z]|\d) 結果 1a2つの例のうち,どちらも最初の正規表現では文字列の一部にしか マッチしていないことがわかると思います. このように Perl のパターンマッチエンジンはうまくマッチさせていけば もっと長い文字列にマッチさせることができる場合でも,最初に見つかった方法で パターンマッチを進めてしまいます.それではなぜもう一方の正規表現では うまく文字列全体にマッチさせることができたのでしょうか.
1つめの例では,
という選択を (?:regex1|regex1regex2+)
という形に変形し, 選択が現れないようにしています.このようにすることで, より長くマッチさせることができ,また,ほとんどの 場合にバックトラックを減らすことができるので 効率的になります.これと同じこと行ない,以下のように一部改良します. regex1regex2*
$toplabel = qq{$alpha(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?}; $domainlabel = qq{$alphanum(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?};2つめの例では,
という選択で, (?:regex1|regex2)
が regex1
の一部とマッチしてしまう場合に, より長くマッチできる可能性である. regex2
を試すことなく regex2
が選択されてしまった ために文字列の一部にマッチしてしまったのです.選択を逆にした, regex1
の形に修正することで,このような事態を避けることができます.実際に その可能性のある選択を持つ部分というと,host の正規表現の hostname と IPv4address の選択の部分になります.なぜなら,IPv4address の正規表現は hostname の一部とマッチしてしまう可能性があるからです.例えば, (?:regex2|regex1)
127.0.0.1.www.din.or.jp という host があった場合,先に IPv4address をマッチさせてしまうと127.0.0.1 の部分に マッチしてしまいます.幸い,最初から host の正規表現は先に hostname をマッチさせるようになっていますので,特に修正する必要はないことになります.最後に,
pseudohttp://foo/bar.htm のように HTTP ではない scheme の途中からマッチしてしまうことがないように, 以下のように改良します.$http_URL_regex = q{\b} . $URI_reference;以上の改良をすべてまとめた最終的なスクリプトは以下のようになりました.
# http URL の正規表現 $http_URL_regex $digit = q{[0-9]}; $alpha = q{[a-zA-Z]}; $alphanum = q{[a-zA-Z0-9]}; $hex = q{[0-9A-Fa-f]}; $escaped = qq{%$hex$hex}; $uric = q{(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]} . qq{|$escaped)}; $fragment = qq{$uric*}; $query = qq{$uric*}; $pchar = q{(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]} . qq{|$escaped)}; $param = qq{$pchar*}; $segment = qq{$pchar*(?:;$param)*}; $path_segments = qq{$segment(?:/$segment)*}; $abs_path = qq{/$path_segments}; $port = qq{$digit*}; $IPv4address = qq{$digit+\\.$digit+\\.$digit+\\.$digit+}; $toplabel = qq{$alpha(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?}; $domainlabel = qq{$alphanum(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?}; $hostname = qq{(?:$domainlabel\\.)*$toplabel\\.?}; $host = qq{(?:$hostname|$IPv4address)}; $hostport = qq{$host(?::$port)?}; $userinfo = q{(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|} . qq{$escaped)*}; $server = qq{(?:$userinfo\@)?$hostport}; $authority = qq{$server}; $scheme = q{(?:https?|shttp)}; $net_path = qq{//$authority(?:$abs_path)?}; $hier_part = qq{$net_path(?:\\?$query)?}; $absoluteURI = qq{$scheme:$hier_part}; $URI_reference = qq{$absoluteURI(?:#$fragment)?}; $http_URL_regex = q{\b} . $URI_reference;
このスクリプトから求めた
http URL の正規表現は次のように なりました.\b(?:https?|shttp)://(?:(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|%[0-9A-Fa-f ][0-9A-Fa-f])*@)?(?:(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.) *[a-zA-Z](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\. [0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f] [0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A- Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f ])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*) *)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f]) *)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])* )?この正規表現を使えば,
http URL の抽出がうまくいくように なります.以下がこれを直接代入して使うスクリプトになります.$http_URL_regex = q{\b(?:https?|shttp)://(?:(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|%[0-9A-Fa-f} . q{][0-9A-Fa-f])*@)?(?:(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.)} . q{*[a-zA-Z](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\.} . q{[0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f]} . q{[0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-} . q{Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f} . q{])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*)} . q{*)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])} . q{*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*} . q{)?};さて,ここまで長々と書いてきましたが,正確に正規表現を書くことを あきらめて,もっと簡単でいいやという人のための
http URL の正規表現が以下になります.s?https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+この正規表現を一旦変数に代入して使用する場合は問題ありませんが,直接正規 表現として利用する場合は次のように書く必要があります.
# 文書 $text から http URL を抽出して @http に格納する @http = $text =~ /s?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g;
/
がになっているのは問題ないと思います. 特に注意しなければいけないのは, \/
$
と@
の部分です.これらはそのままではそれぞれスカラー変数・配列変数として扱われ, 変数展開の対象となってしまいます.そこでこの2つ についてもと \$
のようにする必要があります.もし,この \@
2つ に\
をつけ忘れていた場合はどうなるのか? そのときは,$
については特殊変数 として通常は空文字列に展開されてしまいます.$,
@
についてはで始まるような配列変数は存在しないので, 配列変数としては扱われずそのままになります. @&
トップへ# ftp URL の正規表現 $ftp_URL_regex $digit = q{[0-9]}; $alpha = q{[a-zA-Z]}; $alphanum = q{[a-zA-Z0-9]}; $hex = q{[0-9A-Fa-f]}; $escaped = qq{%$hex$hex}; $uric = q{(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]} . qq{|$escaped)}; $fragment = qq{$uric*}; $query = qq{$uric*}; $pchar = q{(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]} . qq{|$escaped)}; $segment = qq{$pchar*}; $ftptype = q{[AIDaid]}; $path_segments = qq{$segment(?:/$segment)*(?:;type=$ftptype)?}; $abs_path = qq{/$path_segments}; $port = qq{$digit*}; $IPv4address = qq{$digit+\\.$digit+\\.$digit+\\.$digit+}; $toplabel = qq{$alpha(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?}; $domainlabel = qq{$alphanum(?:} . q{[-a-zA-Z0-9]*} . qq{$alphanum)?}; $hostname = qq{(?:$domainlabel\\.)*$toplabel\\.?}; $host = qq{(?:$hostname|$IPv4address)}; $hostport = qq{$host(?::$port)?}; $user = q{(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|} . qq{$escaped)*}; $password = $user; $userinfo = qq{$user(?::$password)?}; $server = qq{(?:$userinfo\@)?$hostport}; $authority = qq{$server}; $scheme = q{ftp}; $net_path = qq{//$authority(?:$abs_path)?}; $hier_part = qq{$net_path(?:\\?$query)?}; $absoluteURI = qq{$scheme:$hier_part}; $URI_reference = qq{$absoluteURI(?:#$fragment)?}; $ftp_URL_regex = q{\b} . $URI_reference;
ftp URL についてはRFC 1738 に書かれています.ただし,現在ではRFC 1738 はRFC 2396 ( 日本語訳 )によって更新されています.更新されていると言ってもRFC 2396 は URI の一般形を定義したものになっているので,ftp URL の定義について直接書かれている部分はありません. そこで,ftp URL の正規表現として,RFC 2396 の URI の一般形の定義をもとに, 「http URL の正規表現」でスキームが http であるURI References として求めた方法と同様の方法で, スキームが ftp であるURI References を考えます.
RFC 1738 に書かれているftp URL の定義を考慮して書き換えた部分は以下のようになります.$segment = qq{$pchar*}; $ftptype = q{[AIDaid]}; $path_segments = qq{$segment(?:/$segment)*(?:;type=$ftptype)?}; $user = q{(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|} . qq{$escaped)*}; $password = $user; $userinfo = qq{$user(?::$password)?}; $server = qq{(?:$userinfo\@)?$hostport}; $authority = qq{$server}; $scheme = q{ftp}; $net_path = qq{//$authority(?:$abs_path)?}; $hier_part = qq{$net_path(?:\\?$query)?}; $absoluteURI = qq{$scheme:$hier_part}; $URI_reference = qq{$absoluteURI(?:#$fragment)?}; $ftp_URL_regex = q{\b} . $URI_reference;
ftp URL はRFC 1738 でftpurl = "ftp://" login [ "/" fpath [ ";type=" ftptype ]] と定義されています.login より後ろの部分は path_segments に当たるわけですが, ; は fpath とその後ろの部分を区切る目的で使用されます.そこで,segment から ; と param を削除し,path_segments をftp URL の定義に適合するように修正しました.同様に login 部分はlogin = [ user [ ":" password ] "@" ] hostport と定義されており, userinfo はuser [ ":" password ] となっています.つまり,: が user と password を区切る目的で使用されるため,userinfo から : を取り除いたものを新たに user,password として定義し userinfo を修正しました. scheme は当然 ftp であり,スキームが ftp であるURI References としてはあり得ない選択部分を削除するなどして URI_reference や absoluteURI などを修正しました.このスクリプトから求めた
ftp URL の正規表現は 次のようになりました.\bftp://(?:(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])* (?::(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)?@)?(? :(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.)*[a-zA-Z](?:[-a-zA- Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?::[0-9]*)? (?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*(?:/(? :[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*(?:;type=[ AIDaid])?)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9 A-Fa-f])*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A -Fa-f])*)?以下がこれを直接代入して使うスクリプトになります.
$ftp_URL_regex = q{\bftp://(?:(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*} . q{(?::(?:[-_.!~*'()a-zA-Z0-9;&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)?@)?(?} . q{:(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.)*[a-zA-Z](?:[-a-zA-} . q{Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?::[0-9]*)?} . q{(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*(?:/(?} . q{:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*(?:;type=[} . q{AIDaid])?)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9} . q{A-Fa-f])*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A} . q{-Fa-f])*)?};
トップへ
ここの記述は内容が古くなっているので, こちら に新しく書き起こしました.
以降の記述は参考に残しておきます.
RFC 2821 とRFC 2822 はRFC 5321 とRFC 5322 によって obsolete となりました.
RFC 821 とRFC 822 はRFC 2821 ( 日本語訳 1~3章 4,5章 6章~ )とRFC 2822 ( 日本語訳 )によって obsolete となりました.メールアドレスについては
RFC 821 ( 日本語訳 )とRFC 822 ( 日本語訳 )に書かれています.perl5.6.0 以前の perl ではメールアドレスの正規表現を正確に記述することは できませんでした.Jeffrey E. F. Friedl 氏原著による 「詳説 正規表現」にはメールアドレスはネストした コメントを持つことができるので正規表現で表わすのは不可能であると 書いてあります.そこで,Jeffrey E. F. Friedl 氏はネストした コメントをあきらめて,次のような6,598バイト にも及ぶ 正規表現を作っています.https://resources.oreilly.com/examples/9781565922570/blob/master/email-opt.pl にソースコードがあります.
[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x 80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\ \x80-\xff\n\015()]*)*\)[\040\t]*)*(?:(?:[^(\040)<>@,;:".\\\[\]\000 -\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\ \\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\04 0\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\ xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80 -\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\0 15()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\x ff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t] *)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:" .\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xf f][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015()]*( ?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\ \x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*@[ \040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x8 0-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\ x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\03 7\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\ \\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\x ff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\ x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\ 040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80- \xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\01 5()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:" .\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80- \xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*( ?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\ 015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\ n\015()]*)*\)[\040\t]*)*)*|(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80- \xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff \n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^()<>@,;:".\\ \[\]\x80-\xff\000-\010\012-\037]*(?:(?:\([^\\\x80-\xff\n\015()]*(? :(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\ x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)|"[^\\\x80-\xff\ n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^()<>@,;:".\\\ [\]\x80-\xff\000-\010\012-\037]*)*<[\040\t]*(?:\([^\\\x80-\xff\n\0 15()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\x ff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t] *)*(?:@[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\ ([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)* \))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\] \000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])| \[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\ \\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]* (?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015() ]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\ \[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\ xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040 )<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\ 037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\ 040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80 -\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x 80-\xff\n\015()]*)*\)[\040\t]*)*)*(?:,[\040\t]*(?:\([^\\\x80-\xff\ n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80 -\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040 \t]*)*@[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\ ([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)* \))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\] \000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])| \[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\ \\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]* (?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015() ]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\ \[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\ xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040 )<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\ 037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\ 040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80 -\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x 80-\xff\n\015()]*)*\)[\040\t]*)*)*)*:[\040\t]*(?:\([^\\\x80-\xff\n \015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80- \xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\ t]*)*)?(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@ ,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80 -\xff][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015( )]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff] [^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)* (?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([ ^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\) )[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\0 00-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[ ^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\ 040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80 -\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x 80-\xff\n\015()]*)*\)[\040\t]*)*)*@[\040\t]*(?:\([^\\\x80-\xff\n\0 15()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\x ff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t] *)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:" .\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80 -\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xf f]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015() ]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^ \\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()] *(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015( )]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?! [^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\01 5\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?: (?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x 80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*>)
email-opt.pl を元に冗長部分を削り落としたのが 以下のスクリプトです.冗長部分を削り落としてもかなりの量です.
(注意:$ctrl は本当は '\000-\037\0177' の間違いです)
(注意:正確には,RFC822 では $CRlist に \n は含まれていません)# $email が正しいメールアドレスか判定する $esc = '\\\\'; $Period = '\.'; $space = '\040'; $tab = '\t'; $OpenBR = '\['; $CloseBR = '\]'; $OpenParen = '\('; $CloseParen = '\)'; $NonASCII = '\x80-\xff'; $ctrl = '\000-\037'; $CRlist = '\n\015'; $qtext = qq/[^$esc$NonASCII$CRlist\"]/; $dtext = qq/[^$esc$NonASCII$CRlist$OpenBR$CloseBR]/; $quoted_pair = qq<${esc}[^$NonASCII]>; $ctext = qq<[^$esc$NonASCII$CRlist()]>; $Cnested = qq<$OpenParen$ctext*(?:$quoted_pair$ctext*)*$CloseParen>; $comment = qq<$OpenParen$ctext*(?:(?:$quoted_pair|$Cnested)$ctext*)*$CloseParen>; $X = qq<[$space$tab]*(?:${comment}[$space$tab]*)*>; $atom_char = qq/[^($space)<>\@,;:\".$esc$OpenBR$CloseBR$ctrl$NonASCII]/; $atom = qq<$atom_char+(?!$atom_char)>; $quoted_str = qq<\"$qtext*(?:$quoted_pair$qtext*)*\">; $word = qq<(?:$atom|$quoted_str)>; $domain_ref = $atom; $domain_lit = qq<$OpenBR(?:$dtext|$quoted_pair)*$CloseBR>; $sub_domain = qq<(?:$domain_ref|$domain_lit)$X>; $domain = qq<$sub_domain(?:$Period$X$sub_domain)*>; $route = qq<\@$X$domain(?:,$X\@$X$domain)*:$X>; $local_part = qq<$word$X(?:$Period$X$word$X)*>; $addr_spec = qq<$local_part\@$X$domain>; $route_addr = qq[<$X(?:$route)?$addr_spec>]; $phrase_ctrl = '\000-\010\012-\037'; $phrase_char = qq/[^()<>\@,;:\".$esc$OpenBR$CloseBR$NonASCII$phrase_ctrl]/; $phrase = qq<$word$phrase_char*(?:(?:$comment|$quoted_str)$phrase_char*)*>; $mailbox = qq<$X(?:$addr_spec|$phrase$route_addr)>; print "ok\n" if $email =~ /^$mailbox$/o;
perl5.6.0 以前の perl では表現できなかったネストしたコメント 部分は,このスクリプトではと $Cnested
の代入文で定義されており, $comment
1回 だけネストを許した正規表現となっています.この2つ の代入文を以下のように変更することでメール アドレスの正規表現を正確に記述することができるようになります.use re 'eval'; $comment = qr<$OpenParen$ctext*(?:(?:$quoted_pair|(??{$comment}))$ctext*)*$CloseParen>;ただし,ここで使用している正規表現
は 実験的なものなので今後変更されたり削除されるかもしれませんので注意が必要です. また, (??{ code })
しているので,この点にも十分注意する必要があります. 何をどう注意する必要があるのかはマニュアルを読んでください. メールアドレスのパターンマッチが終わった時点で use re 'eval';
しておくことをお勧めします. no re 'eval';
メールアドレスが正しいかどうかを調べるには
モジュール またはEmail::Valid
モジュール を使うのがいいと思います. このモジュールを使えば,メールアドレスがMail::CheckUser
RFC 822 に書かれている文法的に正しいかどうかだけではなく, そのメールアドレスが実際に有効かどうかもある程度調べることができます. ただし,その場合はもちろんインターネットに接続されている必要があります. 詳しい使い方はマニュアルを読んでください.さて,ここまでで書いてきたメールアドレスというのは
From行 などで指定できるもののことでして,RFC 822 においては mailbox として定義されています. この mailbox をある文字列からメールアドレスを抽出する目的で使うのは 無茶というものです.そのような目的のときに必要とされるのは mailbox ではなく,addr-spec の方でしょう.mailbox やaddr-spec がどのようなものかと言いますと,たとえば,Foo Bar <foobar@example.com> というのは mailbox ですがaddr-spec ではありません.foobar@exmaple.com というのはaddr-spec だけから成る mailbox になります.そこで先ほどのスクリプトを修正し,ある文字列からメールアドレスを抽出する 目的で使うための
addr-spec の正規表現を以下のように作りました.# メールアドレスの正規表現 $mail_regex $esc = '\\\\'; $Period = '\.'; $space = '\040'; $OpenBR = '\['; $CloseBR = '\]'; $NonASCII = '\x80-\xff'; $ctrl = '\000-\037'; $CRlist = '\n\015'; $qtext = qq/[^$esc$NonASCII$CRlist\"]/; $dtext = qq/[^$esc$NonASCII$CRlist$OpenBR$CloseBR]/; $quoted_pair = qq<${esc}[^$NonASCII]>; $atom_char = qq/[^($space)<>\@,;:\".$esc$OpenBR$CloseBR$ctrl$NonASCII]/; $atom = qq<$atom_char+(?!$atom_char)>; $quoted_str = qq<\"$qtext*(?:$quoted_pair$qtext*)*\">; $word = qq<(?:$atom|$quoted_str)>; $domain_ref = $atom; $domain_lit = qq<$OpenBR(?:$dtext|$quoted_pair)*$CloseBR>; $sub_domain = qq<(?:$domain_ref|$domain_lit)>; $domain = qq<$sub_domain(?:$Period$sub_domain)*>; $local_part = qq<$word(?:$Period$word)*>; $addr_spec = qq<$local_part\@$domain>; $mail_regex = $addr_spec;このスクリプトは,先ほどのスクリプトから, 途中にコメントとスペースやタブがないように変更し,冗長部分を削除したものです. このスクリプトから求めた
addr-spec は以下のようになりました.(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\ \[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][ ^\\\x80-\xff\n\015"]*)*")(?:\.(?:[^(\040)<>@,;:".\\\[\]\000-\037\x 80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\ xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*"))*@(?:[^(\0 40)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000 -\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\]) (?:\.(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,; :".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x 80-\xff])*\]))*以下がこれを直接代入して使うスクリプトになります.
$mail_regex = q{(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\} . q{\[\]\000-\037\x80-\xff])|"[^\\\\\x80-\xff\n\015"]*(?:\\\\[^\x80-\xff][} . q{^\\\\\x80-\xff\n\015"]*)*")(?:\.(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x} . q{80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff])|"[^\\\\\x80-} . q{\xff\n\015"]*(?:\\\\[^\x80-\xff][^\\\\\x80-\xff\n\015"]*)*"))*@(?:[^(} . q{\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\0} . q{00-\037\x80-\xff])|\[(?:[^\\\\\x80-\xff\n\015\[\]]|\\\\[^\x80-\xff])*} . q{\])(?:\.(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,} . q{;:".\\\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\\\x80-\xff\n\015\[\]]|\\\\[} . q{^\x80-\xff])*\]))*};このメールアドレスの正規表現
を使って, $mail_regex
が正しいメールアドレスか判定するには次のように書きます. perl5.004以前の場合は の部分を \z
に置き換えてください. $(?!\n)
# $email が正しいメールアドレス(addr_spec)か判定する if ($email !~ /^$mail_regex\z/o) { print "不正なメールアドレスです\n"; }余談ですが,DoCoMo(i-mode) と J-Phone(J-Sky) ではメールアドレスとして
irregular.@docomo.ne.jp のように @ の直前が.(ピリオド) であるものも 使用できます.しかし,これはRFC 822 に適合しない不正なメールアドレスです.@ の前のlocal-part の部分では.(ピリオド) は必ず他の文字に挟まれていなければならないのです.したがって,.(ピリオド) が先頭にある場合と,@ の直前にある場合は不正なメールアドレスということになります. DoCoMo(i-mode)同士や J-Phone(J-Sky)同士でのメールのやりとりであれば 問題ありませんが,そうでなければ使用するべきではありません.
トップへperl スクリプトは
EUC-JP で書くトップへperl で日本語を扱うにはいろいろと注意しなければならないことがあります. なぜなら,日本語の文字コードには perl が特別な意味として解釈してしまう 文字が含まれているからです.たとえば,perl スクリプトを JIS で次のように書いたとします.
これを正常に実行することはできません.$str = "このTESTで充分"; $str =~ s/このTESTで充分/このテストで十分/; # JIS でも SJIS でも駄目 print $str, "\n";unmatched () in regexp となってしまうはずです. なぜなら,エスケープシーケンスのESC ( B が含まれているために,(
をグループ化のための開き括弧として 解釈してしまうからです.もちろん,このエラーは閉じ括弧の)
がないために括弧が対応していないというエラーです. それではこのスクリプトを SJIS で書いた場合はどうでしょう.今度はunmatched [] in regexp となってしまうはずです.なぜなら SJIS の「充」の文字コードは0x8F 0x5B であり,0x5B というのは ASCII の[
の文字コードであるからです.そこで SJIS の場合には正規表現でエラーにならないように, 次のようにパターンの部分を
と \Q
で挟んでエスケープするという回避方法があります. \E
ところが,これを実際に実行してみると文字化けして しまいます.なぜなら,SJIS の「十」の文字コードは$str = "このTESTで充分"; $str =~ s/\QこのTESTで充分\E/このテストで十分/; # これで SJIS でも大丈夫? print $str, "\n";0x8F 0x5C であり,0x5C というのは ASCII の\
の文字コードであるため,「分」の1バイト目 と合わせて特別な 意味として解釈しようとするためです.\
と「分」の1バイト目 を合わせたエスケープシーケンスというものは ありませんので,結果的に「十」の2バイト目 の\
は無視されることになります.このように SJIS には
2バイト目 が\
である文字が あるために文字化けしてしまいます.同様に,2バイト目 が@
である文字では配列と解釈されてしまうことがあります.2バイト目 が\
である文字については,その後ろに\
を書けば回避することができますが,2バイト目 が@
である文字についてはさらに別の回避手段を取らざるを得なくなります. ちなみに,SJIS で2バイト目 が\
である文字は 「―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭」 です.また,2バイト目 が@
である文字は全角スペースと 「ァА院魁機掘后察宗拭繊叩邸如鼻法諭蓮僉咫奸廖戞曄檗漾瓠磧紂隋蕁襦蹇錙顱鵝」 です.これらの文字以外にも SJIS では問題となる文字がまだまだあります.なお,さきほど SJIS の場合に
と \Q
で挟んでエスケープするという回避方法について 触れましたが,実はこの方法は完全ではありません. たとえば,次のスクリプトを見てください. \E
if ($str =~ /\Q$keyword\E/) { print "マッチした\n"; }このスクリプトのように,ある
キーワード を$keyword
と \Q
で挟めばエラーにならずにうまくパターンマッチできるという話があります. たしかにエラーにはなりませんが,たとえば SJIS で \E
のときに $str = 'テスト';
でパターンマッチを行なうと マッチしてしまいます.これは SJIS の「ス」の文字コードが $keyword = 'X';
0x83 0x58 であり,0x58 というのが ASCII の X の文字コードで あるためです.また,のときに $str = 'ca<b';
のときもマッチしてしまいます. これは「ca<b」という文字列の文字コード $keyword = 'モ=モ';
0x82 0x83 0x82 0x81 0x81 0x83 0x82 0x82 に対して,1バイト ずつずれた位置で 「モ=モ」という文字列の文字コード0x83 0x82 0x81 0x81 0x83 0x82 がマッチしてしまうからです.perl で日本語を扱うための手段の 1つが jperl を使うということです. jperl はオリジナルの perl にパッチをあてて,日本語を扱えるようにしたものです. Windows用の jperl は以下の場所(鈴木 紀夫さん提供)から入手することができます.
http://homepage2.nifty.com/kipp/perl/jperl/それでは最初のスクリプトを
EUC-JP で書いた場合はどうでしょうか.EUC-JP で書いた場合には正常に実行できるはずです.なぜなら,EUC-JP には JIS や SJIS のように perl が特別な意味として解釈してしまうような 文字が含まれていないからです.perl で日本語を扱うには perl スクリプトをEUC-JP で書くのが一番簡単な方法です.以下では,EUC-JP でスクリプトを書くことを前提としています.実は
EUC-JP のパターンマッチにおいても SJIS と同じように 間違ってマッチしてしまう場合があります.このことについては 「正しくパターンマッチさせる」を参照してください.
漢字コードを
EUC-JP に変換して処理するトップへperl スクリプトは
EUC-JP で書いたとしても, 入力した日本語の漢字コードが SJIS や JIS では正常な動作を期待することはできません.そこで何らかの処理を 行なうときには一度 EUC-JP に変換してから行ないます.perl スクリプトをEUC-JP で書き, 漢字コードがEUC-JP である日本語を 処理するというのが,perl で日本語を扱うときに一番問題が 起きにくい方法です.入力した日本語の漢字コードが
EUC-JP ではない場合,または, 漢字コードがわからない場合には,漢字コードをjcode.pl (歌代 和正さん作)を使ってEUC-JP に変換してあげます.を $str
EUC-JP に変換するには次のように書きます.# $str を EUC-JP に変換する require 'jcode.pl'; jcode::convert(\$str, 'euc');
の部分を 'euc'
や 'sjis'
にすれば, SJIS や JIS に変換できます.もし,入力した日本語の漢字コードが 'jis'
であるとわかっている場合には,次のように 明示的に指定することで内部で自動判別しないようにすることができます. $code
# 漢字コードが $code である $str を EUC-JP に変換する require 'jcode.pl'; jcode::convert(\$str, 'euc', $code);「漢字コードを調べる」で自動判別の判定精度を 上げて求めた
を使いたいときにもこの書式を 使います. $code
余談ですが,次のように
された変数に対して, 型グロブを使って変換しようと するのは間違いです. my
宣言# my 宣言された変数を変換するときの間違った例 require 'jcode.pl'; my $str = 'my 宣言された変数の型グロブはない'; jcode::convert(*str, 'euc');された変数の型グロブはないので,これでは 変換することはできません. my
宣言された変数の ハードリファレンスは求めることができるので,最初のスクリプトのように 常に my
宣言のように書くのが一番問題の起きにくい 書き方です. \$str
jcode.pl は以下の場所に最新バージョンが置いてあります.ftp://ftp.iij.ad.jp/pub/IIJ/dist/utashiro/perl/現在の最新バージョンはjcode.pl-2.13 です. これを取ってきてjcode.pl に名前を変更して使います.jcode.pl の使い方はjcode.pl の中に書かれています.よくわからなければ, 小塚 敦さんによる 「jcode.pl の私的な解説書」を読むといいかもしれません.Jcode.pm - jcode.pl の後継(小飼 弾さん作)というものも公開されています.
Jcode.pm は UNICODE に対応していますが,使用するにはjcode.pl のようにコピーするだけでは駄目で,ちゃんと インストールする必要があります.なお,Windows用 の perl である ActivePerl 5.6用に,コンパイル済みのパッケージが以下の場所(鈴木 紀夫さん 提供)で配布されています.http://homepage2.nifty.com/kipp/perl/Jcode/index.html
バージョン 2.10 以前のjcode.pl はスレッドが有効になっている perlでは使用することが できません.スレッドが有効になっている perl では特殊変数 や$_
はレキシカル変数となります.レキシカル変数とは @_
された変数のことです. このレキシカル変数というのは my
宣言することが できないのですが, local
宣言バージョン 2.10 以前のjcode.pl では関数の引数をした local
宣言型グロブ に代入しようとしているために正常に動作しません.最新バージョンの*_
jcode.pl 及びJcode.pm はスレッドが有効になっている perl でも正常に動作します.手元の perl のスレッドが有効になっているかどうかを調べるには
perl -V と入力し実行します.このときusethreads=undef となっていれば無効になっているのでjcode.pl を安心して使うことができます.perl5.005 より前の perl もスレッド機能がないので問題ありません. もし,スレッドが有効になっていた場合にはバージョン 2.10 以前のjcode.pl が使えないことはもちろんの こと,特殊変数 や$_
がレキシカル変数になっていることにも注意してスクリプトを書く必要があります. @_
漢字コードを調べる
トップへ# $str の漢字コードを調べる require 'jcode.pl'; ($match, $code) = jcode::getcode(\$str); $code = 'euc' if $code eq undef and $match > 0;jcode.pl のを使います. getcode
関数には $code
や 'euc'
, 'sjis'
といった文字列が入っています.詳しくは jcode.pl の中の説明を読んでください. 'jis'
ここで注意が必要なのは,漢字コードを正確に 調べることには限界があるということです.SJIS の漢字(第二水準)の一部や SJIS の半角カタカナ
2文字 はEUC-JP の漢字1文字 と区別がつきません.もし,漢字コードがEUC-JP か SJIS の両方の可能性があり,どちらか判断できないときにはは jcode::getcode()
undef
を返します.ただ, 厳密にはどちらか判断できないとは言え,半角カタカナが含まれていない場合にはほ とんどの場合EUC-JP であるので,上のスクリプトでは最終的にundef
ではなくEUC-JP としています.
は SJIS の半角カタカナを考慮せずに判定しています. このため,SJIS だと判断できる半角カタカナが含まれている文字 列でも jcode::getcode()
EUC-JP と間違ってしまうことがあります.そこで,次のよう に書くことで判定精度を上げることができます.これで SJIS を# $str の漢字コードを調べる require 'jcode.pl'; ($match, $code) = jcode::getcode(\$str); $code = 'euc' if $code eq undef and $match > 0; $ascii = '[\x00-\x7F]'; if ($code eq 'euc') { if ($str !~ /^(?:$jcode::re_euc_c|$jcode::re_euc_kana| $jcode::re_euc_0212|$ascii)*$/ox) { if ($str =~ /^(?:$jcode::re_sjis_c|$jcode::re_sjis_kana|$ascii)*$/o) { $code = 'sjis'; } } }EUC-JP と間違って判定する可能性を 減らすことができますが, その分処理に時間がかかってしまうことを忘れてはいけません.このようにして 自動判定の判定精度を上げて求めたは漢字コードを変換するときにも利用することができます. 漢字コードの変換に関しては 「漢字コードを $code
EUC-JP に変換して処理する」を参照.
全角文字が含まれているか判定する
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.全角文字は# $str に全角文字(半角カタカナを含まない)が含まれているか判定する if ($str =~ /[\xA1-\xFE][\xA1-\xFE]/) { print "含まれている\n"; }JIS X 0208 とJIS X 0212 なので, 半角カタカナであるJIS X 0201片仮名 は含みません.全角文字が 含まれているかどうかを判定するには,JIS X 0208 とJIS X 0212 の共通部分であり,ASCII やJIS X 0201片仮名 では現れないパターンを使って判定します. /[\xA1-\xFE][\xA1-\xFE]/
# $str に半角カタカナが含まれているか判定する if ($str =~ /\x8E/) { print "含まれている\n"; }半角カタカナが含まれているかどうかを判定するには,
EUC-JP ではを調べるだけでできます. /\x8E/
# $str に ASCII 以外が含まれているか判定する if ($str =~ /[\x8E\xA1-\xFE]/) { print "含まれている\n"; }ASCII 以外の文字が含まれているかを判定するには,
を調べることでできます. /[\x8E\xA1-\xFE]/
があれば \x8E
JIS X 0201片仮名 の1バイト目 でマッチし,があれば [\xA1-\xFE]
JIS X 0208 の1バイト目 か,JIS X 0212 の2バイト目 でマッチしますので, ASCII 以外の文字が含まれていることがわかります.
が $str
EUC-JP かどうかも わからないときはjcode.pl を使って調べることもできます.jcode.pl を使って 「漢字コードを調べる」で書いたスクリプトでの漢字コードを調べた結果が $str
undef
の場合は ASCII 以外の文字は含まれていないとすることができます. 逆に言えば,undef
ではない場合は ASCII 以外の文字が 含まれているとすることができます.このとき,次のように慌ててを使わずに,いきなり $match
undef
かどうかを調べる方法は間違っています.# $str に ASCII 以外が含まれているか判定するときの間違った例 require 'jcode.pl'; $code = jcode::getcode(\$str); if ($code eq undef) { print "ASCII以外は含まれていない\n"; print "この判断は間違い\n"; }は jcode::getcode()
EUC-JP か SJIS の両方の可能性があり,どちらか判断できないときにもundef
を返します.「漢字コードを調べる」で書いて あるようにを使って $match
undef
の場合を処理する必要があります.
文字が途切れているか判定する
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str の最後の文字が途切れているか判定する if ($str =~ /\x8F$/ or $str =~ tr/\x8E\xA1-\xFE// % 2) { print "最後の文字が途切れている\n"; }
EUC-JP で文字が途切れる可能性があるのは,JIS X 0201片仮名 (半角カタカナ)とJIS X 0208 (全角文字)とJIS X 0212 (補助漢字)です.JIS X 0212 は3バイト で表わされ,最初がで始まります.最初の条件は \x8F
が $str
で終わっていた場合, すなわち, \x8F
JIS X 0212 が1バイト目 で 途切れていた場合を表わしています. 次の条件がJIS X 0201片仮名 とJIS X 0208 が1バイト目 で途切れていた場合と,JIS X 0212 が2バイト目 で途切れていた場合です.は tr/\x8E\xA1-\xFE//
の中の, $str
JIS X 0201片仮名 とJIS X 0208 の1バイト目 と2バイト目 ,JIS X 0212 の2バイト目 と3バイト目 の数を数えています.この数がもし奇数ならば文字が 途切れていることがわかります.
全角英数字を半角英数字に変換する
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str の全角英数字を半角英数字に変換する require 'jcode.pl'; jcode::tr(\$str, '0-9A-Za-z', '0-9A-Za-z');
jcode.pl のを使います.この関数は全角文字に対応した tr
関数tr
です.詳しくはjcode.pl の中の説明を 読んでください.基本的にtr
なので,全角英数字以外にも全角スペースを半角スペースにするなどの変換も 次のように書くことで簡単にできます.# $str の全角スペースなどを半角スペースなどに変換する require 'jcode.pl'; jcode::tr(\$str, ' ()_@-', ' ()_@-');逆に,
第 1引数 と第 2引数 を逆にすれば, 半角文字を全角文字にすることもできます. 半角カタカナと全角カタカナの相互変換に関しては 「半角カタカナを全角カタカナに変換する」を参照.
半角カタカナを全角カタカナに変換する
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str の半角カタカナを全角カタカナに変換する require 'jcode.pl'; jcode::h2z_euc(\$str);
jcode.pl のを使います. h2z_euc
関数
正しくパターンマッチさせる
トップへ
および $str
は $pattern
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.perl で日本語を扱う場合にはスクリプトを
EUC-JP で書き, 漢字コードがEUC-JP である日本語を処理するというのが 一番問題が起きにくい方法であるということを 「perl スクリプトはEUC-JP で書く」と 「漢字コードをEUC-JP に変換して処理する」で述べました.しかし,それだけでは少し困ったことが 起きることがあります.たとえば,次のようなスクリプトを実行すると 間違ってマッチしてしまいます.# 間違ってマッチしてしまう例 $str = 'これはテストです'; $pattern = '好'; if ($str =~ /$pattern/) { print "マッチした\n"; }なぜこのようなことが起きてしまうのかというと,
EUC-JP の「ス」の文字コードは0xA5 0xB9 ,「ト」は0xA5 0xC8 ,「好」は0xB9 0xA5 であり,ちょうど「スト」の真ん中の部分が「好」と同じになるのでマッチして しまうのです.このようにずれた場所でマッチして しまっては困る場合には次のように書きます.# $str に $pattern を正しくマッチさせる $ascii = '[\x00-\x7F]'; $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; if ($str =~ /^(?:$ascii|$twoBytes|$threeBytes)*?(?:$pattern)/) { print "マッチした\n"; }なぜこのような書き方になるのか説明します.最初の間違ってマッチしてしまう スクリプトでは
というように無造作にマッチ させようとしたためにずれた場所でマッチしてしまいました.そこで,ずれた場所で マッチしないようにするには, /$pattern/
の前には日本語の 文字が何文字かあって,その後に $pattern
がくると いうことを明示的に書いてあげる必要があります. $pattern
EUC-JP での1文字 というのは1バイト文字 である ASCII,2バイト文字 であるJIS X 0201片仮名 (半角 カタカナ)とJIS X 0208 (全角文字),3バイト文字 で あるJIS X 0212 (補助漢字)のことです.これを正規表現で 表わしたのがの部分です.この文字が文字列の先頭から何文字か続いた後に (?:$ascii|$twoBytes|$threeBytes)
がくるということを正規表現で書いたのが 上のスクリプトです. $pattern
正規表現で任意の一文字を表わすには普通
を使いますが, 日本語の文字列に対するマッチングでは, .
(ピリオド)で書きたくなる場所を .
(ピリオド)とすればいいことに なります.最初のスクリプトの (?:$ascii|$twoBytes|$threeBytes)
も /$pattern/
だと思えば上のスクリプトのように なるのも納得していただけるのではないでしょうか. /^.*?(?:$pattern)/
日本語の文字列に対して正しくマッチさせる方法として,これまで 書いてきたように
EUC-JP での1文字 というものを ちゃんと意識して正規表現を書くという方法以外に,あらかじめマッチさせる 前に2バイト文字 と3バイト文字 の後ろに 文字の区切りがわかるように区切り文字をつけておくと いう方法があります. 具体的には次のように,マッチの対象となっている日本語の文字列と,マッチさせようとしているパターン $str
の両方に区切り文字をつける処理をしてから マッチングを行ないます.このスクリプトでは区切り文字として $pattern
を使っています. \000
# 区切り文字をつけて正しくマッチさせる(非常に遅い) $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; $pattern =~ s/($twoBytes|$threeBytes)/$1\000/og; $str =~ s/($twoBytes|$threeBytes)/$1\000/og; if ($str =~ /$pattern/) { print "マッチした\n"; }この方法ではマッチさせる前に区切り文字をつける処理を行なうことで, 正規表現そのものは普通に書くことができます.この方法はわかりやすいところは いいのですが,おそらくほとんどの 場合区切り文字を使わない最初のスクリプト よりも実行速度が遅いでしょう.
この
2つ 方法の特徴を考えてみます.区切り文字を使わない方法は, 前処理なしにすぐにパターンマッチを始めることができる.しかし,パターンマッチ そのものは正規表現が複雑なため少し遅い.区切り文字を使った方法は,あらかじめ 文字列全体に対し区切り文字を入れる前処理を行なう必要がある.ただ,パターン マッチそのものは正規表現が複雑にならないために速い.それでは実際に比較したらどうなるか調べてみました. パターンマッチが成功しなかった場合,文字列全体に対し検索を行なうことに なりますが,私がベンチマークをとってみたところ,区切り文字を使わない 方法の方が圧倒的に速かった (
約 15倍 ) です.パターン マッチが成功する場合には,文字列の途中で検索を止めることができるので, 文字列全体に対して必ず前処理を行なわなければならない区切り文字を使った 方法の方が遅いことは言うまでもありません.結局,区切り文字を使わない方法は 正規表現が複雑になった分パターンマッチそのものは少し遅くなりますが, 区切り文字を使う方法の方は,いかんせん区切り文字を入れる処理が遅すぎて パターンマッチそのものの速さが全然活きなかったようです.この結果からすると,データの中 からマッチするものだけ取り出すような処理には明らかに区切り文字を使わない 最初のスクリプトの方がいいと言えます.区切り文字を使った方法の方がいい 場合としては,前処理の遅さをパターンマッチの速さで 補えるほど何度も同じ文字列に対してパターンマッチを行なう場合です. もちろん,これら実行速度に関しては環境に依存する話ですので,実際に自分の 環境で試してみるのがいいでしょう.
次に,日本語の文字列を正しく置換する方法について説明します.次のような スクリプトが間違って置換してしまうということはすでに説明したとおりです.
# 間違って置換してしまう例 $str = 'これはテストです'; $pattern = '好'; $replace = '嫌'; $str =~ s/$pattern/$replace/g;次のように書くことで正しく置換することができます.
# $str の $pattern を $replace に正しく置換する $ascii = '[\x00-\x7F]'; $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; $str =~ s/\G((?:$ascii|$twoBytes|$threeBytes)*?)(?:$pattern)/$1$replace/g;このスクリプトの基本的な考え方は,
EUC-JP での1文字 というものをちゃんと意識して正規表現を書くマッチの 方法と同じです.ただ,マッチさせるだけの場合と違うのはと $1
を使っているところです. \G
を使うのは,置換する部分をマッチさせるときに $1
の前にある文字もいっしょにマッチさせる ことになるため,この部分を置換せずにそのまま残してあげる必要があるからです. そこで $pattern
の前の部分に当たる正規表現 $pattern
を括弧で囲って (?:$ascii|$twoBytes|$threeBytes)*?
で参照できるようにしています. $1
次に
の説明をします. \G
を使うのは \G
修飾子 がつけられているためです.g
修飾子 は,マッチするかどうか判定するだけならば 必要ないですし, また,g
1回 しか置換しない場合も必要ありません. そのときは修飾子 をつけるのをやめて,g
を文字列の先頭にマッチする \G
^
に変えることができます.逆に言えば,修飾子 をつけてg
の中の $str
をすべて置換したいときに,文字列の先頭にだけマッチする $pattern
^
を使うことができないということです.は \G
修飾子 がつけられている ときに,パターンマッチの開始位置にマッチします. つまり,g
は 一番最初は \G
^
と同じで,次からはのすぐ後ろでマッチします.わかりやすく簡単に 言うと, $pattern
はマッチするかどうかこれから調べようと している残りの部分の先頭にマッチすると言えます. \G
を使うことで,ずれた位置で \G
がマッチすることがないようになります. $pattern
置換の場合にも次のように区切り文字をつけて正しく置換する方法があります.
# 区切り文字をつけて正しく置換させる(非常に遅い) $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; $pattern =~ s/($twoBytes|$threeBytes)/$1\000/og; $str =~ s/($twoBytes|$threeBytes)/$1\000/og; $str =~ s/$pattern/$replace/g; $str =~ tr/\000//d; # $str =~ s/($twoBytes|$threeBytes)\000/$1/og;基本的に区切り文字をつけて正しくマッチさせる方法と同じです.ただ, マッチさせるだけの場合と違って,置換後に区切り文字を削除する必要があります. このスクリプトでは区切り文字に
を使っていて, 置換後にこの区切り文字を \000
tr
を使って削除しています.ところが,の中に初めから含まれていた $str
もいっしょに削除してしまいます. \000
tr
を使って削除できるのはの中に区切り文字と同じ $str
が含まれていないという前提が必要です.もし, \000
の中に $str
が含まれて いるかもしれない場合には \000
tr
を使って区切り文字を削除するのを止めて,を $str =~ tr/\000//d;
に変更します. $str =~ s/($twoBytes|$threeBytes)\000/$1/og;
実行速度について
2つ の方法を比較してみました. 与えた文字列に対して全く置換するところがなかった場合には,区切り文字を使わない 方法の方が圧倒的に速かった (約 35倍 ) です.全部の文字を置換する必要がある文字列を与えた 場合でも,区切り文字を使わない方法の方が 4割程度速かったです.もし,区切り文字を使う場合の 方法で後処理にtr
を使わなかった場合には更にスピード差が出るでしょう. 結局,置換の場合でも区切り文字を使う場合は,前処理と後処理に時間がかかり すぎるということが言えます. 実行速度に関しては環境に依存する話なので,どちらが速いか自分の環境で試してみる のが一番だということは言うまでもありません.さて,ここまでの話では
は Perl の文法的に 正しい正規表現という前提でした.ですから, たとえば $pattern
開き括弧 にマッチさせたい場合には(
というようにエスケープする必要があります.CGI などにおいて,ユーザ入力の文字列でマッチするものを検索したい場合などには, 入力された文字列を正規表現として解釈するのではなく,その文字列そのもので 検索したい場合がほとんどでしょう.そのようなときに, \(
としてパターンマッチを行なうと,先ほどの例で 挙げた $pattern
開き括弧 などが入力されたときに正規表現として 正しくないとエラーになってしまいます.そこで正規表現で特別な意味として 解釈される開き括弧などのメタ文字はエスケープして パターンマッチさせる必要があります.(
そのためには,ユーザ入力
に対して, これまでに書いたスクリプトの $keyword
の部分を $pattern
に変更して,パターンマッチの場合は, \Q$keyword\E
置換の場合は,if ($str =~ /^(?:$ascii|$twoBytes|$threeBytes)*?\Q$keyword\E/) { print "マッチした\n"; }というようにします.$str =~ s/\G((?:$ascii|$twoBytes|$threeBytes)*?)\Q$keyword\E/$1$replace/g;から \Q
までメタ文字が無視されるようになります. \E
次に実行速度を上げるための方法を
1つ 書いておきます. これまで 書いてきたように,日本語の文字列に対して正しくマッチさせたり置換する ためには少々複雑な正規表現を使う必要があります.そのため,その複雑に なった分だけ実行速度が遅くなってしまいます. これは,大量のデータの中から 検索したり置換したりする場合には非常に時間がかかるようになってしまうことを 意味します.ここで少し考えてみてください.大量のデータの中から検索するとき, そのほとんどの場合はマッチしないのです. つまり,マッチしないのですから正しくマッチさせる必要はないのです. そこで
を検索したいときには, 次のようにすることでほとんどの 場合実行速度を上げることができます. $pattern
if ($str =~ /$pattern/) { if ($str =~ /^(?:$ascii|$twoBytes|$threeBytes)*?(?:$pattern)/) { print "マッチした\n"; } }
の場合は, $keyword
という正規表現は使わずに次のように /\Q$keyword\E/
を使います. index
関数if (index($str, $keyword) > -1) { if ($str =~ /^(?:$ascii|$twoBytes|$threeBytes)*?\Q$keyword\E/) { print "マッチした\n"; } }
は正規表現 に比べて実行速度が圧倒的に速いので,なんでもかんでも 正規表現ではなく, index
関数が使えないか常に 考えたいものです. index
関数※上記の内容について
最近の perl(perl5.8.8等)では,を使うよりも, index
関数という正規表現を使った方が 速いようです.実行速度は perl のバージョンや実行環境,スクリプト等に 影響されるため,必要に応じてベンチマークをとるのがよいでしょう. /\Q$keyword\E/
これまで書いてきた方法は
EUC-JP だけではなく,SJIS の場合に も応用することができます.SJIS の場合にも SJIS での1文字 という ものを意識して正規表現を書くことになります.SJIS での1文字 に ついては,「文字の正規表現」を参照.実は,
EUC-JP でperl5.005 以降という条件におい ては,ほとんどの場合にこれまで書いてきた方法よりも実行速度 が速く,より扱いやすい方法があります.以下に まとめて列挙します.# EUC-JP で perl5.005 以降限定の方法 $eucpre = qr{(?<!\x8F)}; $eucpost = qr{ (?= (?:[\xA1-\xFE][\xA1-\xFE])* # JIS X 0208 が 0文字以上続いて (?:[\x00-\x7F\x8E\x8F]|\z) # ASCII, SS2, SS3 または終端 ) }x; if ($str =~ /$eucpre(?:$pattern)$eucpost/) { # パターンマッチ print "マッチした\n"; } if ($str =~ /$eucpre\Q$keyword\E$eucpost/) { # キーワードマッチ print "マッチした\n"; } $str =~ s/$eucpre(?:$pattern)$eucpost/$replace/g; # パターン置換 $str =~ s/$eucpre\Q$keyword\E$eucpost/$replace/g; # キーワード置換いずれの場合においても,
と $eucpre
で挟むだけになります.この方法は 正規表現の後読み(lookbehind) と先読み(lookahead) を使っています.後読みは $eucpost
,先読みは (?<regex)
という正規表現になります.このスクリプトでは後読みは否定後読みの (?=regex)
の方を使っています. (?<!regex)
この方法はマッチさせたい正規表現にマッチしたものがずれた位置ではないこと を後読みと先読みによって保証しています.具体的には,後読みの部分で
JIS X 0212 の2バイト目 からずれてマッチしてい ないかチェックしています.JIS X 0212 の2バイト目 からマッチしていた場合は,マッチした部分の直前にJIS X 0212 の1バイト目 ,すなわち,があることになります.しかし,後読みによって \x8F
ではないことが保証されているので, \x8F
JIS X 0212 の2バイト目 からずれてマッチすることは なくなります.また,
JIS X 0208 の2バイト目 からずれてマッチし てしまう場合とJIS X 0212 の3バイト目 からずれてマッ チしてしまう場合についてのチェックは先読み部分で行なっています.もし,このよ うな位置からずれてマッチしてしまった場合,先読み部分にマッチしなくなります. 先読み部分はマッチした部分の後ろに正しくEUC-JP の文字列が続い ているかどうかをチェックしています.具体的には,マッチした部分の後ろから,JIS X 0208 以外のものが来るまで,正しくJIS X 0208 文字が続いているかどうかをチェックしています.この方法では先読みと後読みだけで正しくマッチさせることができます.先読み と後読みはどちらもそれ自体にはマッチした文字列を含まない
0文字幅 の正規表現です.したがって,置 換する場合に置換後の文字列の中にや $eucpre
にマッチした部分のことを考えての $eucpost
のようなものを必要としなくなります. $1
前後の空白文字(全角スペース含)を削除する
トップへ# $str の先頭の空白文字(全角スペース含)を削除する $str =~ s/^(?:\s|$Zspace)+//o; # $str が EUC-JP の場合 $str =~ s/^(?:\s|$Zspace_sjis)+//o; # $str が SJIS の場合 # $str の末尾の空白文字(全角スペース含)を削除する $str =~ s/^($character*?)(?:\s|$Zspace)+$/$1/o; # $str が EUC-JP の場合 $str =~ s/$eucpre(?:\s|$Zspace)+$//o; # $str が EUC-JP の場合(perl5.005以降) $str =~ s/^($character_sjis*?)(?:\s|$Zspace_sjis)+$/$1/o; # $str が SJIS の場合上記スクリプトで使用している変数については 「文字の正規表現」 および 「正しくパターンマッチさせる」 を参照してください.
前後の全角スペースを含む空白文字を削除するとき,次のように書くと 間違って削除してしまう可能性があります.
# $str の末尾の空白文字(全角スペース含)を削除する(間違い) $str =~ s/(?:\s|$Zspace)+$//o; # $str が EUC-JP の場合 $str =~ s/(?:\s|$Zspace_sjis)+$//o; # $str が SJIS の場合先頭の空白文字を削除する場合については特に問題ありませんが, 末尾の空白文字を削除するときには全角スペースがマルチバイト文字の一部などに 間違ってマッチしてしまう可能性があります.例えば,SJIS で
の場合, 間違って末尾を削除してしまいます.詳しくは, 「perl スクリプトは $str = '@=@';
EUC-JP で書く」 および 「正しくパターンマッチさせる」 を参照してください.
文字単位に分割する
トップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str を文字単位に分割して配列 @chars に代入する $ascii = '[\x00-\x7F]'; $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; @chars = $str =~ /$ascii|$twoBytes|$threeBytes/og;最後の代入文をわかりやすく,
と書いてもほぼ同等の動作をします.先にそのように書いた場合の説明をします. @chars = ($str =~ /($ascii|$twoBytes|$threeBytes)/og;
EUC-JP での1文字 がと正規表現で表わすことができることを 「正しくパターンマッチさせる」で述べました. これを括弧で囲ってグループにしています.一方,この代入文は $ascii|$twoBytes|$threeBytes
配列 への代入なので, 右辺はリストコンテキストで実行されます. パターンマッチをリストコンテキストで実行すると,グループにされた正規表現に マッチする文字列のリストが返されます.つまり,@chars
というリストが返されます. さらに ($1, $2, $3,…)
修飾子 がつけられていますので,g
というリストが 返されることになります.この場合はグループにされている正規表現が ($1, $2, $3,…, $1, $2, $3,…)
1つ ですので,ちょうどEUC-JP での1文字 に分割されたリストが返されることになります.最初のスクリプトでは
への代入文の右辺全体を 括弧で囲っていませんが,これは @chars
=
よりもの方が演算子の優先順位が高いので, =~
を先に実行してしまうということはありません. @char = $str
また,正規表現の全体を括弧で囲っていませんが,
修飾子 がつけられているパターンマッチをリストコンテキストで実行したとき, 正規表現の中に括弧がg
1つ もなかった 場合は自動的に正規表現全体を括弧で囲ってあるものと して動作します.このとき,正規表現全体を括弧で囲った 場合よりも実行速度が速いです.後からとして使用するわけでもなく正規表現全体を括弧で囲う ような場合には括弧をつけない方がよいでしょう. $1
特定の長さで折り返す
トップへ# $str を $bytesバイトで折り返す require 'fold.pl'; while (length($str)) { (my $folded, $str) = fold($str, $bytes); print $folded, "\n"; }
fold.pl (歌代 和正さん作)を使うのが簡単です.fold.pl を 使わず,「文字が途切れているか判定する」で書いたように 文字が途切れていないか判定しながらを使って折り返すという方法もありますが, わざわざ書く必要はないでしょう. substr
関数の fold
関数第 3引数 に 1 を指定すれば, 折り返した結果に満たない場合には スペースを補って $bytes
バイトになるようにすることが できます.また, $bytes
バイト第 4引数 に 1 を指定すれば単語境界で折り返すようになります. 詳しくはfold.pl の中の説明を読んでください.なお,fold.pl は補助漢字と SJIS の半角カタカナには対応して いません.また,EUC-JP の半角カタカナは2バイト文字 として扱いますので,半角カタカナが混じっていると 表示幅にずれが発生します.表示幅をそろえたい場合には,半角カタカナを あらかじめ全角カタカナに変換しておくか,折り返すバイト数を適当に処理して あげる必要があります.
Jcode.pm のを使っても同じことができますが,単語境界で折り返したりはできません. jfold
関数おまけとして,半角カタカナに対応した禁則処理しつつ折り返すスクリプトを 載せておきます.このスクリプトは
EUC-JP で書かれ,も $str
EUC-JP という前提ですので,必要ならばあらかじめEUC-JP に変換しておいてください.漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $str を禁則処理しつつ折り返す require 'fold.pl'; require 'jcode.pl'; $no_begin = "!%),.:;?]}¢°’”‰′″℃、。々〉》」』】〕" . "ぁぃぅぇぉっゃゅょゎ゛゜ゝゞァィゥェォッャュョヮヵヶ" . "・ーヽヾ!%),.:;?]}"; # 行頭禁則文字 $no_begin_jisx0201 = "。」、・ァィゥェォャュョッー゛゜"; jcode::z2h_euc(\$no_begin_jisx0201); $no_begin .= $no_begin_jisx0201; # 行頭禁則文字(半角カタカナ) $no_end = "\$([{£\‘“〈《「『【〔$([{¥"; # 行末禁則文字 $no_end_jisx0201 = "「"; jcode::z2h_euc(\$no_end_jisx0201); $no_end .= $no_end_jisx0201; # 行末禁則文字(半角カタカナ) $allow_end = $no_begin; # ぶら下げ行頭禁則文字 $del_space = '(?:\s|\xA1\xA1)'; # 削除する行頭行末空白 $basebytes = 74; # 基本長 $maxbytes = 76; # 最大長 $ascii = '[\x00-\x7F]'; $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; map {$no_begin{$_} = 1;} ($no_begin =~ /$ascii|$twoBytes|$threeBytes/og); map {$no_end{$_} = 1;} ($no_end =~ /$ascii|$twoBytes|$threeBytes/og); map {$allow_end{$_} = 1 + /[\xA1-\xFE]/ - /\x8E/;} ($allow_end =~ /$ascii|$twoBytes|$threeBytes/og); sub fold_properly { my $str = shift; my($folded, $strtmp, $bytestmp, $begin_char, $end_char, $flag); $flag = 1; # 行頭禁則処理状態(1:ぶら下げ, 0:追い出し) $bytestmp = $basebytes; $str =~ tr/\t\n\r\f/ /; # 空白文字をスペースに変換 $str =~ s/^$del_space+//o; # 行頭空白削除 ($begin_char) = %no_begin; # 行頭禁則文字を 1文字代入 while ($no_begin{$begin_char} or $no_end{$end_char}) { ($folded, $strtmp) = fold($str, $bytestmp, 0, 1); while (length($folded) - ($folded =~ tr/\x8E//) <= $basebytes and $strtmp ne '' and $flag) { # 半角カタカナのための表示幅処理 ($folded, $strtmp) = fold($str, $bytestmp, 0, 1); my ($folded_tmp, $strtmp_tmp) = fold($str, $bytestmp + 1, 0, 1); if (length($folded_tmp) - ($folded_tmp =~ tr/\x8E//) <= $basebytes) { ($folded, $strtmp) = ($folded_tmp, $strtmp_tmp); $bytestmp++; } else { last; } } ($begin_char) = $strtmp =~ /^$del_space*($ascii|$twoBytes|$threeBytes)/o; ($end_char) = $folded =~ /($threeBytes|$twoBytes|$ascii)$/o; if ($flag) { # ぶら下げ禁則処理 if ($no_begin{$begin_char} and $allow_end{$begin_char}) { # ぶら下げ可能 if (length($folded) - ($folded =~ tr/\x8E//) + $allow_end{$begin_char} <= $maxbytes) { $bytestmp++; } else { $flag = 0; $bytestmp = $basebytes - 1 + ($folded =~ tr/\x8E//); } } else { $flag = 0; $bytestmp--; } } else { $bytestmp--; } if ($bytestmp == 0) { # 禁則処理不可能 ($folded, $strtmp) = fold($str, $basebytes, 0, 1); last; } } $folded =~ s/^((?:$ascii|$twoBytes|$threeBytes)*?(?=$del_space)) $del_space+$/$1/ox; # 行末空白削除 ($folded, $strtmp); } while (length($str)) { (my $folded, $str) = fold_properly($str); print $folded, "\n"; }
Base64 エンコード・デコードするトップへ
は $str
EUC-JP という前提ですので, 必要ならばあらかじめEUC-JP に変換しておいてください. 漢字コードの変換に関しては 「漢字コードをEUC-JP に変換して処理する」を参照.# $data を Base64エンコードして $encoded_data を求める use MIME::Base64; $encoded_data = encode_base64($data);
Base64 エンコードするには,モジュール のMIME::Base64
を使います. encode_base64
関数Base64 エンコード・デコードについてはRFC 2045 ( 日本語訳 )に書かれています.これによるとBase64 エンコード した出力ストリームの各行は76文字 以内でなければならないと 書かれています.は encode_base64
関数第 2引数 を 指定しないで呼んだ場合には自動的に76文字 ごとに改行コードを 入れて折り返してくれます.# $encoded_data を Base64デコードして元のデータ $data に戻す use MIME::Base64; $data = decode_base64($encoded_data);
Base64 デコードするには,モジュール のMIME::Base64
を使います. decode_base64
関数には $encoded_data
76文字 ごとに 折り返すために挿入されている改行コードが入ったままでもかまいません.次に
encoded-word について説明します.encoded-word についてはRFC 2047 ( 日本語訳 )に書かれています.encoded-word というのは=?charset?encoding?encoded-text?= という形をしたものです.たとえば=?ISO-2022-JP?B?GyRCTmMbKEI=?= はという文字列を "例"
encoded-word にしたものです.ここでは encoding に B を指定したencoded-word について説明します.encoding が B というのは
encoded-text の部分がBエンコード されたものであることを 表わしています.Bエンコード というのはBase64 エンコードと同じエンコード方法ですが,encoded-word の場合はBase64 エンコードとは呼ばずにBエンコード と呼びます.# $str を Bエンコードして encoded-word に変換する(不完全) require 'jcode.pl'; use MIME::Base64; jcode::convert(\$str, 'jis', 'euc', 'z'); $str = '=?ISO-2022-JP?B?' . encode_base64($str, '') . '?=';
Bエンコード するにはを使えばいいのですが, encode_base64
関数第 2引数 を指定しない場合は エンコードした結果に改行コードがついてしまうので,空文字列を指定して 改行コードがつかないようにしています. また,charset にISO-2022-JP を指定する都合上, あらかじめを JIS に変換する必要があります.正確には $str
ISO-2022-JP に変換する必要があります.ISO-2022-JP に変換するには基本的に JIS に変換してあげればいいのですが,ISO-2022-JP では半角カタカナを使うことができません.そこで 半角カタカナが含まれていた場合には全角カタカナに変換する必要があります. これをやるにはの jcode::convert
関数第 4引数 にを指定してあげます. 'z'
encoded-word に変換する基礎はこれだけなのですが, これはあくまでも基礎であってRFC 2047 を満たすことができない 不完全なものです.RFC 2047 にはencoded-word に変換する上で 守らなければならない決まりについて.だいたい次のようなことが書かれています.
encoded-word は75バイト 以内でなければならない.encoded-word を含む行は76バイト 以内でなければならない.encoded-word はそれぞれ独立してデコード可能でなければならない.encoded-text をデコードした文字列の文字コードは,最後に ASCII が指定された状態でなければならない.encoded-word が現れる出現位置に関する決まり.
- Subject や Comment のヘッダフィールドなどの,
'text' 内に出現."(" と")" で区切られた'comment' 内に出現.- From や To,CC ヘッダなどで,
'phrase' 内に出現.'addr-spec' 内で出現してはならない.'quoted-string' 内で出現してはならない.などなど.- 隣り合う
encoded-word の間の'linear-white-space' は無視する.1 から 4 までが
encoded-word に変換するときに関係してきます. さきほどのスクリプトでは 3 と 4 についてはクリアしていますが,1 と 2 については全然気にしていません.1 と 2 についても対応するためには少々困った 問題が起きます.まず,1 についてですが,
encoded-word の長さが75バイト を超えるような場合には,Bエンコード する 対象を短くして,2つ 以上のencoded-word に分けて 変換しなければなりません.2つ 以上のencoded-word に分けるために,Bエンコード した後のencoded-text を 3 が満たされるようにうまく分割することもできますが,それでは 4 を満たすことができなくなってしまいます.4 を満たしつつ対象を短くするには, 適当なところで対象の文字列を分割しては駄目で, ちゃんと日本語の文字単位で短くしなければ なりません.つまり,漢字などの2バイト文字 や3バイト文字 の途中で分割しては駄目だということです. 日本語の文字単位で短くすることができたら,後はjcode.pl を使って JIS に 変換すれば,自動的に最後の文字コードが ASCII の状態になるようにしてくれます.次に,2 についての困った問題というのを説明します.
encoded-word を含む行が76バイト 以内でなければならないということは,encoded-word に変換するときに,変換した後の行が76バイト 以内になっているようにencoded-word の長さを調整しなければならないということになります.もし,encoded-word に変換するとその行が76バイト を超えて しまう場合には,改行して折り返す必要があります.以上が
encoded-word への変換そのものについての少々困った問題 ということになるのですが,実はそれ以前に一番困った問題というのがありまして, それが 5 です.つまり,どの部分をencoded-word に変換すれば いいのか,ということが一番問題なのです.同様に,どの部分をデコードしたら いいのかというのも問題になります.文字列を与えられてうまく処理しろと 言われたら字句解析や構文解析が必要になってしまいます.ここではとても そこまではできませんので,encoded-word に変換したい部分,逆変換 したい部分を与えられた場合のスクリプトを書きます.# $str を encoded-word に変換し $line に追加する require 'jcode.pl'; use MIME::Base64; $ascii = '[\x00-\x7F]'; $twoBytes = '[\x8E\xA1-\xFE][\xA1-\xFE]'; $threeBytes = '\x8F[\xA1-\xFE][\xA1-\xFE]'; sub add_encoded_word { my($str, $line) = @_; my $result; while (length($str)) { my $target = $str; $str = ''; if (length($line) + 22 + ($target =~ /^(?:$twoBytes|$threeBytes)/o) * 8 > 76) { $line =~ s/[ \t\n\r]*$/\n/; $result .= $line; $line = ' '; } while (1) { my $encoded = '=?ISO-2022-JP?B?' . encode_base64(jcode::jis($target, 'euc', 'z'), '') . '?='; if (length($encoded) + length($line) > 76) { $target =~ s/($threeBytes|$twoBytes|$ascii)$//o; $str = $1 . $str; } else { $line .= $encoded; last; } } } $result . $line; } $line = add_encoded_word($str, $line);実行例 $line = 'Subject: '; $str = 'これはテストです.This is test.'; $line = add_encoded_word($str, $line); print $line, "\n"; 実行結果 Subject: =?ISO-2022-JP?B?GyRCJDMkbCRPJUYlOSVIJEckOSElGyhCVGhpcyBpcyB0ZXN0?= =?ISO-2022-JP?B?Lg==?=このスクリプトは
に $line
を $str
encoded-word に変換してから追加します.がかなり長い場合は, $str
encoded-word が速く75バイト 以内になるように当たりをつけてからやった方が いいのですがこのスクリプトでは行なっていません. また,どの部分をencoded-word にするかですが,RFC 2047 には本来encoded-word に変換する必要のないもの,つまり,ASCII だけから 成る単語まで変換するのは推奨できないと 書かれています.ですから,実行例のように is や test. までいっしょにencoded-word に変換 するのはあまりいい例とは言えません.これについては,Subject などの unstructured header の場合に対応したスクリプトを次に書きます.# unstructured header $header を MIMEエンコードする # add_encoded_word() については上のスクリプトを参照 sub mime_unstructured_header { my $oldheader = shift; my($header, @words, @wordstmp, $i) = (''); my $crlf = $oldheader =~ /\n$/; $oldheader =~ s/\s+$//; @wordstmp = split /\s+/, $oldheader; for ($i = 0; $i < $#wordstmp; $i++) { if ($wordstmp[$i] !~ /^[\x21-\x7E]+$/ and $wordstmp[$i + 1] !~ /^[\x21-\x7E]+$/) { $wordstmp[$i + 1] = "$wordstmp[$i] $wordstmp[$i + 1]"; } else { push(@words, $wordstmp[$i]); } } push(@words, $wordstmp[-1]); foreach $word (@words) { if ($word =~ /^[\x21-\x7E]+$/) { $header =~ /(?:.*\n)*(.*)/; if (length($1) + length($word) > 76) { $header .= "\n $word"; } else { $header .= $word; } } else { $header = add_encoded_word($word, $header); } $header =~ /(?:.*\n)*(.*)/; if (length($1) == 76) { $header .= "\n "; } else { $header .= ' '; } } $header =~ s/\n? $//mg; $crlf ? "$header\n" : $header; } $header = mime_unstructured_header($header);実行例 $header = "Subject: ASCII 日本語 ASCIIと日本語 ASCII ASCII\n"; $header = mime_unstructured_header($header); print $header; 実行結果 Subject: ASCII =?ISO-2022-JP?B?GyRCRnxLXDhsGyhCIEFTQ0lJGyRCJEhGfEtcGyhC?= =?ISO-2022-JP?B?GyRCOGwbKEI=?= ASCII ASCIIこのスクリプトは前述のスクリプトの関数
を利用しています. 前述のスクリプトの最後の add_encoded_word()
を削除し,このスクリプトに変更して使います. $line = add_encoded_word($str, $line);
このスクリプトの前半部分で単語ごとに分割しています.ここで分割された 単語ごとに,ASCII だけから成る単語かどうかを判定して
encoded-word に変換するかどうかを決定していきます.このとき 6 に注意する必要があります.デコードのときにencoded-word の間の'linear-white-space' は無視されるのですが,これは1行 の長さが長くなってしまう場合に,encoded-word を分割するために挿入された本来不必要な'linear-white-space' を削除するためのものです.しかし,元から存在する'linear-white-space' の両側をencoded-word に変換してしまうと,デコードのときに間違って削除されてしまうこと になります. そこで,'linear-white-space' の両側をencoded-word に変換する必要がある場合には,'linear-white-space' を含めた両側の 単語を1つ のencoded-word として変換します.# $str を Bデコードして encoded-word を元に戻す require 'jcode.pl'; use MIME::Base64; $lws = '(?:(?:\x0D\x0A|\x0D|\x0A)?[ \t])+'; $ew_regex = '=\?ISO-2022-JP\?B\?([A-Za-z0-9+/]+=*)\?='; $str =~ s/($ew_regex)$lws(?=$ew_regex)/$1/gio; $str =~ s/$lws/ /go; $str =~ s/$ew_regex/decode_base64($1)/egio; jcode::convert(\$str, 'euc', 'jis');このスクリプトは与えられた文字列
の中の $str
encoded-word を元に戻します.隣り合うencoded-word の間の'linear-white-space' は無視します.encoded-word は"(" の直後であるとか,'linear-white-space' の直後であるような場合にencoded-word であって,そうでない場合は一見encoded-word に見えても,偶然そういう 文字列であると解釈し, 勝手に元に戻そうとすべきではありません.しかし,このスクリプトではencoded-word に見えたものはすべて元に戻してしまいますので, 文字列を与える方でその判定を行ない, 元に戻しても問題ないものだけを与える必要があります.たとえば, $str
のときは $str = q{"=?ISO-2022-JP?B?GyRCTmMbKEI=?="};
quoted-string であるので,この中にencoded-word が現れるはずがありません.これを勝手に元に戻そうとしてはいけません.
古い Outlook Express などはencoded-word に変換したものをダブルクォートで囲んでquoted-string にするので,RFC 2047 を満たすことができません.Outlook Express 5 ではこの点は 修正されたようです.しかし,Outlook Express 5 を含む ほとんどのメーラーはencoded-word を含む行が76バイト 以内でなければならないという制約を 満たしていません.
encoded-word への変換を行なうスクリプトとして,mime_pls (mimew.pl ) (生田 昇さん作)というものも公開されています.しかし,これもRFC 2047 を完全に満たしているわけではありません.encoded-word への変換に関しては,Subject 行 やFrom 行 の違いを 考慮せずに同じコメント処理をしてしまいます. また,word単位で行なっていないので, たとえばのような文字列を変換,逆変換を行なうと $str = "testテスト";
のように余分なスペースが入ってしまいます. 特殊変数 "test テスト"
, $`
, $&
を使用しているので, すべてのパターンマッチの速度が少し遅くなってしまう点は改良の余地があります. $'
encoded-word からの逆変換に関しては,さきほど述べたように一見encoded-word に見えるものまで元に戻してしまいます.これを 正しく行なうためにはどうしても構文解析が必要になります.
Jcode.pm の MIMEエンコード 関数と MIMEデコード関数 mime_encode
はバージョン 0.63以降で上記のスクリプ トが採用されています. mime_decode
RFC 2047 を完全に満たしているencoded-word への変換を行なうスクリプトとしては IM(Internet Message) のがあります. 標準モジュールではないので,使うためには IM をインストールする必要があります. 使い方は IM::Iso2022jp
モジュールIso2022jp.pm の中身を見てください.
URIエスケープ・アンエスケープする
'エスケープ' という文字列を'%a5%a8%a5%b9%a5%b1%a1%bc%a5%d7' のようにURIエスケープ するには次のように書きます.# $str を URIエスケープする $str =~ s/(\W)/'%' . unpack('H2', $1)/eg;逆に
'%a5%a8%a5%b9%a5%b1%a1%bc%a5%d7' という文字列をURIアンエスケープ して'エスケープ' という文字列に戻すには次のように書きます.# $str を URIアンエスケープする $str =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('H2', $1)/eg;私がベンチマークをとって調べた限りでは,上記のように URIエスケープ・アンエスケープする 方法が一番実行速度が速いでしょう. これ以外の方法としては
を使わずに unpack
関数と sprintf
関数を使うとか, ord
関数をフォーマット pack
関数'H2'
で使わずにと hex
関数, あるいは, chr
関数と hex
関数をフォーマット pack
関数'C'
で使うとか,修飾子 を使うとか,i
を使うとかいろいろありますが, 特に書く必要はないでしょう. また, {2}
のようにアルファベットを大文字に変換してもいいのですが,その場合は '%A5%A8%A5%B9%A5%B1%A1%BC%A5%D7'
と sprintf
関数を使った方法となり,処理が遅くなります. ord
関数また,ハッシュと
演算子 を使って,次のように計算結果を再利用する方法がありますが,CGI などで使う 程度ではほとんどの場合上記のスクリプト より遅いでしょう.||=
# $str を URIエスケープする(再利用版) $str =~ s/(\W)/$escape{$1} ||= '%' . unpack('H2', $1)/eg;# $str を URIアンエスケープする(再利用版) $str =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/$unescape{$1} ||= pack('H2', $1)/eg;再利用版が遅い理由は,再利用しようとする計算部分,つまり,
や '%' . unpack('H2', $1)
がそれほど遅い処理ではないからです. この部分が遅い処理である場合には,一度計算した結果を数回再利用することで 十分に効果が出ますが,今回の場合のようにそれほど遅い処理ではない場合には, ハッシュを使用したり pack('H2', $1)
による演算のオーバー ヘッドのために逆に遅くなってしまいます.私がベンチマークをとって調べたところ, ||=
URIアンエスケープ の再利用版では再利用率700% ぐらい, つまり,一度計算したすべての結果を7回 再利用したころから ようやく効果が出始めるという程度でした.逆に言えば,大量の文章を処理しようとした場合には効果があるということ なのですが,そのような場合は次のようにあらかじめ変換テーブルを用意しておく 方が実行速度が速いです.
# $str を URIエスケープする(変換テーブル版) foreach $i (0x00 .. 0xFF) { $escape{chr($i)} = sprintf('%%%02x', $i); } $str =~ s/(\W)/$escape{$1}/g;# $str を URIアンエスケープする(変換テーブル版) foreach $i (0x00 .. 0xFF) { $unescape{sprintf('%02x', $i)} = chr($i); $unescape{sprintf('%02X', $i)} = chr($i); } $str =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/$unescape{$1}/g;変換テーブル版では最初に変換テーブルを用意するという前処理が必要に なりますが,変換そのものは
修飾子 がなくなり,文字列展開のみになるので最も実行速度が速いです.e
URIエスケープ の対象となる文字ですが,上記のスクリプトでは 単純にとしていました. しかし,これは厳密には \W
URIエスケープ の必要がない文字までもURIエスケープ してしまいます. 必ずURIエスケープ しなければならない文字はRFC 2396 ( 日本語訳 )で unreserved として定義されている文字以外になります. unreserved 以外の文字だけをURIエスケープ するスクリプトは 以下のようになります.# $str を URIエスケープする(必要最小限版) $str =~ s/([^a-zA-Z0-9_.!~*'()-])/'%' . unpack('H2', $1)/eg;ここから先は CGI や URI 特有の話になります.
URIエスケープ するには,「RFC 2396でURI文字 として使用できる文字 uric として定義されているもの以外を エスケープすればいいので,モジュール のURI::Escape
を使って, 正規表現 uri_escape
関数で表わされる文字以外をエスケープすればいい」という話がありますが, これは間違いです.正確には,ある意味では それでいいのですが,おそらく CGI を書く人にとってはほとんどの場合 間違いでしょう. [;\/?:@&=+\$,A-Za-z0-9\-_.!~*'()]
がやろうとしているのは, URI を入力としたときに uri_escape
関数URI文字 以外の文字をエスケープする ことであって,CGI を書く人がなんらかの値をエスケープしようとすることとは 意味が違います.たとえば,のとき, $value = 'A&B=C';
とすることを考えたらどうなると思いますか? print "http://foo.bar/cgi-bin/hoge.cgi?value=$value";
を使って uri_escape
関数をエスケープしても $value
&
や=
はURI文字 なのでエスケープはされません.この結果,と value=A
という B=C
2つ を&
でつなげていると解釈されてしまいます. 実はは uri_escape
関数第 2引数 で 変換対象とする文字を与えることができます.ただ,やっている内容は上に書いた スクリプトと同じことなので,わざわざ標準ではないモジュール をインストールして使うこともないでしょう.URI::Escape
次に,スペースと + の相互変換の話をします.CGI に何らかのデータを 渡す方法としては,FORM の GET または POST を使う方法とコマンドライン引数として 渡す方法の
2つ があります.この2つ 方法ではそれぞれ スペースと + の相互変換の話が違っています.FORM の GET または POST を 使う方法についてはHTML 4.0 ( 日本語訳 ) の17.13.4 Form content types に content types が デフォルトのapplication/x-www-form-urlencoded のときの エンコード方法として書かれています.コマンドライン引数として渡す方法に ついてはCGI/1.1 の 5. The CGI Script Command Line に書かれています.
application/x-www-form-urlencoded でのエンコードでは control names と values のスペースは + に変換し,それ以外の 予約文字を%HH の形式にURIエスケープ しま す.そして,controle names と values を = で区切った組とし,その組 を & で区切って並べます.つまり,スペースは + に変換し,それ以外の予約文字をURIエスケープ した上で,name1=value1&name2=value2 というような形式にすることです. control names や values に対して行なう文字処理部分は次のようになります.# $str に対しエンコードの文字処理部分を行なう $str =~ s/([^\w ])/'%' . unpack('H2', $1)/eg; $str =~ tr/ /+/;スペースは + に変換しなければならないので,
URIエスケープ した後でと再度 変換しなおす方法もありますが,このスクリプトのようにスペースに対して余計な 処理を行なわないようにした方が実行速度が速いです. s/%20/+/g;
# $str に対しデコードの文字処理部分を行なう $str =~ tr/+/ /; $str =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('H2', $1)/eg;先に
URIアンエスケープ してしまうと,+
に変換されているスペースと区別がつかなくなるので,その前に+
をスペースに戻しておきます. このときとしても可能ですが, 文字単位の変換なので実行速度が速い $str =~ s/\+/ /g;
を使います. tr
関数世間一般で使用されている
URLエンコード というのが何を 指しているのか私にはよくわからないのですが,application/x-www-form-urlencoded でのエンコードのことをURLエンコード と言うのであれば,予約文字を%HH の形式に変換するURIエスケープ の処理 だけを指してURLエンコード と言うのは間違いになりま す.もし,URIエスケープ のことをURLエンコード と 言うのであれば,スペースを + に変換しなければならないというのは間違いになります.一方,コマンドライン引数として渡す方法ですが,この方法の書式は
search-string = search-word *( "+" search-word ) となっています.具体的な例で言いますと,http://foo.bar/cgi-bin/hoge.cgi?arg1+arg2+arg3 のように なります.このときスペースを + に変換するという 話はどこにもありません.search-string 同士を 区切っている + は最初から + であって,スペースを変換したもの ではないのです.もし,search-string にスペースが 含まれていた場合には,その他の予約文字と同様URIエスケープ されることになるので%20 に 変換されることになります.スペースを間違って + に変換してしまうと, たとえばのとき,これを CGI の引数として渡そうと $value = 'A B C';
とすることを考えたら, print "http://foo.bar/cgi-bin/hoge.cgi?$value";
となり,このとき http://foo.bar/cgi-bin/hoge.cgi?A+B+C
hoge.cgi は3つ の引数, 'A'
, 'B'
を受け取ることになってしまいます. これは 'C'
http://foo.bar/cgi-bin/hoge.cgi?A%20B%20C とするのが正解になります.コマンドライン引数として渡す方法ではスペースが + に変換されている わけではないので,受け取った側で + をスペースに戻すようなことを してはいけないということになります.コマンドライン引数として渡す方法でも
QUERY_STRING からquery 部分 ,つまり,? 以降 の部分を取得することができます.もし, FORM の GET または POST を使う方法とコマンドライン引数として 渡す方法のどちらでデータが渡されるのかわからない場合に,QUERY_STRING からデータをもらって処理するためには, + をスペースに変換すべきか変換すべきではないのか判断する必要が あります.判断する方法は簡単で,QUERY_STRING に = が含まれているかどうかを調べます.もし含まれていれば,それはapplication/x-www-form-urlencoded でのエンコードをされている ことになります.含まれていなければ,コマンドライン引数としてデータが 渡されたことになります.
トップへs/\x0D\x0A|\x0D|\x0A/\n/g;このスクリプトは Windows(DOS),Mac,UNIX のいずれかのプラットフォームの改 行コードを自プラットフォームの改行コードに統一します.改行コードは Windows(DOS)では
, Macでは \x0D\x0A
\x0D
,UNIX では\x0A
なので, これらすべての改行コードに対応するにはとする必要があります. このとき順番は \x0D\x0A|\x0D|\x0A
を必ず最初にしなければなりません. \x0D\x0A
改行コードを統一するために
と書くのは間違いです. このように書いて正常に動作するのは Windows(DOS) と UNIX の perl のみで,Mac の perl では正常に動作しません.よ く「改行コードは Windows(DOS) では s/\r\n|\r/\n/g;
,Mac では \r\n
,UNIX では \r
である」という人がいますが,これはある意味正しいと言えなくもないのですが, 根本的には間違っています. 以降で何がどう間違っているのか説明しますが,その前に実際の値として の改行コード値と論理的な改行文字が別物であるということを頭の片隅にとどめてお いてください. \n
まず,実際の値としての改行コード値が Windows(DOS)では
,Macでは \x0D\x0A
, UNIX では \x0D
であるということは特に問題ないでしょう.それでは \x0A
や \r
というのは一体何なのでしょうか? 答えはそれぞれ Perl という言語の中で論理的に 定義された復帰文字と改行文字です. プラットフォームによって改行コード値は \n
であったり, \x0D\x0A
や \x0D
であったりするわけですが,改行するためにはどのプラットフォームであろうと Perl という言語では論理的な改行文字である \x0A
を出力することになります.つまり,「改行は Windows(DOS) でも Mac でも UNIX でも \n
」なのです. \n
それでは
と \r
が実際にどのようなコード値になっているのかまとめたものが以下の表になります (Macは推測.間違いはご指摘ください). \n
Windows(DOS) Mac UNIX 改行コード値 \x0D\x0A
\x0D
\x0A
改行文字 \n
\n
\n
復帰文字 \r
\r
\r
print FH "\n";
を出力 \x0D\x0A
を出力 \x0D
を出力 \x0A
print FH "\r";
を出力 \x0D
を出力 \x0A
を出力 \x0D
binmode FH;
print FH "\n";を出力 \x0A
を出力 \x0D
を出力 \x0A
print FH "\r\n";
を出力 \x0D\x0D\x0A
を出力 \x0A\x0D
を出力 \x0D\x0A
binmode FH;
print FH "\r\n";を出力 \x0D\x0A
を出力 \x0A\x0D
を出力 \x0D\x0A
自プラットフォームの改行コード値だけを対象にしているのならば,たとえば, UNIX の perl ならば
は \x0D
であり, \r
は \x0A
であるとすることができます. ただし,その場合でもテキスト処理するときに限られます. 何らかのバイナリファイル内の \n
が改行を意味するわけではないからです. 当然,改行コードを統一するスクリプトというように, 自プラットフォーム以外の改行コード値のことも考えて処理する場合には勝手に \x0A
は \x0D
で, \r
は \x0A
だと決めつけてはいけません. \n
や \r
はあくまでも論理的な文字なのです. 最初のスクリプトは,Windows(DOS),Mac,UNIX での実際の改行コード値を論理的な改行文字に置換しているという意味になります. \n
最初のスクリプトは簡潔でわかりやすく書かれてはいますが, 実行速度は遅いで す. 次のように
tr
を使って2文 でやった方が圧倒的に速いです.s/\x0D\x0A/\n/g; tr/\x0D\x0A/\n\n/;なお,Perl内部では Windows(DOS) でも Mac でも UNIX でも,
は \r
か \x0D
のどちらかであり, \x0A
はその逆であるという特性を利用することで, 次のように書くことが可能です. \n
s/\x0D\x0A/\n/g; tr/\r/\n/; # 意味的には tr/\x0D\x0A/\n\n/;このスクリプトの方が,
tr
で変換が行なわれなかった場合において,わずかに実行速度が速くなります. ただし,これまで述べてきたように意味的には間違った書き方であることを 十分理解した上で使う必要があります.
トップへs/\x0D\x0A|\x0D|\x0A/<BR>/g;改行コードは Windows(DOS)では
, Macでは \x0D\x0A
, UNIX では \x0D
なので, これらすべての改行コードに対応するには \x0A
とする必要があります.このとき順番は \x0D\x0A|\x0D|\x0A
を必ず最初にしなければなりません. このスクリプトは簡潔でわかりやすく書かれてはいますが, 実行速度は遅いです. \x0D\x0A
とするとほんの少し速くなりますが,ほとんどの場合次のように s/\x0D\x0A|[\x0D\x0A]/<BR>/g;
3文 でやった方が圧倒的に速いです.s/\x0D\x0A/<BR>/g; s/\x0D/<BR>/g; s/\x0A/<BR>/g;改行について詳しくは「改行コードを統一する」 を参照.
トップへtr/\x0D\x0A//d;「改行コードを
<BR> に変換する」と同じ手法で,と書いても改行コードを削除することができます.また, s/\x0D\x0A|\x0D|\x0A//g;
<BR> に変換する場合と違って,, \x0D
という \x0A
2つ の文字を両方とも削除すればいいので,と書いても同じことができます. しかし,このように文字単位で変換する場合は, s/[\x0D\x0A]//g;
と書いた方が tr/\x0D\x0A//d;
や s/\x0D\x0A|\x0D|\x0A//g;
とするよりも実行速度が速いので, s/[\x0D\x0A]//g;
tr
を使って改行コードを削除するようにするのがいいでしょう.ここで注意が必要なのは,この方法で改行コードを削除すると, 文字列の中に含まれるすべての改行コードが 削除されるということです.
1行 入力された場合のように, 文字列の最後にだけ改行コードがあるとわかっている ときは,chomp
を使います.ただし,chomp
は Windows(DOS) や Mac,UNIX といった処理系に依存します.を UNIX の perl で \x0D\x0A
chomp
した場合は,が残ってしまいます. もし,複数の処理系の改行コードを想定しなければならない場合は,次のように して文字列の最後の改行を削除します. \x0D
s/\x0D?\x0A?$//;改行について詳しくは「改行コードを統一する」 を参照.
トップへ# CSV形式の $line から値を取り出して @values に入れる { my $tmp = $line; $tmp =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @values = map {/^"(.*)"$/ ? scalar($_ = $1, s/""/"/g, $_) : $_} ($tmp =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); }
CSV(Comma Separated Value)形式 というのは, 完全にアプリケーションに依存した形式であるので, このスクリプトであらゆるアプリケーションが扱うCSV形式 の 行から値を取り出せるわけではありません.このスクリプトはもっとも需要が あると思われ,また,比較的一般的な定義である Excel が出力するCSV形式 について扱うこととしました.Excel が出力するCSV形式 がどのようなものか Excel のヘルプに載って いませんでしたが,私が独自に調べた結果以下のようなものであるとしました.
- 基本的にコンマで区切った部分がスペースを含めて値である.
- 値にコンマやダブルクウォートが含まれる場合は, 値全体をダブルクウォートで囲む.
- 値に含まれるダブルクウォートは
"" となる.このスクリプトでは,まずはじめに
のコピーを $line
に取ってから処理しています. コピーを取らずに処理すると, 次の処理で $tmp
を変更してしまうことになるためです. 具体的には,抽出処理を簡単にするために,最後の値の後ろに コンマをつけ加えています.このとき $line
の最後に 改行コードがついていた場合を考え,改行コードの削除も同時に行なっています. ここまでの処理で $line
の中身は $line
値,値,値, というように値, の繰り返しになっています.次に
値,値,値, という形から個々の値を 取り出すわけですが,これを行なう ために修飾子 をつけた パターンマッチを行ないます.g
修飾子 をつけた パターンマッチをリストコンテキストで実行すると,g
によるグループにマッチした部分文字列のリストを 返します.値の部分にマッチする正規表現をグループにしておけば, 値のリストを取り出すことができるわけです. ()
ここで注意が必要なのは,
値, となっているものと,"値", となっているものの2種類 があることです.そして,"値", の形の方の値にはコンマが含まれている可能性があります.したがって, 単純にや split /,/, $tmp
のようにしてしまうと, 値の中のコンマによって値が ($tmp =~ /([^,]*),/g)
2つ に別れてしまうことになります. そこでまずは値を区切っているコンマで 値 と"値" を正確に取り出すことを考えます.
値, の形の値にはコンマが含まれていませんから, 値 の部分にマッチさせるにはとすればいいことになります. 一方, /([^,]*),/
"値", の形の"値" の部分にマッチさせるには,とすればいいように思うかもしれませんが, /("[^"]*"),/
CSV形式 の3番目 の定義により,値にはというのが含まれている可能性があります.そこで, ""
以外に [^"]
の場合も考え, ""
とすればいいことになります.この /("(?:[^"]|"")*"),/
2つ の形を合成して,($tmp =~ /("(?:[^"]|"")*"|[^,]*),/g) となります. これで 値 または"値" のリストとして 取り出すことができます.ただ,正規表現の部分はこのままでもいいのですが, スクリプトではさらにこの正規表現をJeffrey E. F. Friedl 氏 原著による「詳説 正規表現」で 「ループ展開」として書かれている 手法で変形し実行速度を速くしてあります.最後に
"値" から値を復元する必要があります.値 の形ならそのまま,"値" の形ならば両側のダブルクウォートを取り除き,さらには " に変換します. この処理を ""
の中で行なっています. これで map
関数CSV形式 の行から値を取り出すことができます.
モジュール を使えば同じようなことが できますが,ASCII しか扱えないので日本語が含まれる場合には使えません.Text::CSV
モジュール をバイナリモードで使えば日本語を扱うことができます. ただし,どちらのモジュールも標準ではないため アーカイブファイルを取ってきてインストールする必要があります.Text::CSV_XS
トップへ# 値に改行コードを含む CSV形式を扱う while (my $line = <DATA>) { $line .= <DATA> while ($line =~ tr/"// % 2 and !eof(DATA)); $line =~ s/(?:\x0D\x0A|[\x0D\x0A])?$/,/; @values = map {/^"(.*)"$/s ? scalar($_ = $1, s/""/"/g, $_) : $_} ($line =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g); # @values を処理する }値に改行コードを含む
CSV形式 は 「CSV形式 の行から値のリストを取り出す」 で書いた Excel が出力するCSV形式 を以下のように修正したものであるとしました.
- 基本的にコンマで区切った部分がスペースを含めて値である.
- 値にコンマやダブルクウォート, 改行コードが含まれる場合は, 値全体をダブルクウォートで囲む.
- 値に含まれるダブルクウォートは
"" となる.
CSV形式 の1行 に現れるダブルクウォートは,"値" のように値を囲む場合と, 値に含まれていたダブルクウォートが"" となっている場合です. したがって,CSV形式 の1行 には, ダブルクウォートが必ず偶数個あることになります. もし,ダブルクウォートが奇数個だった場合には,値に含まれる改行コードによって, もともと1行 のCSV形式 だった行が複数行に別れてしまっ ていることになります.そこでダブルクウォートの数を数え,奇数個だった場合には次の行を追加します. これをダブルクウォートが偶数個になるまで繰り返します.
でダブルクウォートの数を数えています. こうして正しく tr/"//
CSV形式 の1行 を取り出すことができたら,あとは 「CSV形式 の行から値のリストを取り出す」 とほとんど同じスクリプトで処理することができます.唯一の違いはの中でのパターンマッチで, map
関数修飾子 をつけていることです.s
修飾子 をつけることによって, ピリオドが改行コードにもマッチするようになります.s
トップへ# 値の配列 @values から CSV形式の行 $line に変換する $line = join ',', map {(s/"/""/g or /[\r\n,]/) ? qq("$_") : $_} @values;このスクリプトは,値の配列から 「
CSV形式 の行から値のリストを 取り出す」や「値に改行コードを含むCSV形式 を扱う」で定義したCSV形式 の行に 変換するものです.値をコンマで区切って結合させる前に, ダブルクウォート・改行コード・コンマのいずれかが含まれる値については,
"値" の形に変換する必要があります.また, ダブルクウォートについては,"" に変換しておく必要があります. これをによって一度に行なってしまっています. map
関数
は置換を行ない, その回数を返します. したがって, s///
というのは, ダブルクウォートがあればそれを s/"/""/g
に置換し,置換した回数, つまりこの場合は値に含まれていたダブルクウォートの個数を返すことになります. この個数が 1以上,または改行コードかコンマが含まれていた場合に ""
"値" の形に変換しています.このスクリプトによって得られる
には最後に改行コードが含まれていませんので, ファイルなどに書き出すときには次のように改行コードをつける必要があります. $line
print $line, "\n";
トップへここでは以下のようなデータに対するソートを例に説明します.
1つ 1つ の要素は第 1~3項 をコンマで 区切った形式をしています.@data = ('A,7,緑', 'C,6,青', 'B,4,赤', 'A,9,紫', 'A,2,黄緑', 'B,10,黄', 'C,3,青紫');# 第 2項でソートする @data = map {$_->[0]} sort {$a->[2] <=> $b->[2]} map {[$_, split /,/]} @data;ソート後のデータ @data = ('A,2,黄緑', 'C,3,青紫', 'B,4,赤', 'C,6,青', 'A,7,緑', 'A,9,紫', 'B,10,黄');この方法は Schwartzian Transform と呼ばれている方法です.このスクリプトはデータを要素の
第 2項 の 数字の部分でソートしています.ソートを行なうにはを使えばいいのですが, もとの要素のままではアルファベット,数字,色がコンマで区切られた sort
関数1つ の文字列になってしまっているので, 数字の部分だけでソートすることができません. そこで,要素から数字の部分を抜き出してソートする必要があります. 要素から数字の部分を抜き出してソートするには次のように書けばできます.@data = sort { my ($alpha_a, $num_a, $color_a) = split(/,/, $a); my ($alpha_b, $num_b, $color_b) = split(/,/, $b); $num_a <=> $num_b; } @data;しかし,この方法は非常に効率が悪いものです. なぜならば,比較が行なわれるたびに要素を分解しているからです.そこで, あらかじめ要素を分解しておき,比較するときに余計な処理をさせないことが 重要となります.要素を分解,比較,もとの要素に戻すという ことを一度に効率的にやってしまうのが Schwartzian Transform です.
最初のスクリプトに戻って説明します.ソートは
3行 に渡って 書かれていますが,これで1文 です.実際の実行は3行目, 2行目, 1行目 の順番で 行なわれます.それぞれ,要素を分解,比較,もとの要素に戻すということを やっています.まず,3行目 でデータの1つ 1つ の要素に対して, 無名配列へのリファレンスを作って, これを要素とする新たな配列に変換しています.新しい要素はです.これは簡単に言ってしまえば, [$_, split /,/]
(もとの要素, 第 1項, 第 2項, 第 3項) という配列だと思えば いいでしょう.次に,2行目 で実際にソートします.や $a->[2]
は, $b->[2]
3行目 であらかじめ分解して新しい要素に変換した 無名配列の添え字 2 の要素,つまり,第 2項 を表わしています.ここでは要素を取り出しているだけなので, 毎回分解していたやり方に比べて効率がよいことが わかると思います.最後に1行目 でもとの要素に戻しています.Schwartzian Transform には,毎回分解していたやり方に比べて効率が よい,無名配列を使うことによって中間データを保持するため作業用の配列を 特別に用意する必要がない,すべての 処理を簡潔に記述することができる といった特徴があります.しかし,無名配列を使用しているために,無名配列から 要素を取り出すというオーバーヘッドが生じてしまいます.このため,次のように 作業用の配列を用意して行なう方法の 方が実行速度が速いです.
# 第 2項でソートする(作業用配列を使った高速版) @tmp = map {(split /,/)[1]} @data; @data = @data[sort {$tmp[$a] <=> $tmp[$b]} 0 .. $#tmp];このスクリプトは,最初に
作業用配列 に@tmp
第 2項 を取り出しておき,配列の添え字のリスト値に対してソートを行ない,ソートされた 添え字をもとに配列スライスで 0 .. $#tmp
からリスト値を取り出すことでソートしています. @data
トップへここでは以下のようなデータに対するソートを例に説明します.
1つ 1つ の要素は第 1~3項 をコンマで 区切った形式をしています.@data = ('A,7,緑', 'C,6,青', 'B,4,赤', 'A,9,紫', 'A,2,黄緑', 'B,10,黄', 'C,3,青紫');# 第 1項でソートし,さらに第 2項で降順ソートする @data = map {$_->[0]} sort {$a->[1] cmp $b->[1] or $b->[2] <=> $a->[2]} map {[$_, split /,/]} @data;ソート後のデータ @data = ('A,9,紫', 'A,7,緑', 'A,2,黄緑', 'B,10,黄', 'B,4,赤', 'C,6,青', 'C,3,青紫');このスクリプトは,まず
第 1項 のアルファベットでソートし, 同じアルファベットの中ではさらに第 2項 の数字を比較して降順に なるようにソートしています.基本的な動作は 「特定の項目でソートする」で説明した内容と同じです. 違いは2行目 のの中だけです. 複数の項目でソートしたい場合は,このようにソート条件を sort
関数or
を使って並べてやるだけです.第 1項 のアルファベットでソートする ときは文字列の比較になりますのでcmp
を使います.第 2項 は数字の比較になりますのでを使います. また, <=>
第 2項 は降順で ソートしたいので,と $a
を左右逆にしなければなりません. $b
上のスクリプトを作業用配列を使って実行速度が速くなるようにしたものが 次のスクリプトになります.
# 第 1項でソートし,さらに第 2項で降順ソートする(作業用配列を使った高速版) @tmp1 = @tmp2 = (); foreach (@data) { my ($first, $second) = split /,/; push(@tmp1, $first); push(@tmp2, $second); } @data = @data[sort {$tmp1[$a] cmp $tmp1[$b] or $tmp2[$b] <=> $tmp2[$a]} 0 .. $#tmp1];このスクリプトの基本的な 動作は「特定の項目でソートする」で説明した内容と 同じです.簡潔に記述したいのであれば Schwartzian Transform ですが, 実行速度を速くしたいのであれば簡潔に記述することをあきらめるしかないようです.
おまけとして,数字で始まる文字列を含まない任意の項目数のデータを昇順で多 重ソートするスクリプトを載せておきます.このスクリプトは
perl5.005 以降でしか動作しませんが,の形を expr foreach ()
の形にすれば, foreach () {expr}
perl5.005 以前のperl5 でも動作するようになります.# 任意の項目数のデータを昇順で多重ソートする @data = map {$_->[0]} sort {$x = ($a->[$_] <=> $b->[$_] or $a->[$_] cmp $b->[$_]) and return $x foreach (1 .. $#$a); -1} map {[$_, split /,/]} @data;
トップへここでは以下のようなデータに対するソートを例に説明します.
1つ 1つ の要素は第 1~3項 をコンマで 区切った形式をしています.@data = ('A,7,緑', 'C,6,青', 'B,4,赤', 'A,9,紫', 'A,2,黄緑', 'B,10,黄', 'C,3,青紫');# 第 3項が自分で決めた順番になるようにソートする $i = 0; undef(%color); foreach $name ('赤', '黄赤', '黄', '黄緑', '緑', '青緑', '青', '青紫', '紫', '赤紫') { $color{$name} = $i++; } @data = map {$_->[0]} sort {$color{$a->[3]} <=> $color{$b->[3]}} map {[$_, split /,/]} @data;ソート後のデータ @data = ('B,4,赤', 'B,10,黄', 'A,2,黄緑', 'A,7,緑', 'C,6,青', 'C,3,青紫', 'A,9,紫');ソートの基本的な動作は「特定の項目でソートする」 で説明した内容と同じです. 自分で決めた順番でソートするためには,その順番を数字に変換できる ようにハッシュに定義しておき,あとはその数字を 使ってソートするだけです.このスクリプトでは,
第 3項 の色が赤,黄赤,黄,黄緑,緑,青緑,青,青紫,紫, 赤紫という順番になるようにソートしています.色の名前のままではソートする ことはできませんので,順に0~9 の数字に対応するように ハッシュを定義しています.ソートするときは,このハッシュから対応する数字に 変換し,数字の比較でソートします.
トップへ# $year年 $mon月 $mday日の曜日を求める use Time::Local; $time = timelocal(0, 0, 0, $mday, $mon - 1, $year - 1900); ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($time); $wday_string = (qw(日 月 火 水 木 金 土))[$wday];
を使って,まずは年月日時分秒を 1970年1月1日00時00分00秒からの秒数(MacPerl では 1904年1月1日00時00分00秒からの秒数)に変換します.このとき年と月の引数は それぞれ timelocal
関数, - 1900
する必要があります.次に,その秒数から - 1
を使って曜日 localtime
関数を求めます. 最後にその数字を文字列に変換してあげます. $wday
(0~6)通常は上の書き方で問題ないのですが, ほとんどの計算機で
1970年~2037年 までしか 計算できないという制限があります.そこで,この範囲を超えるような 場合があるときはツェラー(Zellar) の公式というものを使って 次のように書きます. ツェラーの公式を使えば制限はありませんし, 実行速度も速いのですが, すぐに思い出せない,覚えられないという欠点があります.# $year年 $mon月 $mday日の曜日を求める $wday = getwday($year, $mon, $mday); $wday_string = (qw(日 月 火 水 木 金 土))[$wday]; sub getwday { my($year, $mon, $mday) = @_; if ($mon == 1 or $mon == 2) { $year--; $mon += 12; } int($year + int($year / 4) - int($year / 100) + int($year / 400) + int((13 * $mon + 8) / 5) + $mday) % 7; }
トップへ# 一週間前の年月日($year年 $mon月 $mday日)を求める use Time::Local; # 一週間前の時間を求める $time = time() - 60 * 60 * 24 * 7; ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($time); $year += 1900; $mon++;現在の時間を
で求め,その秒数から 一週間前の時間に なるように time
関数を引いてあげます. この秒数を 60 * 60 * 24 * 7
を使って年月日に変換します. localtime
関数が返す値は,年が西暦から 1900を引いた値で,月は localtime
関数0~11 までの値を返します. したがって,最後に年と月をそれぞれ+ 1900 ,+ 1 する必要があります.を返す値を大きく すれば未来の年月日を求めることができます. time
関数年月日や時間に関する
モジュール を使っても同じようなことができますが,標準のモジュールではないため アーカイブファイルを取ってきてインストールする必要があります.Date::Calc
トップへ# $year年 $mon月の末日 $lastday を求める $lastday = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$mon - 1] + ($mon == 2 and $year % 4 == 0 and ($year % 400 == 0 or $year % 100 != 0));最初の行は
1月 から12月 までの末日を 並べたリスト値に 対して,添え字に対応する値を 取り出しています. $mon - 1
2行目 ,3行目 は閏年のための補正です. 基本的に 4で割れる年は閏年だが, 100で割れる年のときは閏年ではない,ただし,400で割れる年は閏年である. 言い換えると,4で割れる年のうちand (
400で割れる年とor
100で割れない年 ) が閏年ということになります. 閏年の2月 の末日を計算するときという条件を表わしたものが2行目 ,3行目 です.Perlでは
and
やor
は, 最後に評価した値を返します. また,や ==
は真のときに 1, 偽のときに空文字列を返します.このことから, !=
2行目 ,3行目 の条件式は閏年の2月 の末日を計算するときは 1, そうでないときは空文字列を返すことになります. これをリスト値から取り出した末日に加えます.+
は数値の和を求める 演算子ですので,空文字列のときは数値として解釈できないので 0 に変換されます.
トップへ# $year年 $mon月の第$n $wday(0-6)曜日が何日か求める # getwday() は別途参照 $wday1 = getwday($year, $mon, 1); $mday = 1 + ($wday - $wday1) % 7 + 7 * ($n - 1); print $mday, "\n";まず初めに,その月の
1日 の曜日を求めます.年月日から曜日を求める方法については, 「年月日から曜日を求める」を参照してください. $wday1
次に
1日 を基準に求めたい日付を計算します. 求めたい曜日と $wday
1日 の曜日の差を加えて あげれば $wday1
第1 の日付を 求めることができます.しかし,ここで単純に加えたのでは差が負数の場合に まずいことになります. そこで$wday
曜日で場合分けしてもよいのですが, 7 で割った余り(必ず非負数)を求めれば, うまく if
文第1 の日付を求めることができます. 最後に$wday
曜日の $n
番目の 日付を求めるために $wday
曜日を加えて終わりです. 7 * ($n - 1)
求めた日付が本当に存在するかどうかは, 「年月から末日を求める」で書いたように末日を 求めて比較すればよいことになります.
年月日や時間に関する
モジュール を使っても同じようなことができますが,標準のモジュールではないため アーカイブファイルを取ってきてインストールする必要があります.Date::Calc
トップへ1 while s/^([-+]?\d+)(\d\d\d)/$1,$2/;このスクリプトの
while
の前の1
は特に意味が ないダミーの式です.本当に行ないたい部分はの条件式の部分 の置換です. このスクリプトは,置換が行なわれると条件式が真になり,ダミーの式である while
文1
を実行し,再び置換を行なおうとします.置換が行なわれるとコンマが1つ 追加されます.つまり,コンマを1つ ずつ 追加していき,追加できなく なった時点でが終了することになります. while
文では,実際にどのように置換しコンマを追加しているのか説明します.
の部分が,数字を先頭から見て, 符号を考慮にいれつつ数字である限りできるだけ伸ばそうとします. ところが,その後ろに ^([-+]?\d+)
というのがあるので,少なくとも数字を (\d\d\d)
3つ 残さなければ パターンマッチできません.したがって,には 後ろに数字を $1
3つ 残した前の部分の数字,には残された $2
3つ の数字が代入されることに なります.その間にコンマを入れてあげます.これでコンマが1つ 追加されます. これを繰り返すことで数字全体に対して3桁 ごとにコンマで 区切ることができます. つまり,このスクリプトでは数字を桁の小さい方から3桁 ずつ大きい方に向かって区切っています.
を使った方法よりも,次のように書いた 方が桁が大きい場合には実行速度が速いです. while
文s/(\d{1,3})(?=(?:\d\d\d)+(?!\d))/$1,/g;このスクリプトは
修飾子 を使うことで,置換文だけで数字をコンマで区切っています.コンマで区切る方法もg
を使った置換と違い, 桁の大きい方から区切っています.どのように 区切っているのか説明します. while
文ここで一番注目しなければいけないのが,
の部分です.これは正規表現 (?=regex)
regex
にマッチする 文字列が次にくる場合に マッチする0文字幅 の正規表現です.「0文字幅」 と いうのは,文字列の先頭や最後を表わす^
や$
のように, 文字としての幅がないという意味です.ちょうどが単語の境界に マッチするように, \b
も文字と文字の間で マッチするものと 考えるといいでしょう.「次にくる場合」というのは,たとえば (?=regex)
という正規表現の場合, foo(?=bar)
foo
やfoohoge
などはマッチしません.なぜならfoo
の次にbar
がこないからです.foobar
の場合はマッチするわけですが, マッチするのはfoo
の部分だけです.は次に (?=bar)
bar
が こなければならないと言っているだけでbar
を含んでいるわけでは ないからです.これが正規表現foobar
との違いです. foo(?=bar)
は (?!regex)
の否定の形で, 次にこない場合にマッチします. (?=regex)
話をスクリプトに戻しますと,
は数字が次にこないような, (?=(?:\d\d\d)+(?!\d))
3桁 の数字の1回 以上の繰り返しが次にくることを表わ しています.数字が次にこないということは数字の終わりを意味しています.$
と違って文字列の最後でなくても数字が終わっている部分に マッチするということです.全体として,桁の小さい方から3桁 ずつ数字を まとめているわけです.ただし,ですので, (?=regex)
3桁 ずつ数字をまとめたものが次にくると言っているだけで, それを含んでしまっているわけではありません.そして,一番桁の大きい部分がにマッチします.この部分の後ろにコンマを 追加します. これで (\d{1,3})
1つ コンマが追加されたわけですが,修飾子 によって他にも 置換できるところをすべて置換しにいきます.このときに先ほど「含んでいない」 と言った部分が効いてきます.最初の置換で実際にマッチした数字は一番桁の 大きい部分のところです.次にg
修飾子 によって 他のところを 置換しにいくときは,このマッチした部分の次から置換を始めようとします. もし,含んでしまっていたら,数字全体にマッチしてしまいますので, コンマがg
1つ しか追加されずに終了してしまいます.このスクリプトでは小数のことを考えていないので,小数点以下の部分まで コンマで区切ってしまうという問題点があります.これを修正したものが次のスクリ プトです.
s/\G((?:^[-+])?\d{1,3})(?=(?:\d\d\d)+(?!\d))/$1,/g;
はパターンマッチの開始位置にマッチします. これが入っていることで,いきなり小数点以下部分にマッチしたりすることがなくな り,前回マッチした部分のすぐ次の桁からの部分にだけ注目して置換を行なうことが できるようになります. \G
のことは 「正しくパターンマッチさせる」の中でも触れています. \G
実行速度ですが,私がベンチマークをとって調べたところ,
2番目 の方法よりもその修正版である3番目 の方法の方 があらゆる場合において実行速度が速かったです.1番目 の方法と3番目 の方法ですが,6桁 以上の数字においては3番目 の方法の方が実行速度が速かったです.5桁 以下で は1番目 の方法が一番速かったのです.もし,実行速度を気にするのであれば,次のように書く方法があります.実行速 度は上記のどの方法よりも速いです.符号や小数にも対 応しています.
# $num を3桁ごとにコンマで区切る(高速版) $num = reverse(join(',', reverse($num) =~ /((?:^\d+\.)?\d{1,3}[-+]?)/g)) if $num =~ /^[-+]?\d\d\d\d/;このスクリプトは数字を一旦ひっくり返してから,前から
3桁 ごとに区切り,それらをコンマで連結した上で再度ひっくり返し ています.このスクリプトのように複雑な正規表現を避けたり,パターンマッチ文の 評価回数を少なくしたりすることで実行速度を速くできる場合があります.ですが,これがなくても動作します.しかし,その場 合は if
文3桁 以下の場合,つまり,全くコンマで区切る必要がない場合に おいても,数字をひっくり返してから元に戻すという作業が発生してしまい実行速度 が遅くなってしまいます.は数字の絶対値が 1000 以上であるとしてもいいのですが,私がベンチマークをとってみたところ,このスク リプトのように正規表現で判断した方が実行速度が速かったです.ただ,一般にパター ンマッチは遅いものなので,他の組み込み関数で簡単に代用できる場合は正規表現を 避けた方が実行速度が速くなることが多いです. if
文次のスクリプトは複雑な正規表現を避け,組み込み関数で代用するということを 更に進めたものです.実行速度は最速です.もちろん, 符号や小数にも対応しています.
# $num を3桁ごとにコンマで区切る(最速版) if ($num =~ /^[-+]?\d\d\d\d+/g) { for ($i = pos($num) - 3, $j = $num =~ /^[-+]/; $i > $j; $i -= 3) { substr($num, $i, 0) = ','; } }このスクリプトはまず初めに
で,全くコンマで区 切る必要がない場合に無駄な処理をさせないという判断を行なうと同時に,どこまで が整数部分であるのかということもチェックしています.正規表現の最後に if
文+
がつけてあるので小数点が来るか,または,文字列の最後に達するかす るまでできるだけ長くマッチしようとします.したがって,この正規表現で整数部分 がすべてマッチすることになります.このパターンマッチに おいてに
修飾子 がつけられているところに注意してください.g
修飾子 をつけたパターンマッチを スカラーコンテキストで評価した場合,パターンマッチ対象となった文字 列は前回マッチした場所を記憶しています.そのため,もう一度同じ文字列に 対してg
修飾子 をつけたパターンマッチを行なうと,続き からパターンマッチが行なわれるようになります.このスクリプトではg
によって前回マッチした場所の記憶, すなわち,整数部分が文字列のどの位置までかということを取得しています. この情報を使ってコンマを追加することで 小数点以下の場所までコンマを追加することがなくなります. pos
関数整数部分の最後の場所がわかれば,後は
3文字 ごとにコンマを追加 していくことになります. このスクリプトでは追加の作業をで行なっています.この関数の substr
関数第 3引数 が0
なので文字と文字の間に文字列,この場合は,コンマを追加することになります.こ のとき単純に3文字 ごとに追加していったのでは,符号がついていた 場合に符号と数字の間に間違ってコンマを追加してしまう場合があります.そのため, 符号がついていた場合には符号を含めないように処理を止めるようにしています.具 体的には,の値は符号が ついていなければ空文字列,符号がついていれば $j
1
になります.空文字列は比較するときには0
と解釈されます.
トップへ# $num を四捨五入して小数点以下 $decimals桁にする sub round { my ($num, $decimals) = @_; my ($format, $magic); $format = '%.' . $decimals . 'f'; $magic = ($num > 0) ? 0.5 : -0.5; sprintf($format, int(($num * (10 ** $decimals)) + $magic) / (10 ** $decimals)); }# 計算例 $number = 1.2345; $number_0 = round($number, 0); # 四捨五入して整数にする $number_2 = round($number, 2); # 小数点以下2桁まで求める $number_3 = round($number, 3); # 小数点以下3桁まで求める print $number_0, "\n"; # --> 1 print $number_2, "\n"; # --> 1.23 print $number_3, "\n"; # --> 1.235四捨五入の基本は,正数の小数点以下を四捨五入して正整数にすることです. これをやるには,
0.5 を加えてで小数点以下を削除すればできます. 小数点以下 int
関数n 桁 のところで四捨五入したい場合は,まず 10 の(n - 1) 乗 します.これで四捨五入したい桁の 部分が小数第 1位 のところにきます. あとは基本どおりに四捨五入し,今度は 10 の-(n - 1) 乗 してもとの桁に戻します. 負数の場合は,0.5 を加えるのではなく引くというところが違い, あとは正数の場合と同じです.こうして求めた数値はそのままでは正確に求めたい桁数になっていない場合が あります.そこで最後に
を使って不要な桁を削除しています. sprintf
関数単に小数点以下の特定の桁までに数字を丸めたい場合には,次にように
sprintf
のを使えば可能です. %f
# $num を小数点以下 $decimals桁までに丸める(完全な四捨五入ではない) $num = sprintf("%.${decimals}f", $num);完全な四捨五入ではないと書いたのは,特定の条件下では 結果が四捨五入とならないからです.例えば 0.15 を小数第2位で四捨五入して,小数点以下1桁までにした結果の 0.2 を期待して,
と書いても, ほとんどの環境で 0.2 とはならずに 0.1 と表示されることでしょう. printf("%.1f\n", 0.15);
トップへ# $num を切り上げて小数点以下 $decimals桁にする sub ceil { my ($num, $decimals) = @_; my ($format, $tmp1, $tmp2); $format = '%.' . $decimals . 'f'; $tmp1 = $num * (10 ** $decimals); $tmp2 += $tmp1 <=> ($tmp2 = int($tmp1)); sprintf($format, $tmp2 / (10 ** $decimals)); }# 計算例 $number = 1.2345; $number_0 = ceil($number, 0); # 切り上げて整数にする $number_2 = ceil($number, 2); # 小数点以下2桁まで求める $number_3 = ceil($number, 3); # 小数点以下3桁まで求める print $number_0, "\n"; # --> 2 print $number_2, "\n"; # --> 1.24 print $number_3, "\n"; # --> 1.235このスクリプトの基本は「数字を四捨五入する」の スクリプトと同じです.実際に切り上げを行なっている部分は,
の直前の文です.この文をわかりやすく 書きかえると次のようになります. sprintf
関数$tmp2 = int($tmp1) + ($tmp1 <=> int($tmp1));これは,小数点以下を切り落とした数字と元の数字を比較して,もし違えば 切り上げるということを行なっています.切り上げる方向は絶対値が大きくなる 方向です.
モジュール のPOSIX
を使えば切り上げを行なうことができますが,この関数は切り上げて整 数にすることしかやってくれません.また,切り上げる方向は正の方向ですので,負 数を切り上げる場合には注意が必要です. ceil
関数
トップへ# 配列 @array から重複した要素を取り除く { my %count; @array = grep(!$count{$_}++, @array); }このようにすると配列の 要素の出現順序が保存されます. また,
ハッシュ には配列の要素をキーとし, その値には出現回数が入っています.出現回数の否定を条件式とすることで 重複した要素を取り除くことができます.具体的には,初めて出現したときは 出現回数を%count
0回 から1回 にするわけですが, そのときの条件式は ++
となり真となります. 次に出現したとき,つまり,重複していたときは, !0
出現回数 1 以上の数値に対しての否定となり必ず偽となります.
ハッシュ は局所化されていますので, このブロックを抜けた時点で自動的に消滅します. もし,出現回数を利用したいのであれば,このブロック内で利用するか, または,次のように書くことで後から利用することができます.%count
# 配列 @array から重複した要素を取り除く # 後から出現回数を利用したい undef(%count); @array = grep(!$count{$_}++, @array);
トップへ# 配列 @array をランダムに並び替える srand; for (my $i = @array; --$i; ) { my $j = int rand ($i + 1); next if $i == $j; @array[$i, $j] = @array[$j, $i]; }
は srand
関数を使う前に 一度だけ実行しておく必要があります.もし, rand
関数を実行しておかないといつも同じ結果に なってしまいます.ただ, srand
関数perl5.004 以降ではが使われたときに,まだ一度も rand
関数関数を 実行していなかった場合には自動で実行してくれます. srand
では $i = @array
がスカラーコンテキストで実行されますので, 配列の大きさを返します. このスクリプトの @array
は for
文(配列の大きさ - 1)回 実行され,の ブロックの中の for
文3つ 目の文で要素の 入れ替えを配列スライスを 使って行なっています.の初期値は配列の大きさ, つまり, $i
(最後の添え字の値 + 1) となっているのですが,条件文 が評価されるときに--$i
- 1 されますので,のブロックの中に 入るときには for
文(最後の添え字の値) になっています.もし,元の配列を残しておきたいのならば次のように書きます.
# 配列 @old をランダムに並び替えた配列 @new を作る srand; @new = (); foreach (@old) { my $r = int rand (@new + 1); push(@new, $new[$r]); $new[$r] = $_; }このスクリプトは配列の要素を入れ替えるという先ほどのスクリプトと違い, 要素を
1つ ずつ新しい配列に追加していくのですが, そのときに新しい要素は ランダムに選んだ場所に入れ,もともとそこにあった要素を一番最後に 移動させるというものです.
戻る