Объединение регулярных выражений в PHP - PullRequest
10 голосов
/ 29 октября 2008

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

$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 (расширенный синтаксис). Поэтому я не могу просто объединить два, потому что второе выражение не игнорирует регистр, а первое не использует расширенный синтаксис (и любые пробелы в нем значительны!

Ответы [ 6 ]

3 голосов
/ 29 октября 2008

EDIT

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

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

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

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

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

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

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

Спасибо!

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

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

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

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

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

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

Код

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

И без лишних слов ...

/**
 * 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 /191163/obedinenie-regulyarnyh-vyrazhenii-v-php
 * 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: я сделал эту публикацию вики-сообщества доступной для редактирования. Вы знаете, что это значит ...!

3 голосов
/ 29 октября 2008

Я вижу, что 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' в качестве второго аргумента новой функции, это будет соответствовать просто отлично).

3 голосов
/ 29 октября 2008
  1. Снимите разделители и флаги с каждого. Это регулярное выражение должно сделать это:

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

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

1 голос
/ 29 октября 2008

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

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

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

0 голосов
/ 29 октября 2008

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

$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
}
0 голосов
/ 29 октября 2008
function preg_magic_coalasce($split, $re1, $re2) {
  $re1 = rtrim($re1, "\/#is");
  $re2 = ltrim($re2, "\/#");
  return $re1.$split.$re2;
}
...