4

これは、これまでパーサー/レクサーを作成したことがない人からの初歩的な質問です。

PHP で CSS のトークナイザー/パーサーを作成しています (「OMG、なぜ PHP で?」と繰り返さないでください)。構文は W3C によってここ (CSS2.1)ここ (CSS3、ドラフト)にきちんと書き留められています。

これは 21 の可能なトークンのリストであり、すべて (2 つを除く) を静的文字列として表すことはできません。

私の現在のアプローチは、21 個のパターンを含む配列を何度もループしてif (preg_match())、ソース文字列の一致を一致ごとに減らしていくというものです。原則として、これは非常にうまく機能します。ただし、1000 行の CSS 文字列の場合、これには 2 ~ 8 秒かかります。これは、私のプロジェクトには多すぎます。

今、私は他のパーサーがどのように CSS をトークン化して数秒で解析するのか頭を悩ませています。OK、C常に PHP よりも高速ですが、それにもかかわらず、明らかなD'Oh!はありますか? 私が陥ったのですか?

残りの文字列の最初の文字として「@」、「#」、または「"」をチェックし、関連する正規表現のみを適用するなど、いくつかの最適化を行いましたが、これによりパフォーマンスが大幅に向上することはありませんでした。

これまでの私のコード(スニペット):

$TOKENS = array(
  'IDENT' => '...regexp...',
  'ATKEYWORD' => '@...regexp...',
  'String' => '"...regexp..."|\'...regexp...\'',
  //...
);

$string = '...CSS source string...';
$stream = array();

// we reduce $string token by token
while ($string != '') {
    $string = ltrim($string, " \t\r\n\f"); // unconsumed whitespace at the
        // start is insignificant but doing a trim reduces exec time by 25%
    $matches = array();
    // loop through all possible tokens
    foreach ($TOKENS as $t => $p) {
        // The '&' is used as delimiter, because it isn't used anywhere in
        // the token regexps
        if (preg_match('&^'.$p.'&Su', $string, $matches)) {
            $stream[] = array($t, $matches[0]);
            $string = substr($string, strlen($matches[0]));
            // Yay! We found one that matches!
            continue 2;
        }
    }
    // if we come here, we have a syntax error and handle it somehow
}

// result: an array $stream consisting of arrays with
// 0 => type of token
// 1 => token content
4

5 に答える 5

3

レクサー ジェネレーターを使用します。

于 2010-04-09T20:08:22.717 に答える
0

私が最初にすることは、を取り除くことpreg_match()です。などの基本的な文字列関数strpos()ははるかに高速ですが、それは必要ないと思います。で文字列の先頭にある特定のトークンを探し、preg_match()その文字列の先頭の長さを部分文字列として単純に取っているようです。substr()次のように、単純な代わりにこれを簡単に実現できます。

foreach ($TOKENS as $t => $p)
{
    $front = substr($string,0,strlen($p));
    $len = strlen($p);  //this could be pre-stored in $TOKENS
    if ($front == $p) {
        $stream[] = array($t, $string);
        $string = substr($string, $len);
        // Yay! We found one that matches!
        continue 2;
    }
}

すべてのトークンの長さを事前に計算して$TOKENS配列に格納することで、さらに最適化できます。これによりstrlen()、常に呼び出す必要がなくなります。$TOKENS長さによってグループに分類した場合、トークンの長さごとに分析されている現在の文字列を 1 回だけ取得し、その長さのすべてのトークンを実行してから次に進むことができるため、substr()呼び出しの数をさらに減らすこともできます。substr($string)トークンの次のグループ。

于 2010-04-09T19:30:35.377 に答える
0

これは古い投稿ですが、まだこれに 2 セントを寄付しています。問題の元のコードを大幅に遅くするものの1つは、次の行です。

$string = substr($string, strlen($matches[0]));

文字列全体を処理する代わりに、考えられるすべての正規表現に十分な、その一部 (たとえば 50 文字) を取得します。次に、同じコード行を適用します。この文字列が事前に設定された長さを下回ると、さらにデータをロードします。

于 2013-09-15T02:03:35.497 に答える
0

(おそらく) より高速な (ただしメモリ フレンドリーではない) アプローチは、ストリーム全体を一度にトークン化することです。次のように、トークンごとに代替の 1 つの大きな正規表現を使用します。

 preg_match_all('/
       (...string...)
       |
       (@ident)
       |
       (#ident)
       ...etc
   /x', $stream, $tokens);

 foreach($tokens as $token)...parse
于 2010-04-09T21:42:56.587 に答える
0

正規表現を使用せず、文字ごとにスキャンします。

$tokens = array();
$string = "...code...";
$length = strlen($string);
$i = 0;
while ($i < $length) {
  $buf = '';
  $char = $string[$i];
  if ($char <= ord('Z') && $char >= ord('A') || $char >= ord('a') && $char <= ord('z') || $char == ord('_') || $char == ord('-')) {
    while ($char <= ord('Z') && $char >= ord('A') || $char >= ord('a') && $char <= ord('z') || $char == ord('_') || $char == ord('-')) {
      // identifier
      $buf .= $char;
      $char = $string[$i]; $i ++;
    }
    $tokens[] = array('IDENT', $buf);
  } else if (......) {
    // ......
  }
}

ただし、それではコードが保守できなくなるため、パーサー ジェネレーターの方が適しています。

于 2010-04-10T22:40:41.397 に答える