10

正規表現を含む次の 2 つの文字列があるとします。それらを合体させるにはどうすればよいですか?より具体的には、2 つの式を代替として使用したいと考えています。

$a = '# /[a-z] #i';
$b = '/ Moo /x';
$c = preg_magic_coalesce('|', $a, $b);
// Desired result should be equivalent to:
// '/ \/[a-zA-Z] |Moo/'

もちろん、これを文字列操作として行うのは実用的ではありません。これには、式の解析、構文ツリーの構築、ツリーの合体、およびツリーに相当する別の正規表現の出力が含まれるためです。この最後のステップがなくても、私は完全に満足しています。残念ながら、PHP には RegExp クラスがありません (またはありますか?)。

これを達成する方法はありますか?ちなみに、他に方法を提供する言語はありますか?これはごく普通のシナリオではないでしょうか。ないと思います。:-(

あるいは、 2 つの式のどちらかが一致するかどうか、どちらが先に一致するか (同じ位置で一致する場合、どちらの一致が長いか) を効率的にチェックする方法はありますか? これが私が現在行っていることです。残念ながら、私はこれを長い文字列に対して行います。非常に頻繁に、2 つ以上のパターンを作成します。結果は遅いです(はい、これは間違いなくボトルネックです)。

編集:

もっと具体的に書くべきでした - すみません。$a$b変数であり、その内容は私の制御外です! それ以外の場合は、それらを手動で合体させます。したがって、使用されている区切り文字や正規表現修飾子について、私は何の推測もできません。たとえば、最初の式ではi修飾子 (大文字と小文字を区別しない) を使用し、2 番目の式ではx(拡張構文) を使用していることに注意してください。したがって、2 番目の式は大文字と小文字を区別せず、最初の式は拡張構文を使用しないため、2 つを連結することはできません (そして、その中の空白は重要です!

4

6 に答える 6

3

porneL が実際にこれについて説明しているように見えますが、これでほとんどの問題を処理できます。前の部分式で設定された修飾子 (他の回答が見逃していたもの) をキャンセルし、各部分式で指定されているように修飾子を設定します。また、スラッシュ以外の区切り文字も処理します (ここで使用できる文字の仕様が見つからなかったので、 を使用.しました。さらに絞り込むことができます)。

弱点の 1 つは、式内の後方参照を処理できないことです。それに関する私の最大の懸念は、後方参照自体の制限です。それは、読者/質問者への演習として残します。

// Pass as many expressions as you'd like
function preg_magic_coalesce() {
    $active_modifiers = array();

    $expression = '/(?:';
    $sub_expressions = array();
    foreach(func_get_args() as $arg) {
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1([eimsuxADJSUX]+)$/', $arg, $matches)) {
            $modifiers = preg_split('//', $matches[3]);
            if($modifiers[0] == '') {
                array_shift($modifiers);
            }
            if($modifiers[(count($modifiers) - 1)] == '') {
                array_pop($modifiers);
            }

            $cancel_modifiers = $active_modifiers;
            foreach($cancel_modifiers as $key => $modifier) {
                if(in_array($modifier, $modifiers)) {
                    unset($cancel_modifiers[$key]);
                }
            }
            $active_modifiers = $modifiers;
        } elseif(preg_match('/(.)(.*)\1$/', $arg)) {
            $cancel_modifiers = $active_modifiers;
            $active_modifiers = array();
        }

        // If expression has modifiers, include them in sub-expression
        $sub_modifier = '(?';
        $sub_modifier .= implode('', $active_modifiers);

        // Cancel modifiers from preceding sub-expression
        if(count($cancel_modifiers) > 0) {
            $sub_modifier .= '-' . implode('-', $cancel_modifiers);
        }

        $sub_modifier .= ')';

        $sub_expression = preg_replace('/^(.)(.*)\1[eimsuxADJSUX]*$/', $sub_modifier . '$2', $arg);

        // Properly escape slashes
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/';
    return $expression;
}

編集:私はこれを書き直しました(私はOCDなので)、最終的には次のようになりました:

function preg_magic_coalesce($expressions = array(), $global_modifier = '') {
    if(!preg_match('/^((?:-?[eimsuxADJSUX])+)$/', $global_modifier)) {
        $global_modifier = '';
    }

    $expression = '/(?:';
    $sub_expressions = array();
    foreach($expressions as $sub_expression) {
        $active_modifiers = array();
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1((?:-?[eimsuxADJSUX])+)$/', $sub_expression, $matches)) {
            $active_modifiers = preg_split('/(-?[eimsuxADJSUX])/',
                $matches[3], -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
        }

        // If expression has modifiers, include them in sub-expression
        if(count($active_modifiers) > 0) {
            $replacement = '(?';
            $replacement .= implode('', $active_modifiers);
            $replacement .= ':$2)';
        } else {
            $replacement = '$2';
        }

        $sub_expression = preg_replace('/^(.)(.*)\1(?:(?:-?[eimsuxADJSUX])*)$/',
            $replacement, $sub_expression);

        // Properly escape slashes if another delimiter was used
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/' . $global_modifier;
    return $expression;
}

現在では(?modifiers:sub-expression)むしろを使用し(?modifiers)sub-expression|(?cancel-modifiers)sub-expressionていますが、どちらにも奇妙な修飾子の副作用があることに気付きました。たとえば、どちらの場合も、部分式に/u修飾子がある場合、一致しません (ただし'u'、新しい関数の 2 番目の引数として渡すと、うまく一致します)。

于 2008-10-29T03:06:13.553 に答える
3

編集

コードを書き直しました!現在、次のようにリストされている変更が含まれています。さらに、エラーを探すために大規模なテストを行いました (数が多すぎるため、ここには掲載しません)。これまでのところ、私は何も見つけていません。

  • 関数は 2 つの部分に分割されましpreg_splitた。正規表現を受け取り、そのままの式 (区切り記号なし) と修飾子の配列を含む配列を返す別の関数があります。これは便利かもしれません (実際には既に使用されています。これが、この変更を行った理由です)。

  • コードが後方参照を正しく処理するようになりました。結局のところ、これは私の目的のために必要でした。追加するのは難しくありませんでした.後方参照をキャプチャするために使用される正規表現は奇妙に見えます. . ところで、奇数の一致をチェックする私の方法よりも良い方法を知っている人はいますか? 負の後読みは、正規表現ではなく固定長の文字列しか受け付けないため、ここでは機能しません。ただし、前のバックスラッシュが実際にエスケープされているかどうかをテストするには、ここで正規表現が必要です。

    さらに、PHP が匿名使用をキャッシュするのにどれほど優れているかはわかりませんcreate_function。パフォーマンスに関しては、これは最善の解決策ではないかもしれませんが、十分なようです。

  • 健全性チェックのバグを修正しました。

  • 私のテストでは不要であることが示されたので、廃止された修飾子のキャンセルを削除しました。

ところで、このコードは、私が PHP で取り組んでいるさまざまな言語の構文ハイライターのコア コンポーネントの 1 つです。他の場所にリストされている代替手段に満足できないためです。

ありがとう!

porneL ,まぶたのない, 素晴らしい作品! 大変感謝します。実は諦めていました。

私はあなたのソリューションに基づいて構築したので、ここで共有したいと思います。これは私の場合は関係ないので、後方参照の再番号付けは実装しませんでした (… と思います)。たぶん、これは後で必要になるでしょう。

いくつか質問があります...</h2>

@eyelidlessness : _古いモディファイヤをキャンセルする必要性を感じるのはなぜですか? 私が見る限り、修飾子はとにかくローカルでのみ適用されるため、これは必要ありません。あ、そうそう、もう一つ。区切り文字のエスケープは複雑すぎるようです。これが必要だと思う理由を説明してください。私のバージョンも同様に機能するはずですが、非常に間違っている可能性があります。

また、ニーズに合わせて関数のシグネチャを変更しました。また、私のバージョンはより一般的に役立つと思います。繰り返しますが、私は間違っているかもしれません。

ところで、SO での実名の重要性に気付くはずです。;-) コードの真の功績を認めることはできません。:-/

コード

とにかく、これまでの結果を共有したいと思います。他の誰もそのようなものを必要としていないとは信じられないからです. コード非常にうまく機能しているようです。ただし、広範なテストはまだ行われていません。 コメントしてください!

そして、これ以上苦労することなく…</p>

/**
 * Merges several regular expressions into one, using the indicated 'glue'.
 *
 * This function takes care of individual modifiers so it's safe to use
 * <em>different</em> modifiers on the individual expressions. The order of
 * sub-matches is preserved as well. Numbered back-references are adapted to
 * the new overall sub-match count. This means that it's safe to use numbered
 * back-refences in the individual expressions!
 * If {@link $names} is given, the individual expressions are captured in
 * named sub-matches using the contents of that array as names.
 * Matching pair-delimiters (e.g. <code>"{…}"</code>) are currently
 * <strong>not</strong> supported.
 *
 * The function assumes that all regular expressions are well-formed.
 * Behaviour is undefined if they aren't.
 *
 * This function was created after a {@link https://stackoverflow.com/questions/244959/
 * StackOverflow discussion}. Much of it was written or thought of by
 * “porneL” and “eyelidlessness”. Many thanks to both of them.
 *
 * @param string $glue  A string to insert between the individual expressions.
 *      This should usually be either the empty string, indicating
 *      concatenation, or the pipe (<code>|</code>), indicating alternation.
 *      Notice that this string might have to be escaped since it is treated
 *      like a normal character in a regular expression (i.e. <code>/</code>)
 *      will end the expression and result in an invalid output.
 * @param array $expressions    The expressions to merge. The expressions may
 *      have arbitrary different delimiters and modifiers.
 * @param array $names  Optional. This is either an empty array or an array of
 *      strings of the same length as {@link $expressions}. In that case,
 *      the strings of this array are used to create named sub-matches for the
 *      expressions.
 * @return string An string representing a regular expression equivalent to the
 *      merged expressions. Returns <code>FALSE</code> if an error occurred.
 */
function preg_merge($glue, array $expressions, array $names = array()) {
    // … then, a miracle occurs.

    // Sanity check …

    $use_names = ($names !== null and count($names) !== 0);

    if (
        $use_names and count($names) !== count($expressions) or
        !is_string($glue)
    )
        return false;

    $result = array();
    // For keeping track of the names for sub-matches.
    $names_count = 0;
    // For keeping track of *all* captures to re-adjust backreferences.
    $capture_count = 0;

    foreach ($expressions as $expression) {
        if ($use_names)
            $name = str_replace(' ', '_', $names[$names_count++]);

        // Get delimiters and modifiers:

        $stripped = preg_strip($expression);

        if ($stripped === false)
            return false;

        list($sub_expr, $modifiers) = $stripped;

        // Re-adjust backreferences:

        // We assume that the expression is correct and therefore don't check
        // for matching parentheses.

        $number_of_captures = preg_match_all('/\([^?]|\(\?[^:]/', $sub_expr, $_);

        if ($number_of_captures === false)
            return false;

        if ($number_of_captures > 0) {
            // NB: This looks NP-hard. Consider replacing.
            $backref_expr = '/
                (                # Only match when not escaped:
                    [^\\\\]      # guarantee an even number of backslashes
                    (\\\\*?)\\2  # (twice n, preceded by something else).
                )
                \\\\ (\d)        # Backslash followed by a digit.
            /x';
            $sub_expr = preg_replace_callback(
                $backref_expr,
                create_function(
                    '$m',
                    'return $m[1] . "\\\\" . ((int)$m[3] + ' . $capture_count . ');'
                ),
                $sub_expr
            );
            $capture_count += $number_of_captures;
        }

        // Last, construct the new sub-match:

        $modifiers = implode('', $modifiers);
        $sub_modifiers = "(?$modifiers)";
        if ($sub_modifiers === '(?)')
            $sub_modifiers = '';

        $sub_name = $use_names ? "?<$name>" : '?:';
        $new_expr = "($sub_name$sub_modifiers$sub_expr)";
        $result[] = $new_expr;
    }

    return '/' . implode($glue, $result) . '/';
}

/**
 * Strips a regular expression string off its delimiters and modifiers.
 * Additionally, normalize the delimiters (i.e. reformat the pattern so that
 * it could have used '/' as delimiter).
 *
 * @param string $expression The regular expression string to strip.
 * @return array An array whose first entry is the expression itself, the
 *      second an array of delimiters. If the argument is not a valid regular
 *      expression, returns <code>FALSE</code>.
 *
 */
function preg_strip($expression) {
    if (preg_match('/^(.)(.*)\\1([imsxeADSUXJu]*)$/s', $expression, $matches) !== 1)
        return false;

    $delim = $matches[1];
    $sub_expr = $matches[2];
    if ($delim !== '/') {
        // Replace occurrences by the escaped delimiter by its unescaped
        // version and escape new delimiter.
        $sub_expr = str_replace("\\$delim", $delim, $sub_expr);
        $sub_expr = str_replace('/', '\\/', $sub_expr);
    }
    $modifiers = $matches[3] === '' ? array() : str_split(trim($matches[3]));

    return array($sub_expr, $modifiers);
}

PS: この投稿コミュニティ wiki を編集可能にしました。これが何を意味するか分かります…!

于 2008-10-29T20:48:46.450 に答える
3
  1. それぞれから区切り文字とフラグを取り除きます。この正規表現はそれを行う必要があります:

    /^(.)(.*)\1([imsxeADSUXJu]*)$/
    
  2. 式を結合します。フラグを挿入するには、非キャプチャ括弧が必要です。

    "(?$flags1:$regexp1)|(?$flags2:$regexp2)"
    
  3. 後方参照がある場合は、キャプチャ括弧をカウントし、それに応じて後方参照を更新します (たとえば、適切に結合された/(.)x\1//(.)y\1/is /(.)x\1|(.)y\2/)。

于 2008-10-29T00:02:40.117 に答える
1

どの言語でもそのように正規表現をまとめることは不可能だと確信しています-それらには互換性のない修飾子がある可能性があります。

おそらく、それらを配列に入れてループするか、手動で結合するだけです。

編集:編集で説明されているように一度に1つずつ実行している場合、部分文字列で2番目のものを実行できる可能性があります(開始から最も古い一致まで)。それは物事を助けるかもしれません。

于 2008-10-28T21:45:33.027 に答える
0

次のような別の方法でも実行できます。

$a = '# /[a-z] #i';
$b = '/ Moo /x';

$a_matched = preg_match($a, $text, $a_matches);
$b_matched = preg_match($b, $text, $b_matches);

if ($a_matched && $b_matched) {
    $a_pos = strpos($text, $a_matches[1]);
    $b_pos = strpos($text, $b_matches[1]);

    if ($a_pos == $b_pos) {
        if (strlen($a_matches[1]) == strlen($b_matches[1])) {
            // $a and $b matched the exact same string
        } else if (strlen($a_matches[1]) > strlen($b_matches[1])) {
            // $a and $b started matching at the same spot but $a is longer
        } else {
            // $a and $b started matching at the same spot but $b is longer
        }
    } else if ($a_pos < $b_pos) {
        // $a matched first
    } else {
        // $b matched first
    }
} else if ($a_matched) {
    // $a matched, $b didn't
} else if ($b_matched) {
    // $b matched, $a didn't
} else {
    // neither one matched
}
于 2008-10-28T22:21:22.753 に答える
0
function preg_magic_coalasce($split, $re1, $re2) {
  $re1 = rtrim($re1, "\/#is");
  $re2 = ltrim($re2, "\/#");
  return $re1.$split.$re2;
}
于 2008-10-28T21:47:19.547 に答える