-->

2010-04-03

php の ob_start の利用例 (preg_replace の正規表現の例)

html を出力する前にスペースを取り除く例です。
<?php
function callback($buffer)
{
    $buf = $buffer;
    $pattern = array();
    $replace = array();

    // 改行を \n に統一する。
    $pattern[] = "/(?:\r\n)|[\r\n]/";
    $replace[] = "\n";

    // \n を除く制御文字を削除する。
    $pattern[] = "/[\\x00-\\x09\\x0b-\\x1f]/";
    $replace[] = " ";

    // 反映させる。
    $buf = preg_replace($pattern , $replace, $buf);

    // 修正しない部分を保管して代替文字に入れ替える。
    $tmpName = "__TMP__";
    $i = 0;
    do
    {
        if (!isset($GLOBALS[$tmpName . $i]))
        {
            $tmpName .= $i;
            break;
        }
        if ($i > 10)
        {
            $tmpName .= md5(mt_rand()) . md5(mt_rand());
            break;
        }
        $i++;
    }
    while (true);
    $GLOBALS[$tmpName] = array();
    $buf = preg_replace_callback(
        "/<(pre|s(?:cript|tyle)|xmp)(?:\s*|(?:\s+[^>]+))>(.*?)<\/\\1\s*>/is",
        create_function('$matches',
                        '$tmp =& $GLOBALS["' . $tmpName . '"];' .
                        'if ($matches[2] === "") { return $matches[0]; }' .
                        '$tmp[] = $matches[0];' .
                        'return "<\\x00," . count($tmp) . ",\\x01>";'),
        $buf);

    $pattern = array();
    $replace = array();

    // 最初と最後の空白を削除する。
    $pattern[] = "/^\s+|\s+$/";
    $replace[] = "";

    // コメントを削除する。
    $pattern[] = "/<\!\-\-.*?(?<=\-\-)>/s";
    $replace[] = "";

    // 行ごとの最初と最後の半角スペースを削除する。
    $pattern[] = "/^ +| +$/m";
    $replace[] = "";

    // 改行を減らす。
    $pattern[] = "/\\n{3,}/";
    $replace[] = "\n\n";

    // 半角スペースを減らす。
    $pattern[] = "/ {2,}/";
    $replace[] = " ";

    // <, > に接触する空白を削除する。
    $pattern[] = "/\s*([<>])\s*/";
    $replace[] = "$1";

    // </head>までのタグに改行を追加する。
    $pattern[] = "/^(.*?)(<\/head>)/eis";
    $replace[] = "rtrim(preg_replace(\"/><(?!\\/)/\", \">\\n<\", \"$1\")) . \"\\n$2\\n\"";

    // ブロック要素の終わりに改行を追加する。
    $pattern[] = "/<\/(?:address|blockquote|c(?:aption|ol(?:group)?)|d(?:iv|[dlt])|" .
        "f(?:ieldset|orm)|h[1-6r]|l(?:egend|i)|noscript|ol|p(?:re)?|" .
        "t(?:able|body|foot|head|[dhr])|ul)>/i";
    $replace[] = "$0\n";

    // brタグの終わりに改行を追加する。
    $pattern[] = "/<br((?:\s*)|(?:\s+[^>]+?))\/?" . ">/ei";
    $replace[] = "\"<br\" . preg_replace(\"/^\\s+|\\s+$/\", \" \", \" $1 \") . \"/>\\n\"";

    // 反映させる。
    $buf = preg_replace($pattern , $replace, $buf);

    // 代替文字を元に戻す。
    $buf = preg_replace_callback(
        "/<\\x00,(\d+),\\x01>/",
        create_function(
            '$matches',
            '$tmp =& $GLOBALS["' . $tmpName . '"];' .
            'return $tmp[$matches[1] - 1];'
            ),
        $buf);

    return $buf;
}
ob_start("callback");

主な用途はタグに隣接するスペースの削除です。
<tag>\n
  <tag>\n
      This is an example.\n
  </tag>\n
</tag>\n

htmlソースの見易さのために、ブロック要素の終わりに改行を追加するため、スタイルシートなどでブロック要素を display: inline; にしている場合、余分なスペースが発生します。

以下は詳細です。
http://jp.php.net/manual/ja/reference.pcre.pattern.syntax.php

    // 改行を \n に統一する。
    $pattern[] = "/(?:\r\n)|[\r\n]/";
    $replace[] = "\n";
\r\n を優先して検索して、あれば \n に変更し、無ければ \r を \n に変更します。(\n を \n に変換する意味の無い処理も含まれます)

    // \n を除く制御文字を削除する。
    $pattern[] = "/[\\x00-\\x09\\x0b-\\x1f]/";
    $replace[] = " ";
\n(10番)を除く 0 ~ 31番の文字を 32番(半角スペース)へ変更します。
http://ja.wikipedia.org/wiki/ASCII#ASCII.E5.88.B6.E5.BE.A1.E6.96.87.E5.AD.97

UCS-4 などの該当部分が1バイトではない文字コードは破壊されます。
表現は16進数です。 (\x0A → \10, \x1F → \31, \x20 → \32)
[ ... ] で括った場合で 文字-文字 となっている場合、範囲を指定しています。
例えば半角スペースを \x20 と表現した場合の利点は、 \x00-\x20 と書いた場合見やすいことや、スペースを無視する正規表現 ( /pattern/x ) の際に半角スペースを指定する、などです。

    // 修正しない部分を保管して代替文字に入れ替える。
    $tmpName = "__TMP__";
    ...
    $GLOBALS[$tmpName] = array();
    $buf = preg_replace_callback(
        "/<(pre|s(?:cript|tyle)|xmp)(?:\s*|(?:\s+[^>]+))>(.*?)<\/\\1\s*>/is",
        create_function('$matches',
                        '$tmp =& $GLOBALS["' . $tmpName . '"];' .
                        'if ($matches[2] === "") { return $matches[0]; }' .
                        '$tmp[] = $matches[0];' .
                        'return "<\\x00," . count($tmp) . ",\\x01>";'),
        $buf);
pre, script, style, xmp タグを保管場所に移動します。

利用していないグローバル変数を検索して空の配列とし、文字列の保管場所にします。
    $tmpName = "__TMP__";
    ...
    $GLOBALS[$tmpName] = array();

タグの中身が空の場合には移動をしません。
http://jp.php.net/manual/ja/function.preg-replace-callback.php
'if ($matches[2] === "") { return $matches[0]; }' .

始まりのタグ
<(pre|s(?:cript|tyle)|xmp)(?:\s*|(?:\s+[^>]+))>

大文字小文字を無視します。( /pattern/i )

中身
(.*?)

改行を含む任意の文字にマッチします。( /pattern/s )
中身に </script> などがあれば失敗し、期待した動作になりません。
? をつけない場合、最後の </script> にマッチします。
(量指定子の後に疑問符を付けると、貪欲さは消え、できるだけ少ない回数だけマッチします付近の説明)

終わりのタグ
<\/\\1\s*>

\\1 は後方参照で始まりのタグで指定した pre, script, style, xmp の、どれかが入ります。(括弧の始まりの後ろに ?: と付かない (...) で囲まれた部分です)
phpの正規表現は文字列に正規表現を入れるので \\1 は 動作する際には \1 になっています。
例えば \\n, \n は \n という文字か、改行(line feed)自体の文字かの違いになりますので変化は無いです。
スペースを無視する正規表現( /pattern/x ) では、その違いは重要です。

    // 最初と最後の空白を削除する。
    $pattern[] = "/^\s+|\s+$/";
    $replace[] = "";
^ は文字列の始まりです。
$ は文字列の終わりです。
| は、or、もしくは、の意味です。
\s は改行を含む空白です。

    // コメントを削除する。
    $pattern[] = "/<\!\-\-.*?(?<=\-\-)>/s";
    $replace[] = "";
<!-- という文字を探し、あった場合、後ろに続く処理を行います。
.*? は改行を含む任意の文字にマッチしますが、常に最短の短さでマッチしようとしますので > の有る無しを常に探します。
> があった場合 > の手前の文字が -- であるかどうかをチェックします。
> の手前の文字が -- であった場合(<!--> など)マッチしたことになります。

    // 行ごとの最初と最後の半角スペースを削除する。
    $pattern[] = "/^ +| +$/m";
    $replace[] = "";
行ごとの最初と最後にマッチします。( /pattern/m )

// "/^ +| +$/m" の場合
  aaa  
  bbb  
  ccc  

// "/^ +| +$/" の場合
  aaa  
  bbb  
  ccc  

    // 改行を減らす。
    $pattern[] = "/\\n{3,}/";
    $replace[] = "\n\n";
\n が 3回以上続く場合 \n\n に変換します。

    // 半角スペースを減らす。
    $pattern[] = "/ {2,}/";
    $replace[] = " ";
半角スペースが2個以上続く場合、半角スペース1個に変換します。

繰り返しの例です。
a が0~1の時にマッチします。
a?
a{0,1}

a が0文字以上の時にマッチします。
a*
a{0,}

a が1文字以上の時にマッチします。
a+
a{1,}

文字の指定の後の ? と、文字の連続する回数の指定の後の ? が意味が違います。
後者は最小限の長さでマッチしてください、という意味です。

aab? に bが含まれます。
$ php -r '$buf = "aabbcc"; if (preg_match("/(aab?)(b*cc)/", $buf, $m)) { var_dump($m); }'
array(3) {
  [0]=>
  string(6) "aabbcc"
  [1]=>
  string(3) "aab"
  [2]=>
  string(3) "bcc"
}

aab?? に bが含まれません。
$ php -r '$buf = "aabbcc"; if (preg_match("/(aab??)(b*cc)/", $buf, $m)) { var_dump($m); }'
array(3) {
  [0]=>
  string(6) "aabbcc"
  [1]=>
  string(2) "aa"
  [2]=>
  string(4) "bbcc"
}

aab?? に bが含まれます。bcc の部分の b の長さが固定なためです。
$ php -r '$buf = "aabbcc"; if (preg_match("/(aab??)(bcc)/", $buf, $m)) { var_dump($m); }'
array(3) {
  [0]=>
  string(6) "aabbcc"
  [1]=>
  string(3) "aab"
  [2]=>
  string(3) "bcc"
}

    // <, > に接触する空白を削除する。
    $pattern[] = "/\s*([<>])\s*/";
    $replace[] = "$1";
$1 は ( ... ) で指定した <, > のどちらかが入ります。

    // </head>までのタグに改行を追加する。
    $pattern[] = "/^(.*?)(<\/head>)/eis";
    $replace[] = "rtrim(preg_replace(\"/><(?!\\/)/\", \">\\n<\", \"$1\")) . \"\\n$2\\n\"";
head の部分の大文字小文字を無視します。 ( /pattern/i )
.* の部分の任意の文字に改行を含めます。 ( /pattern/s )
( /pattern/e ) は $replace[] の部分を phpのコードとして解釈します。

php コード自体で書いた場合下記のような意味です。($matches に配列でマッチした文字が入っている場合)
rtrim(preg_replace("/><(?!\/)/", ">\n<", "{$matches[1]}")) . "\n{$matches[2]}\n";

"/><(?!\/)/"
>< にマッチしますが < の後ろに / が有る場合、マッチしません。(マッチする内容に / を含みません)

>< を >(改行)< に変換後に、文字列の後ろのスペースを取り除き(rtrim) (改行)</head>(改行) を追加します。

    // ブロック要素の終わりに改行を追加する。
    $pattern[] = "/<\/(?:address|blockquote|c(?:aption|ol(?:group)?)|d(?:iv|[dlt])|" .
        "f(?:ieldset|orm)|h[1-6r]|l(?:egend|i)|noscript|ol|p(?:re)?|" .
        "t(?:able|body|foot|head|[dhr])|ul)>/i";
    $replace[] = "$0\n";
</tagname> を </tagname>(改行) へ変更します。
指定しているタグ名は、ブロック要素です。
http://ja.wikipedia.org/wiki/HTML%E8%A6%81%E7%B4%A0#.E3.83.96.E3.83.AD.E3.83.83.E3.82.AF.E3.83.AC.E3.83.99.E3.83.AB.E8.A6.81.E7.B4.A0

括弧が多いのは、マッチにかかる時間を減らすためです。
aa|ab|ac → aa か ab か ac を探す。
a[abc]   → a を探してから a, b, c を探す。

p(?:re)?      → p, pre を探す。
d(?:iv|[dlt]) → div, dd, dl, dt を探す。

    // brタグの終わりに改行を追加する。
    $pattern[] = "/<br((?:\s*)|(?:\s+[^>]+?))\/?" . ">/ei";
    $replace[] = "\"<br\" . preg_replace(\"/^\\s+|\\s+$/\", \" \", \" $1 \") . \"/>\\n\"";
<br で始まり
スペースが0文字以上、もしくはスペース1文字以上で > では無い文字が1文字以上あり
/ が0~1文字あり
> で終わる
場合の文字の後ろに改行を追加します。

[^ ... ] の場合 ... を含まないという意味になります。
1文字(1バイト)にしか効果が有りません。

文字列の場合(preg_xxx で利用されている pcreライブラリは perl 互換です)
http://www.din.or.jp/~ohzaki/regex.htm#Without

スペースが0文字以上 の場合
<br />

スペースが1文字以上で > では無い文字が1文字以上 の場合
<br class="className" />
例えば<br class="<className>" />となっていた場合、失敗します。(htmlとしてはおかしいですが " の数が合っていればブラウザは正常に解釈する場合が多いです)

    // 代替文字を元に戻す。
    $buf = preg_replace_callback(
        "/<\\x00,(\d+),\\x01>/",
        create_function(
            '$matches',
            '$tmp =& $GLOBALS["' . $tmpName . '"];' .
            'return $tmp[$matches[1] - 1];'
            ),
        $buf);
<\\x00,(\d+),\\x01> の \x00, \x01 の部分は実際にはブラウザで見ると文字化けを起こすような文字が指定されています。
<\\x00,(\d+),\\x01> を埋め込む前に// \n を除く制御文字を削除する。で 元々ある \x00, \x01 は削除されています。

<\\x00,(\d+),\\x01> の 数字の部分は配列の個数で入っているので
'return $tmp[$matches[1] - 1];' で -1 で番号を指定します。
$tmp[] = $matches[0]; のように空の配列に追加した場合、配列のキーは 0, 1, 2 ... の順番で追加されます。
配列が1個 → $tmp = array(0 => "xxx");
配列が2個 → $tmp = array(0 => "xxx", 1 => "yyy");

この処理(function callback($buffer))は、htmlのタグのオプションの値の <, > が &lt;, &gt; のようにエスケープされていることや
key="a   b   c" が key="a b c" のようにスペースが短くなっても問題ないことや
(例えば <a onclick="javascript: ... code ...">click</a> などで問題が出る場合がある)
<script> ... </script> の中に var a="</script>" のような指定が無いこと
などの前提が必要です。
よって html の処理には tidy などがお勧めです。

0 件のコメント: