Вопрос

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

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

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

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

РЕДАКТИРОВАТЬ:

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

Это было полезно?

Решение

Я вижу, что 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) | (? cancel-modifiers) , но я ' Мы заметили, что у обоих странные побочные эффекты модификатора. Например, в обоих случаях, если подвыражение имеет модификатор / u , оно не будет соответствовать (но если вы передадите 'u' в качестве второго аргумента новая функция, которая будет соответствовать просто отлично).

Другие советы

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

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

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

  • РЕДАКТИРОВАТЬ

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

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

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

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

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

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

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

    Спасибо!

    порноЛ, отсутствие век, Потрясающая работа!Большое большое спасибо.Я фактически сдался.

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

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

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

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

    Кстати, теперь вы должны осознать важность настоящих имен в 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 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);
    }
    

    ПС:Я сделал эту вики-сообщество редактируемым.Ты знаешь что это значит …!

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

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

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

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

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

    $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
    }
    
    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top