Вложенное регулярное выражение просмотра вперед и назад

У меня проблемы с вложенными '+' / '-' lookahead / lookbehind в регулярном выражении.

Допустим, я хочу изменить '*' в строке на '%', и предположим, что '\' экранирует следующий символ. (Превращение регулярного выражения в sql-подобную команду ^^).

Итак, строка

  • '*test*' следует заменить на '%test%',
  • '\\*test\\*' -> '\\%test\\%', но
  • '\*test\*' и '\\\*test\\\*' должны остаться прежними.

Я пытался:

(?<!\\)(?=\\\\)*\*      but this doesn't work
(?<!\\)((?=\\\\)*\*)    ...
(?<!\\(?=\\\\)*)\*      ...
(?=(?<!\\)(?=\\\\)*)\*  ...

Какое правильное регулярное выражение будет соответствовать символам '*' в приведенных выше примерах?

В чем разница между (?<!\\(?=\\\\)*)\* и (?=(?<!\\)(?=\\\\)*)\* или, если они по сути неверны, разница между регулярными выражениями с такой визуальной конструкцией?


person bliof    schedule 23.10.2011    source источник
comment
На каком языке ты говоришь? И действительно ли вы ожидаете, что \*test\* останется прежним и не превратится в *test*?   -  person Gumbo    schedule 23.10.2011


Ответы (5)


Чтобы найти неэкранированный символ, вы должны искать символ, которому предшествует четное количество (или ноль) escape-символов. Это относительно просто.

(?<=(?<!\\)(?:\\\\)*)\*        # this is explained in Tim Pietzcker' answer

К сожалению, многие механизмы регулярных выражений не поддерживают просмотр назад с переменной длиной, поэтому мы должны заменить его на просмотр вперед:

(?=(?<!\\)(?:\\\\)*\*)(\\*)\*  # also look at ridgerunner's improved version

Замените это содержанием группы 1 и знаком %.

Объяснение

(?=           # start look-ahead
  (?<!\\)     #   a position not preceded by a backslash (via look-behind)
  (?:\\\\)*   #   an even number of backslashes (don't capture them)
  \*          #   a star
)             # end look-ahead. If found,
(             # start group 1
  \\*         #   match any number of backslashes in front of the star
)             # end group 1
\*            # match the star itself

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

person Tomalak    schedule 23.10.2011
comment
Хорошее замечание (также @ridgerunner) о просмотре назад с неопределенной длиной. Не все используют механизмы регулярных выражений .NET или JGSoft. - person Tim Pietzcker; 23.10.2011

Хорошо, поскольку Тим решил не обновлять свое регулярное выражение с моими предложенными модами (а ответ Томалака не так оптимизирован), вот мое рекомендуемое решение:

Заменить: ((?<!\\)(?:\\\\)*)\* на $1%

Вот он в виде закомментированного фрагмента PHP:

// Replace all non-escaped asterisks with "%".
$re = '%             # Match non-escaped asterisks.
    (                # $1: Any/all preceding escaped backslashes.
      (?<!\\\\)      # At a position not preceded by a backslash,
      (?:\\\\\\\\)*  # Match zero or more escaped backslashes.
    )                # End $1: Any preceding escaped backslashes.
    \*               # Unescaped literal asterisk.
    %x';
$text = preg_replace($re, '$1%', $text);

Приложение: решение для JavaScript без поиска

Вышеупомянутое решение требует ретроспективного просмотра, поэтому оно не будет работать в JavaScript. Следующее решение JavaScript не использует поиск назад:

text = text.replace(/(\\[\S\s])|\*/g,
    function(m0, m1) {
        return m1 ? m1 : '%';
    });

Это решение заменяет каждый экземпляр обратной косой черты на себя, а каждый экземпляр * звездочки - знаком %.

Изменить 2011-10-24: исправлена ​​версия Javascript для правильной обработки таких случаев, как: **text**. (Спасибо Алану Муру за указание на ошибку в предыдущей версии.)

person ridgerunner    schedule 23.10.2011
comment
+1 для упрощения регулярного выражения @Tim, но ваша версия, безопасная для JavaScript, не работает на **test**. : - / Я не думаю, что это можно сделать за одну replace операцию JS. - person Alan Moore; 24.10.2011
comment
@ Алан Мур - совершенно верно. Спасибо за зоркий глаз! Однако это можно сделать с помощью одного replace(), использующего функцию обратного вызова. Смотрите последнее воплощение. - person ridgerunner; 25.10.2011

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

s/\G([^*\\]*(?:\\.[^*\\]*)*)\*/$1%/g;

Основная часть регулярного выражения [^*\\]*(?:\\.[^*\\]*)* является примером идиомы Фридла о «развернутом цикле». Он использует как можно больше отдельных символов, кроме звездочки или обратной косой черты, или пар символов, состоящих из обратной косой черты, за которой следует что-либо. Это позволяет избежать использования неэкранированных звездочек, независимо от того, сколько экранированных обратных косых черт (или других символов) им предшествует.

\G привязывает каждое совпадение к позиции, где закончилось предыдущее совпадение, или к началу ввода, если это первая попытка совпадения. Это не позволяет механизму регулярных выражений просто пропускать экранированные обратные косые черты и в любом случае сопоставлять неэкранированные звездочки. Таким образом, каждая итерация /g управляемого совпадения потребляет все до следующей неэкранированной звездочки, захватывая все, кроме звездочки в группе №1. Затем он снова подключается, и * заменяется на %.

Я думаю, что это, по крайней мере, так же читабельно, как и подходы поиска, и его легче понять. Для этого действительно требуется поддержка \G, поэтому он не будет работать в JavaScript или Python, но он отлично работает в Perl.

person Alan Moore    schedule 23.10.2011

Таким образом, вы, по сути, хотите сопоставить *, только если ему предшествует четное количество обратных косых черт (или, другими словами, если оно не экранировано)? Тогда вам вообще не нужно смотреть вперед, потому что вы только оглядываетесь назад, не так ли?

Ищи

(?<=(?<!\\)(?:\\\\)*)\*

и заменить на %.

Объяснение:

(?<=       # Assert that it's possible to match before the current position...
 (?<!\\)   # (unless there are more backslashes before that)
 (?:\\\\)* # an even number of backslashes
)          # End of lookbehind
\*         # Then match an asterisk
person Tim Pietzcker    schedule 23.10.2011
comment
Близко, но (как вы знаете) очень немногие механизмы регулярных выражений поддерживают просмотр назад переменной длины. Измените ретроспективный просмотр на группу захвата $1 и строку замены на: $1%, и тогда он должен работать для большинства (но все же не js). - person ridgerunner; 23.10.2011
comment
Хм, правда. Будем надеяться, что он использует .NET :) - person Tim Pietzcker; 23.10.2011
comment
Теперь, поскольку bliof указал, что он использует Perl, я бы обычно отказался от своего ответа, поскольку он не работает в Perl из-за ограничений, упомянутых выше. Но поскольку другие ответы относятся к этому, я оставлю его здесь. - person Tim Pietzcker; 24.10.2011

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

  • Обратные косые черты экранируют любой следующий за ними символ, а не только другие обратные косые черты. Таким образом, (\\.)* съест целую цепочку экранированных символов, независимо от того, являются они обратными косыми чертами или нет. Вам не нужно беспокоиться о четных или нечетных косых чертах; просто проверьте наличие одного \ в начале или в конце цепочки (решение JavaScript ridgerunner действительно использует это в своих интересах).

  • Поисковые запросы - не единственный способ убедиться, что вы начинаете с первой обратной косой черты в цепочке. Вы можете просто найти символ без обратной косой черты (или начало строки).

В результате получается короткий простой шаблон, который не требует поисков и обратных вызовов, и он короче всего, что я видел до сих пор.

/(?!<\\)(\\.)*\*/g

И строка замены:

"$1%"

Это работает в .NET, что позволяет просматривать назад и должен работать у вас на Perl. Это можно сделать в JavaScript, но без просмотра назад или привязки \G я не вижу способа сделать это в однострочном режиме. Обратный вызов Ridgerunner должен работать, как и цикл:

var regx = /(^|[^\\])(\\.)*\*/g;
while (input.match(regx)) {
    input = input.replace(regx, '$1$2%');
}

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

person Justin Morgan    schedule 16.10.2012
comment
К сожалению, с этим регулярным выражением есть проблема. Когда вы попытаетесь сопоставить что-то вроде *\\*, это не удастся (вы получите первый запуск с ^\*, но второй перейдет к [^\\], и \* не будет ничего для сопоставления) - person bliof; 17.10.2012
comment
@bliof - Ты прав. И я не могу придумать, как решить эту проблему в регулярном выражении в стиле JS без обратного вызова или цикла. В других вариантах этого, когда вы делаете что-то другое, кроме замены * (например, подсчет экранированных обратных косых черт или что-то в этом роде), это все равно будет работать. Я не думаю, что однострочник может сделать это в JavaScript, но я буду редактировать что-нибудь, что будет. Спасибо, что поправили меня, у меня было сильное чувство, что это слишком хорошо, чтобы быть правдой. - person Justin Morgan; 17.10.2012