正規表現 : してログ

相対 URL を絶対 URL に解消する関数を探したのですが、ぴったりのものが見つからないので自分で作ってみました。ただし、簡単そうにみえて難しい部類なので、あらゆるパターンで完璧かどうかは保証できません。なお、HTML を処理すると言っても、<a>、<img> の2つのみが処理対象になっています。他のタグを含めるには、正規表現のパターンを追加してください。

ソースコード
function html_absolute_url($base,$html) {

	return preg_replace_callback(

		'/(<a\s+.*?href=")(.+?)(".*?>)'.
		'|(<a\s+.*?href=\')(.+?)(\'.*?>)'.
		'|(<img\s+.*?src=")(.+?)(".*?>)'.
		'|(<img\s+.*?src=\')(.+?)(\'.*?>)/',

		create_function(
			'$matches',
			'$base = unserialize(\''.serialize($base).'\');'.
			'$n = (count($matches)-1)/3-1;'.
			'$url = $matches[$n*3+2];'.
			'$url = absolute_url($base,$url);'.
			'return $matches[$n*3+1].$url.$matches[$n*3+3];'
		),

		$html
	);

}

function absolute_url($base,$url) {

	if (substr($base,-1)!='/') $base.='/';

	$pbase = parse_url($base);

	$purl = parse_url($url);

	if (!isset($purl['scheme'])) {
		if (!isset($purl['host'])) {
			// スキームとホストが省略されている
			if (substr($url,0,1)=='/') {
				// 絶対URL
				$url = $pbase['scheme'].'://'.$pbase['host'].$url;
			} else {
				// 相対URLと判定(./と../の解消は行わない)
				$url = $base.$url;
			}
		} else {
			// スキームのみ省略されている
			$url = $pbase['scheme'].':'.$url;
		}
	} else {
		// 完全なURLと判定
	}

	return $url;
}

html_absolute_url が HTML を処理する関数で、absolute_url が URL を個別に処理する関数です。PHP5.3 未満に対応するために、preg_replace_callback のコールバック関数でクロージャを使用しておらず、変数の値を無理やり展開して渡しています。また、正規表現の中でシングルとダブルクオーテーションをそれぞれ記述していますが、まとめてうまく表現できないものかと思います。もっとスマートに書ける人がいたら、教えて欲しいです。

PCRE 修飾子で「s」を指定した場合「ドットメタ文字は改行を含むすべての文字にマッチします」、「 これを設定しない場合は、改行にはマッチしません」とあります。例えば「/^(.+)$/m」のように書けば、複数行テキストを配列に取ることができるはずなのですが、実際やってみると改行を含むテキストが返ってきます。対処法で trim を掛けたりしていましたが、せっかくなので調べてみました。

改行の意味するもの

改行の種類として「LF」「CR+LF」「CR」の3種類があると思いますが、実際にテストコードを書いてみたところ、ドットメタ文字は「CR」にマッチし「LF」にマッチしませんでした。マニュアルには単に「改行」と書かれているので、これらのすべてに対応しているものと思い込んでおりましたが、違うようです。従って、上の例で改行に見えていたのは「CR+LF」の「CR」が残っていたものと思われます。

改行を配列に取る正しい例

原因が分かれば話は簡単で、文字クラスの否定を使って「CR」及び「LF」を指定すれば良いだけです。

preg_match_all('/^([^\r\n]+)$/m', $text, $match);
まとめ

マニュアルなどで単に「改行」と言った場合は「LF」のみを指すと思って良さそうです。プログラム書くときも特に意識しないで "\n" を使ってますし、UNIX 系では「LF」なのだから当然と言えば当然でした。毎回悩むのもアレなので、改行テキストを綺麗な配列に取る関数を自作しておくと楽かも知れません。

要注意なのは、HTML の改行コードが「CR+LF」で規定されている点です。このため、textarea で POST されたデータは「CR+LF」になっており、取り扱う際に注意しなければなりません。また、PHP などで HTML を出力する際の改行コードを「LF」のみとしてしまうのは、本来間違いということになります(今まで結構やってたなあ)。多くの場合、「LF」で出力しても、「CR+LF」「LF」が混在していてもブラウザの表示には影響しませんが、気になる方は HTML は「CR+LF」と覚えておくといいかも知れません。

正規表現で html を処理する下記のようなコード、一行コメントアウト(ダブルスラッシュ)でコメントアウトすると致命的なエラーになります。これ、何故だか分かりますか? 私はこれで小一時間ほど、悶々としていました。

preg_match('/<a.+?>(.+?)<\/a>/su',$html,$match);

これは一行コメントアウトが、それを書いた場所から改行コードまでをコメントアウトするもの、という認識でいる限り分かりません。正しくは「改行コードまたは PHP コードブロックの終わり」までをコメントアウトするもの(こちらのマニュアル参照)です。PHP コードブロックの終わりとは、すなわち「?>」のことであり、上記の正規表現内に文字の並びとして現れています。

結果、行末までのコメントアウトとならず、途中で PHP コードブロックから外れて直接ソースコードが出力されるばかりか、致命的な構文エラーの原因となってしまいます。このようなコードをコメントアウトしたい場合は、複数行コメント(「/*」と「*/」)で囲みます。

特に、今回のような html を処理する場合の正規表現式で出てきそうな、この形は注意したいところです。