Объединение регулярных выражений в PHP

Предположим, что у меня есть две строки, содержащие регулярные выражения. Как я могу объединить их? В частности, я хочу, чтобы эти два выражения были альтернативными.

$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 (или нет?).

Есть ли способ достичь этого? Кстати, предлагает ли какой-либо другой язык способ? Разве это не нормальный сценарий? Думаю нет.: - (

Альтернативно, есть ли способ проверить эффективно, если одно из двух выражений совпадает, и какое из них соответствует ранее (и если они совпадают в той же позиции, что матч длиннее)? Это то, что я делаю в данный момент. К сожалению, я делаю это по длинным строкам, очень часто, для более чем двух моделей. Результат медленный (и да, это определенно узкое место).

EDIT:

Я должен был быть более конкретным - извините. $a и $b являются переменными, их содержимое находится вне моего контроля! В противном случае я бы просто объединил их вручную. Поэтому я не могу делать никаких предположений о используемых разделителях или модификаторах регулярных выражений. Обратите внимание, например, что мое первое выражение использует модификатор i (игнорировать оболочку), а во втором - x (расширенный синтаксис). Поэтому я не могу просто конкатенировать два, потому что второе выражение не игнорирует обсадную колонну, а первое не использует расширенный синтаксис (и любые пробелы в нем значительны!

Ответ 1

Я вижу, что porneL на самом деле описал кучу этого, но это устраняет большую часть проблемы. Он отменяет модификаторы, установленные в предыдущих подвыражениях (которые пропустили другой ответ) и устанавливает модификаторы, как указано в каждом подвыражении. Он также обрабатывает разделители без слэша (я не мог найти спецификацию того, какие символы разрешены здесь, поэтому я использовал ., вы можете еще больше сузить).

Одна слабость заключается в том, что она не обрабатывает обратные ссылки в выражениях. Моя самая большая забота об этом - это ограничения самих обратных ссылок. Я оставлю это как упражнение для читателя/эксперта.

// 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

  • Разделите разделители и флаги от каждого. Это регулярное выражение должно сделать это:

    /^(.)(.*)\1([imsxeADSUXJu]*)$/
    
  • Объединяйте выражения вместе. Вам понадобится не захватывающая скобка для ввода флагов:

    "(?$flags1:$regexp1)|(?$flags2:$regexp2)"
    
  • Если есть обратные ссылки, посчитайте соответственно скопируйте скобки и обновите обратные ссылки (например, правильно присоединились /(.)x\1/ и /(.)y\1/ is /(.)x\1|(.)y\2/).

Ответ 3

ИЗМЕНИТЬ

Ive переписал код! Теперь он содержит изменения, перечисленные ниже. Кроме того, я провел обширные тесты (которые я не буду публиковать здесь, потому что их слишком много), чтобы искать ошибки. До сих пор я не нашел никого.

  • Теперь функция разделяется на две части: Theres представляет собой отдельную функцию preg_split, которая принимает регулярное выражение и возвращает массив, содержащий голое выражение (без разделителей) и массив модификаторов. Это может пригодиться (это уже на самом деле, поэтому я сделал это изменение).

  • Теперь код корректно обрабатывает обратные ссылки. Это было необходимо для моей цели в конце концов. Это было трудно добавить, регулярное выражение, используемое для захвата обратных ссылок, просто выглядит странно (и может быть на самом деле крайне неэффективным, оно выглядит NP-сложно для меня, но это только интуиция и применяется только в странных случаях). Кстати, кто-нибудь знает лучший способ проверить нечетное количество матчей, чем мой путь? Отрицательные lookbehind не будут работать здесь, потому что они принимают только строки фиксированной длины вместо регулярных выражений. Тем не менее, мне нужно регулярное выражение здесь, чтобы проверить, действительно ли предыдущая обратная косая черта сбежала сама.

    Кроме того, я не знаю, насколько хорош PHP при кешировании анонимного create_function. По производительности, это может быть не лучшее решение, но оно кажется достаточно хорошим.

  • Я установил ошибку в проверке работоспособности.

  • Ive удалил отмену устаревших модификаторов, так как мои тесты показывают, что это необязательно.

Кстати, этот код является одним из основных компонентов синтаксического маркера для разных языков, на котором Im работает в PHP, так как Im не удовлетворен перечисленными альтернативами в другом месте.

Спасибо!

porneL, eyelidlessness, удивительная работа! Большое большое спасибо. Я действительно отказался.

Я построил свое решение, и я хотел бы поделиться им здесь. Я не реализовал повторное нумерацию обратных ссылок, так как это не имеет значения в моем случае (я думаю...). Возможно, это будет необходимо позже, однако.

Некоторые вопросы...

Одна вещь, @eyelidlessness: Почему вы чувствуете необходимость отменить старые модификаторы? Насколько я понимаю, это необязательно, так как модификаторы применяются только локально. Ах да, еще одна вещь. Ваше преодоление разделителя кажется слишком сложным. Подумайте, почему вы думаете, что это необходимо? Я считаю, что моя версия должна работать, но я могу быть очень не прав.

Кроме того, я изменил подпись вашей функции в соответствии с моими потребностями. Я также считаю, что моя версия в целом полезна. Опять же, я могу ошибаться.

Кстати, вы должны теперь осознать важность настоящих имен на SO.;-) Я не могу дать вам настоящий кредит в коде.: -/

Код

В любом случае, я хотел бы поделиться своим результатом до сих пор, потому что я не могу поверить, что кому-то еще не нужно что-то подобное. Код, похоже, работает очень хорошо. Обширные тесты еще предстоит сделать. Прокомментируйте!

И без дальнейших церемоний...

/**
 * Merges several regular expressions into one, using the indicated 'glue'.
 *
 * This function takes care of individual modifiers so it 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 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: Я создал эту публикацию сообщества публикации. Вы знаете, что это значит...!

Ответ 4

Я уверен, что невозможно просто поместить регулярные выражения вместе на любом языке - они могут иметь несовместимые модификаторы.

Я бы просто поместил их в массив и пропустил через них или объединил их вручную.

Изменить: если вы делаете их по одному, как описано в вашем редактировании, возможно, вы сможете запустить второй в подстроке (от начала до самого раннего совпадения). Это может помочь.

Ответ 5

function preg_magic_coalasce($split, $re1, $re2) {
  $re1 = rtrim($re1, "\/#is");
  $re2 = ltrim($re2, "\/#");
  return $re1.$split.$re2;
}

Ответ 6

Вы можете сделать это альтернативным способом:

$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
}