レベル: 中級 Martin Streicher (martin.streicher@gmail.com), Editor in Chief, McClatchy Interactive
2008年 1月 01日 パターン・マッチングはソフトウェアでは非常に一般的な作業です。そのため、そうした作業を手軽に行うための特別な手法として、正規表現が進化してきました。この手軽な手法をコードの中でどう使えばよいのか、この「PHP での正規表現をマスターする」シリーズの第 1 回である今回の記事で学びましょう。
すべてのマシンは、入力されたものを使って何らかの処理を行い、出力を生成します。例えば電話は、音声のエネルギーを電気信号に変換し、そしてその電気信号を音声に再度変換して会話を可能にします。エンジンは燃料 (蒸気や核分裂、ガソリン、あるいは人間の肉体労働) を取り込み、それを仕事に変換します。ブレンダー (カクテルを作る機械) はラム酒、氷、ライム、キュラソーを飲み込んで激しく攪拌し、マイタイを作り出します。(あるいは、もっと都会的なものがお好みであれば、シャンパンと洋梨のネクターを使ってベリーニを試すこともできます。ブレンダーは本当にいろいろなカクテルが作れて素晴らしいマシンです。)
ソフトウェアはデータを変換するものであるため、それぞれのアプリケーションもマシンです (もっとも、物理的な実体がないことを考えると「仮想的な」マシンですが)。例えばコンパイラーは入力としてソース・コードを想定し、そのソース・コードを、実行に適したバイナリー・コードに変化させます。気象予報士は過去の測定値に基づいて予報を発表します。また画像エディターは、入力された画素の集合を対象に作業を行い、例えば画像をシャープにしたりスタイリングしたりするために 1 つ 1 つの画素やグループの画素を対象にルールを適用し、その画素の集合を出力します。
他のすべてのマシンとまったく同じように、ソフトウェア・アプリケーションも何らかの原料、例えば数字のリストや XML スキーマにカプセル化されたデータ、あるいはプロトコルなどを想定しています。もしプログラムに誤った (つまり型や形式が異なる) 原料が入力されると、結果を予測できない可能性が高く、破滅的なことさえ起こるかもしれません。格言にあるとおり、「ガーベジイン・ガーベジアウト (Garbage In, Garbage Out: ゴミのような情報を入力してもゴミのような情報しか得られない)」というわけです。
ごく些細な問題以外の、ほとんどすべての問題の場合、不適切なデータから適切なデータを拾い出したり、誤った出力を防ぐために不適切なデータを拒否したり、あるいはその両方を行う必要があります。これはまさに、PHP による Web アプリケーションにも当てはまります。入力が手動で行われるにせよ、あるいはプログラムによる Ajax (Asynchronous JavaScript + XML) リクエストで行われるにせよ、プログラムは何か計算が行われる前に入力情報を入念に検査する必要があります。ある数値は一定の範囲に収まっていなければならないかもしれませんし、あるいは整数に制限しなければならないかもしれません。また、ある値は特定の書式、例えば郵便を配達するためのコードなどと一致する必要があるかもしれません。例えば、米国の ZIP コード (郵便番号) は、5 桁の数字、ハイフン、そして追加の 4 桁の数字から成るオプションの「プラス 4」修飾子とで構成されています。他のストリングも一定数の文字でなければならないかもしれません (例えば米国の各州を省略表記するための 2 文字など)。ストリングの入力の場合は特に、不正行為が隠されている可能性があるため、PHP アプリケーションでは、アプリケーションの振る舞いを変更したりセキュリティーを回避したりできる SQL クエリーや JavaScript コードなどのコードを埋め込む悪意のアクターに対して、十分に警戒する必要があります。
しかしプログラムは、入力が数字であるかどうか、あるいは一定の規則 (例えば郵便コードの規則) に従っているかどうかを、どのように判断するのでしょう。基本的には、突き合わせを行うためにはちょっとしたパーサーが必要です (このパーサーがステート・マシンを作成し、入力を読み取り、トークンを処理し、状態をモニターし、そして結果を出力します)。しかし、たとえ単純なパーサーであっても、それを作成して維持管理するのは楽ではありません。
幸いなことに、パターン・マッチング分析はコンピューティングの世界では非常に一般的に要求されるため、そうした作業を手軽に行うための特別な手法と、そしてもちろんエンジンが、(UNIX® の幕開けあたりから) 長年にわたって進化してきました。正規表現 (regex) は、簡潔で理解しやすい記法でパターンを記述します。正規表現エンジンは、正規表現とデータが与えられると、そのデータがあるパターンにマッチしているかどうかと、マッチしている場合はそのマッチしている箇所を出力します。
ここで、正規表現を適用した簡単な例を示します。この例は UNIX のコマンドライン・ユーティリティーである grep から引用したもので、1 つあるいは複数の UNIX テキスト・ファイルの内容の中で、ある指定されたパターンを検索します。コマンド grep -i -E '^Bat' は、(キャレット [^] で示されるように) beginning-of-line (行頭) の直後に大文字または小文字の b、a、t が続くシーケンスを検索します (-i オプションによってパターン・マッチングでの大文字小文字が無視されるため、例えば B と b は等価です)。そこで、以下のような heroes.txt というファイルを用意しました。
リスト 1. heroes.txt
Catwoman
Batman
The Tick
Black Cat
Batgirl
Danger Girl
Wonder Woman
Luke Cage
The Punisher
Ant Man
Dead Girl
Aquaman
SCUD
Blackbolt
Martian Manhunter
|
このファイルに上記の grep コマンドを実行すると、マッチしたものが 2 つ出力されます (下記)。
正規表現
PHP は正規表現用のプログラミング・インターフェースを 2 つ提供しています。1 つは POSIX (Portable Operating System Interface) であり、もう 1 つが PCRE (Perl Compatible Regular Expressions) です。一般的に、後者が好まれます。その理由は、PCRE の方が POSIX 実装よりもずっと強力であり、Perl に用意されているすべての演算子を提供しているためです。POSIX の正規表現関数コールの詳細に関しては、PHP のドキュメンテーションを読んでください (「参考文献」を参照)。ここでは PCRE 機能に焦点を絞ることにします。
PHP の PCRE 正規表現には、特定の文字やその他の演算子との突き合わせを行う演算子や、ストリングの最初または最後など特定の場所に対して突き合わせを行う演算子、あるいは単語の最初または最後に対して突き合わせを行う演算子が含まれています。また他にも正規表現で記述できる内容としては、(「これ、または、あれ」のように記述できる) 複数候補や、固定長、可変長、あるいは無限長の反復や、また文字のセット (例えば「a から m までの任意の文字」など) や、文字クラスつまり文字の種類 (印刷可能な文字、あるいは句読点) などを記述することもできます。正規表現の特殊演算子を使うと、グループ化 (ある演算子を他の演算子群とまとめて一括適用する方法) を行うこともできます。
表 1 は一般的な正規表現演算子の一部を示しています。表 1 の基本演算子 (そして他の演算子) は連結や組み合わせが可能であり、それらの組み合わせを使うことで、(非常に) 複雑な正規表現を作成することができます。
表 1. 一般的な正規表現演算子
| 演算子 | 用途 |
|---|
| . (ピリオド) | 任意の 1 文字との突き合わせを行います |
|---|
| ^ (キャレット) | 行、またはストリングの最初に発生する空のストリングとの突き合わせを行います |
|---|
| $ (ドル記号) | 行の最後に発生する空のストリングとの突き合わせを行います |
|---|
| A | 大文字の A との突き合わせを行います
|
|---|
| a | 小文字の a との突き合わせを行います
|
|---|
| \d | 任意の 1 桁の数字との突き合わせを行います |
|---|
| \D | 数字以外の任意の 1 文字との突き合わせを行います |
|---|
| \w | 任意の英数字 1 文字との突き合わせを行います (シノニムは [:alnum:] です)
|
|---|
| [A-E] | 大文字の A、B、C、D、E のいずれかとマッチするかどうかの突き合わせを行います
|
|---|
| [^A-E] | 大文字の A、B、C、D、E 以外の任意の文字とマッチするかどうかの突き合わせを行います
|
|---|
| X? | X の位置に文字が存在しない場合との突き合わせ、および 1 個の大文字の X との突き合わせを行います
|
|---|
| X* | ゼロ個以上の大文字の X との突き合わせを行います |
|---|
| X+ | 1 個以上の大文字の X との突き合わせを行います |
|---|
| X{n} | 正確に n 個の大文字の X との突き合わせを行います |
|---|
| X{n,m} | n 個以上 m 個以下の大文字の X との突き合わせを行います。m を省略すると、n 個以上の X と突き合わせを行います |
|---|
| (abc|def)+ | abc または def という文字列の少なくとも一方が 1 個以上あるかどうかの突き合わせを行います (abc および def はマッチします) |
|---|
正規表現の一般的な使い方として、次のような例があります。例えば、ある Web サイトは各ユーザーに対してログインを作成するように要求するとします。各ユーザー名は 3 文字以上 10 文字以下の英数字であり、かつ英文字で始まる必要があります。この仕様を必ず満たすようにするために、ユーザー名がアプリケーションに送信されたら、^[A-Za-z][A-Za-z0-9_]{2,9}$ という正規表現を使ってユーザー名を検証することもできます。
キャレットによってストリングの先頭を検出します。最初のセット [A-Za-z] は任意の英文字を表します。2 番目のセット [A-Za-z0-9_]{2,9} は、2 個以上 9 個以下の、任意の英文字、任意の数字、そしてアンダースコアーの連続を表します。そしてドル記号 ($) によってストリングの最後を検出します。
ちょっと見ると、ドル記号は不必要に思えるかもしれませんが、実は重要なのです。もしドル記号を省略すると、この正規表現は、「1 個の英文字で始まり、2 個から 9 個の英数字が続き、最後は任意の数の任意の文字で終わる」すべてのストリングを検出してしまいます。つまりストリングの終わりを示すドル記号がないと、先頭部分がマッチする非常に長いストリング、例えば「martin1234-cruft」などによってフォルス・ポジティブが発生します。
PHP と正規表現をプログラミングする
PHP には、テキスト内でマッチする部分を見つける関数や、マッチする部分を見つけるごとに別のテキストに置き換える (検索置換を行う) 関数、そしてリストの要素の中からマッチする部分を見つける関数などが用意されています。以下に示すのはそうした関数です。
-
preg_match()
-
preg_match_all()
-
preg_replace()
-
preg_replace_callback()
-
preg_grep()
-
preg_split()
-
preg_last_error()
-
preg_quote()
これらの関数の説明のために、特定のパターンを求めて単語のリストを検索する簡単な PHP アプリケーションを作成してみましょう。このアプリケーションでは、単語と正規表現は従来の Web フォームで提供され、検索結果は単純な関数 print_r() を使ってブラウザーに表示されます。こうした簡単なプログラムは、正規表現を試したり改善したりしたいときに便利です。
リスト 2 は、その PHP コードを示しています。すべての入力は単純な HTML のフォームから提供されます。(簡潔にするために、対応するフォームは示しておらず、また PHP コードのエラーをトラップするためのコードも省略してあります。)
リスト 2. パターンとテキストを比較する
<?php
//
// divide the comma-separated list into individual words
// the third parameter, -1, permits a limitless number of matches
// the fourth parameter, PREG_SPLIT_NO_EMPTY, ignores empty matches
//
$words = preg_split( '/,/', $_REQUEST[ 'words' ], -1, PREG_SPLIT_NO_EMPTY );
//
// remove the leading and trailing spaces from each element
//
foreach ( $words as $key => $value ) {
$words[ $key ] = trim( $value );
}
//
// find the words that match the regular expression
//
$matches = preg_grep( "/${_REQUEST[ 'regex' ]}/", $words );
print_r( $_REQUEST['regex' ] );
echo( '<br /><br />' );
print_r( $words );
echo( '<br /><br />' );
print_r( $matches );
exit;
?>
|
まず、カンマで区切られた単語から成るストリングが、preg_split() 関数を使って個々の要素に分割されます。この関数は、指定された正規表現とマッチするすべてのポイントでストリングを分割します。ここでは、その正規表現は「 , 」(カンマ: カンマ区切りリストという名前の元になっている区切り文字) のみです。コードの中でカンマの前後に置かれているスラッシュは、この正規表現の開始と終了を示しています。
preg_split() の、3 番目と 4 番目の引数はオプションですが、どちらも便利なものです。3 番目の引数として整数 n を渡すと、マッチするもののうち最初の n 個のみが返されます。また -1 を渡すと、マッチするものがすべて返されます。4 番目の引数としてフラグ PREG_SPLIT_NO_EMPTY を渡すと、preg_split() によって空の結果がすべて削除されます。
次に、カンマで区切られた単語のリストの各要素は、trim() 関数によって余分なもの (前後の空白) が削除され、指定された正規表現と比較されます。関数 preg_grep() によってリストの各要素を比較する処理は非常に容易になります (最初の引数としてパターンを提供し、そして 2 番目の引数として、突き合わせるための単語の配列を提供すればよいだけです)。するとこの関数はマッチしたものの配列を返します。
例えば、パターンとして正規表現 ^[A-Za-z][A-Za-z0-9_]{2,9}$ を入力し、配列にはさまざまな長さの単語のリストを指定すると、リスト 3 のような実行結果が得られます。
リスト 3. 単純な正規表現による実行結果
^[A-Za-z][A-Za-z0-9_]{2,9}$
Array ( [0] => martin [1] => 1happy [2] => hermanmunster )
Array ( [0] => martin )
|
また、オプションのフラグ PREG_GREP_INVERT を使うと、preg_grep() による演算が反転され、そのパターンにマッチしない要素を見つけることができます (コマンドラインでの grep -v と同じです)。それには、22 行目を $matches = preg_grep( "/${_REQUEST[ 'regex' ]}/", $words, PREG_GREP_INVERT ) で置き換え、リスト 3 の入力を再度使用します。すると、Array ( [1] => 1happy [2] => hermanmunster ) が得られます。
ストリングを分解する
preg_split() と preg_grep() は簡単ながら強力な関数です。preg_split() は、もし予測可能なパターンでサブストリングが区切られていれば、ストリングをサブストリングに分解することができます。関数 preg_grep() も、リストを迅速にフィルタリングします。
しかし、1 つ、あるいはそれ以上の複雑なルールを使ってストリングを分解しなければならない場合はどうなるのでしょう。例えば、米国の電話番号は通常、「(305) 555-1212」や「305-555-1212」あるいは「305.555.1212」の形式で表現されます。もし区切り用の記号をなくすと、どれも 10 桁となり、正規表現 \d{10} を使って簡単に認識することができます。しかし、米国では 3 桁の市外局番 (area code) と 3 桁の市内局番 (prefix) は 0 または 1 で始まることができません (0 と 1 は市外通話用の番号であるため)。この数字のシーケンスを個々の桁に分割して複雑なコードを作成しなくても、正規表現を使うことで、有効な電話番号かどうかをテストすることができます。
リスト 4 は、この作業を行うコード・スニペットを示しています。
リスト 4. 電話番号が米国の電話番号として有効かどうかを判断する
<?php
$punctuation = preg_quote( "().-" );
$number = preg_replace( "/[$punctuation]/", '', $_REQUEST[ 'number' ] );
$valid = "/[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}/";
if ( preg_match( $valid, $number ) == 1 ) {
echo( "${_REQUEST[ 'number' ]} is valid<br />" );
}
exit;
?>
|
このコードをステップごとに調べてみましょう。
-
表 1 に示したように、正規表現はいくつかの演算子 (例えば大括弧
[ ] など) を使って、文字のセットを指定します。対象となるテキストの中で、そうした演算子として使われる文字との突き合わせをしたい場合には、正規表現の中でその演算子を「エスケープ」するために、その演算子の前にバックスラッシュ (\) を付ける必要があります。その演算子をエスケープしてしまえば、他のリテラルと同じように突き合わせをすることができます。例えば、完全修飾のホスト名などに見られるように、リテラルのピリオドとの突合せを行いたい場合には、 \. と記述します。あるいはオプションとして、1 行目のように、preg_quote() にストリングを渡すと、preg_quote() は発見したすべての正規表現演算子を自動的にエスケープします。もし 1 行目の後で echo() $punctuation を実行すると、\(\)\.- が表示されるはずです。
- 2 行目は電話番号からすべての区切り文字を削除します。
preg_replace() 関数は、$punctuation の中に指定されている文字を見つけると (このためにセット演算子 [ ] が使われています)、その文字を空のストリングで置き換え、実質的にその文字を削除します。そして新しいストリングが返され、$number に割り当てられます。
- 4 行目は有効な米国の電話番号のパターンを定義しています。
- 5 行目は、数字のみとなった電話番号をこのパターンと比較し、突き合わせを行います。マッチするものがある場合、関数
preg_match() は 1 を返します。マッチするものが見つからない場合には 0 を返します。処理中にエラーが発生した場合には、False を返します。従って、マッチしたかどうかをチェックするためには、戻り値が 1 かどうかを調べます。戻り値が 1 以外の場合には preg_last_error() の結果をチェックします (PHP V5.2.0 またはそれ以降を使っている場合)。その結果が 0 ではない場合には、計算の制限 (正規表現の再帰の深さなど) を超えたのかもしれません。PHP の正規表現で使用される定数と制限についての説明は、PCRE Regular Expression Functions のページにあります (「参考文献」を参照)。

 |
キャプチャー
例えばデータ検証の場合のように、必要なことは「マッチするかどうか」というテストのみ、ということがよくあるものです。しかし正規表現はそれよりも、マッチすることを証明するために、そしてそのマッチしたその情報を抽出するために使われることの方が多いものです。
電話番号の例に戻ると、マッチするものが見つかった場合には、市外局番と市内局番、そして加入者番号を、データベースの中の別のフィールドに保存することもできます。正規表現はマッチした場合、その内容をキャプチャーによって記憶することができます。キャプチャー演算子には括弧が使われ、正規表現のどこにでも置くことができます。またキャプチャーをネストし、大きなキャプチャーのサブセグメントを見つけることもできます。例えば、10 桁の電話番号の市外局番と市内局番、そして加入者番号をキャプチャーするためには、以下のようにします。
/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/
|
マッチするものが見つかると、最初の 3 桁は最初の 1 対の括弧の中にキャプチャーされ、次の 3 桁は 2 対目の括弧の中に、そして最後の 4 桁が残りの 1 対の括弧の中にキャプチャーされます。preg_match() の呼び出しをうまく利用すると、キャプチャーしたものを取得することができます。
リスト 5. キャプチャーしたものを preg_match() によって取得する
$valid = "/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";
if ( preg_match( $valid, $number, $matches ) == 1 ) {
echo( "${_REQUEST[ 'number' ]} is valid<br />" );
echo( "Entire match: ${matches[0]}<br />" );
echo( "Area code: ${matches[1]}<br />" );
echo( "Prefix: ${matches[2]}<br />" );
echo( "Number: ${matches[3]}<br />" );
}
|
この場合の $matches のように、preg_match() の3 番目の引数を変数にすると、その変数は、キャプチャー結果のリストに設定されます。ここではそれぞれ、ゼロ番目の要素 (インデックスは 0) は正規表現にマッチした全体であり、1 番目の要素は、1 番目の 1 対の括弧に関してマッチした部分であり、等々です。
ネストしたキャプチャーは、セグメントとサブセグメントを実質的に任意の深さまでキャプチャーします。ネストしたキャプチャーで面倒なことは、それぞれのマッチした箇所が、マッチした結果を格納している配列 (例えば $matches など) のどこに現れるかを予測しなければならないことです。その場合には、正規表現の先頭から左括弧の数をカウントし、そのカウントを配列のインデックスとする、というルールに従います。
リスト 6 は住所 (street address) の各部分を抽出する (少しずるい) 例です。
リスト 6. 住所の各部分を抽出するコード
$address = "123 Main, Warsaw, NC, 29876";
$valid = "/((\d+)\s+(\w+)),\s+(\w+),\s+([A-Z]{2}),\s+(\d{5})/";
if ( preg_match( $valid, $address, $matches ) == 1 ) {
echo( "Street: ${matches[1]}<br />" );
echo( "Street number: ${matches[2]}<br />" );
echo( "Street name: ${matches[3]}<br />" );
echo( "City: ${matches[4]}<br />" );
echo( "State: ${matches[5]}<br />" );
echo( "Zip: ${matches[6]}<br />" );
}
|
この場合も、正規表現にマッチしたパターン全体はインデックス 0 にあります。street number はどこにあるのでしょう。左から数えると、\d+ によって Street number の突き合わせが行われ、その \d+ を囲んでいる左括弧は左から 2 番目です。従って、$matches[2] は 123 です。$matches[4] には City (市町村名) が、$matches[6] には Zip (郵便番号) が格納されます。
強力な手法
テキストの処理は非常に一般的であり、PHP は大量の操作を容易にするために、いくつかの機能を提供しています。以下に示すのはそのためのヒントです。
-
preg_replace() 関数は、1 つのストリングまたはストリングの配列に対して操作を行うことができます。1 つのストリングではなくストリングの配列を使って preg_replace() を呼び出すと、その配列の中のすべての要素が処理されて置き換えが行われます。この場合、preg_replace() は変更されたストリングの配列を返します。
- 他の PCRE 実装と同様、置き換えたものの中からもサブパターンとマッチするものを参照することもでき、自己参照型の操作とすることができます。この一例として、電話番号のフォーマットを統一する問題を考えてみてください。すべての区切り文字を削除し、ドットで置き換えるという問題です。そのための 1 つのソリューションをリスト 7 に示します。
リスト 7. 区切り文字をドットで置き換える
$punctuation = preg_quote( "().-" );
$number = preg_replace( "/[$punctuation]/", '', $_REQUEST[ 'number' ] );
$valid = "/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";
$standard = preg_replace( $valid, "\\1.\\2.\\3", $number );
if ( strcmp ($standard, $number) ) {
echo( "The standard number is $standard<br />" );
}
|
パターンに対するテストと、パターンがマッチした場合の標準的な電話番号への変換が、1 つのステップで行われます。
まとめ
PHP アプリケーションが扱うデータは次第に大量になってきています。フォーム入力の検証が必要な場合であれ、内容を分解する必要がある場合であれ、正規表現は大いに役立ちます。
参考文献 学ぶために
製品や技術を入手するために
- 皆さんの次期オープン・ソース開発プロジェクトを IBM trial software を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
-
IBM 製品の試用版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。
議論するために
著者について  | |  | Martin Streicherは McClatchy Interactive の最高技術責任者であり、Linux Magazineの編集長であり、Web 開発者であり、また developerWorks への頻繁な寄稿者でもあります。彼は Purdue Universityでコンピューター・サイエンスの修士号を取得しており、1986年以来、UNIX ライクのシステムでプログラミングを行ってきています。 |
記事の評価
|