Посмотрите сзади: вся ярость в regex?


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

(?<=<td><a href="/xxx.html?n=[0-9]{0, 5}">).*(?=</a><span

И это из другого вопроса:

$url = "www.example.com/id/1234";
preg_match("/d+(?<=id/[d])/",$url,$matches);

Когда действительно лучше использовать позитивный взгляд вокруг?Можете ли вы привести несколько примеров?

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

Спасибо за все ответы. В дополнение к приведенным ниже, я рекомендую проверитьотличный ответ М. Бюттнера здесь.

8 8

8 ответов:

  1. Вы можете захватить перекрывающиеся совпадения, и вы можете найти совпадения, которые могли бы лежать в lookarounds других совпадений.
  2. Вы можете выразить сложные логические утверждения о вашем совпадении (потому что многие движки позволяют использовать несколько утверждений lookbehind/lookahead, которые все должны совпадать, чтобы совпадение было успешным).
  3. Lookaround-это естественный способ выражения общего ограничения "соответствует X, если за ним следует / предшествует Y". Это (возможно) менее естественно, чтобы добавить дополнительные "совпадающие" детали, которые должны быть выброшены постобработкой.
Отрицательные утверждения lookaround, конечно, еще более полезны. В сочетании с #2, они могут позволить вам сделать некоторые довольно волшебные трюки, которые могут быть даже трудно выразить в обычной логике программы.

Примеры, по многочисленным просьбам:

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

    ATG(?=((?:...)*(?:TAG|TAA|TGA)))
    

    Это простое регулярное выражение ищет стартовый кодон ATG, за которым следует некоторое количество кодонов, а затем стоп-кодон. Он извлекает все, что выглядит как ген (без начального кодона), и правильно выводит гены, даже если они перекрываются.

  • Сопоставление нулевой ширины: предположим, вы хотите найти каждый tr с определенным классом в созданной компьютером HTML-странице. Вы могли бы сделайте что-нибудь вроде этого:

    <tr class="TableRow">.*?</tr>(?=<tr class="TableRow">|</table>)
    

    Это относится к случаю, когда внутри строки появляется голый </tr>. (Конечно, в целом, HTML-парсер-лучший выбор, но иногда вам просто нужно что-то быстрое и грязное).

  • Несколько ограничений: предположим, у вас есть файл с данными, такими как id:tag1,tag2,tag3,tag4, с тегами в любом порядке, и вы хотите найти все строки с тегами "зеленый" и "яйцо". Это можно легко сделать с двумя lookaheads:

    (.*):(?=.*\bgreen\b)(?=.*\begg\b)
    

Есть две замечательные вещи о lookaround выражениях:

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

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

Я пытаюсь обратиться к вашим точкам зрения:

  • какой-то элемент оглядки в запросе, который, как мне кажется, не является необходимым для успеха матча

    Конечно, они необходимы для матча. Как только lookaround утверждения терпит неудачу, нет никакого соответствия. Они могут быть использованы для обеспечения условий вокруг паттерна, которые должны быть дополнительно истинными. Все регулярное выражение совпадает только в том случае, если:

    1. Шаблон подходит и

    2. Утверждения lookaround истинны.

    ==> но возвращенное соответствие - это только шаблон.

  • Когда действительно лучше использовать позитивный взгляд вокруг?

    Простой ответ: когда вы хотите, чтобы вещи были там, но вы не хотите соответствовать им!

    Как Берги упоминал в своем ответе , они являются утверждениями нулевой ширины, это означает, что они не соответствуют последовательности символов, они просто обеспечивают ее быть там. Таким образом, символы внутри выражения lookaround не "потребляются", механизм регулярных выражений продолжает работу после последнего "потребляемого" символа.

  • Что касается вашего первого примера:

    (?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span
    

    Я думаю, что есть недоразумение с вашей стороны, когда вы пишете " имеет простое решение для захвата .*". .* не является "захваченным", это единственное, что соответствует выражению. Но совпадают только те символы, которые имеют "<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">" перед и a "<\/a><span " после (эти двое не являются частью матча!).

    "захваченный" - это только то, что было сопоставлено с группой захвата .

  • Второй пример

    \d+(?<=id\/[\d])
    

    Интересно. Он соответствует последовательности цифр (\d+), и после последовательности утверждение lookbehind проверяет, есть ли одна цифра с "id/" перед ней. Означает, что он не будет работать, если есть более одной цифры или если текст "id/" перед цифрой отсутствует. Означает, что это регулярное выражение соответствует только одной цифре, когда есть соответствующий текст раньше.

  • Учебные ресурсы

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

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

Проверка
Проверка обычно выполняется на весь текст. Такие ищейки, как вы описываете, невозможны.

Матч
Извлечение части текста. Lookarounds используются в основном из-за лени разработчиков: избегание захватов .
Например, если у нас в файле настроек есть строка Index=5, мы можем сопоставить /^Index=(\d+)/ и взять первую группу, или сопоставить /(?<=^Index=)\d+/ и взять все.
Как было сказано в других ответах, иногда требуется перекрытие между совпадениями, но это относительно редко.

Заменить
Это похоже на матч с одним отличием: весь матч удаляется и заменяется новой строкой (и некоторые захвачены группы).
Пример: мы хотим выделить имя в "Hi, my name is Bob!".
Мы можем заменить /(name is )(\w+)/ на $1<b>$2</b>,
но правильнее заменить /(?<=name is )\w+/ на <b>$&</b> - и никаких захватов вообще.

Раскол
split берет текст и разбивает его на массив маркеров, при этом ваш шаблон является разделителем. Это делается с помощью:

  • найдите match. Все, что было до этого матча, является символическим.
    • содержание матча отбрасывается, но:
    • в большинстве вкусов, каждый захваченная группа в матче также является токеном (особенно не в Java).
  • Когда совпадений больше нет, остальная часть текста является последним символом.

Здесь lookarounds являются решающими. Сопоставление символа означает его удаление из результата или, по крайней мере, отделение от его маркера.
Пример: у нас есть разделенный запятыми список строк в кавычках: "Hello","Hi, I'm Jim."
Разбиение на запятые /,/ неверно: {"Hello", "Hi, I'm Jim."}
Мы не можем добавить кавычку, /",/: {"Hello, "Hi, I'm Jim."}
Единственный хороший вариант-смотреть в оба, /(?<="),/: {"Hello", "Hi, I'm Jim."}

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

Заключение

Чтобы ответить на главный вопрос-эти lookarounds используются потому, что:

  • иногда выне можете подобрать нужный текст.
  • разработчики бездельничают.

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

Например: регулярное выражение ^[0-9A-Z]([-.\w]*[0-9A-Z])*@ (1) также может быть записано ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])@(2) с использованием положительного взгляда назад (простая проверка имени пользователя в адресе электронной почты).

Регулярное выражение (1) может вызвать много отступлений по существу, потому что [0-9A-Z] является подмножеством [-.\w] и вложенных кванторов. Регулярное выражение (2) уменьшает чрезмерное отступление, более подробная информация здесь Backtracking , раздел Control Backtracking > Lookbehind Assertions.

Для получения дополнительной информации о backtracking

Я напечатал это некоторое время назад, но был занят (все еще занят, поэтому я мог бы занять некоторое время, чтобы ответить) и не удосужился отправить его. Если вы все еще открыты для ответов...


Есть ли какой-то педагогический ресурс, который их продвигает?
Я так не думаю, это просто совпадение, я верю.

Но, например, этот запрос из недавнего вопроса имеет простое решение для захвата .*, но зачем вам использовать взгляд сзади?

(?<=<td><a href="\/xxx\.html\?n=[0-9]{0, 5}">).*(?=<\/a><span

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

(<td><a href="\/xxx\.html\?n=[0-9]{0,5}">).*?(<\/a><span)

Если это для замены, или

<td><a href="\/xxx\.html\?n=[0-9]{0,5}">(.*?)<\/a><span

На матч. Хотя HTML-парсер определенно был бы более целесообразным здесь.

Lookarounds в этом случае I верьте медленнее. Смотритеregex101 demo , где соответствие составляет 64 шага для групп захвата, но 94+19 = 1-3 шага для lookarounds.

Когда действительно лучше использовать позитивный взгляд вокруг?Не могли бы вы привести несколько примеров?

Ну, lookarounds имеют свойство быть утверждениями нулевой ширины, что означает, что они на самом деле не комментируют совпадения, в то время как они вносят свой вклад в решение, чему соответствовать, а также допускают перекрытие спички.

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

Некоторые "эксплойты"я могу найти, просматривая некоторые мои старые ответы (ссылки ниже будут демо из regex101). Когда/Если вы видите что-то, что вам не знакомо, я, вероятно, не буду объяснять это здесь, так как вопрос сосредоточен на положительных результатах поиска, но вы всегда можете посмотреть на демонстрационные ссылки, которые я предоставил, где там есть описание регулярного выражения, и если вы все еще хотите получить какие-то объяснения, дайте мне знать, и я постараюсь объяснить как можно больше.

Чтобы получить совпадения между определенными символами:

В некоторых матчах положительные lookahead делают вещи проще, где lookahead может сделать то же самое, или когда это не так практично, чтобы использовать не lookarounds:

Собака вздохнула. -Я не супер-собака и не особенная собака, - сказал пес, - я обыкновенная собака, а теперь оставь меня в покое!- Собака толкнула его. отошел и направился к другой собаке.

Мы хотим получить все dog (независимо от случая) вне кавычек. С положительным взглядом мы можем сделать это :

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*$)

Чтобы убедиться, что впереди есть четное число цитат. С отрицательным lookahead, это будет выглядеть так это :

\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

Чтобы убедиться, что впереди нет нечетного числа кавычек. Или используйте что-то вроде this , если вы не хотите lookahead, но вам придется извлечь матчи группы 1:

(?:"[^"]+"[^"]+?)?(\bdog\b)
Хорошо, теперь скажем, что мы хотим противоположного; Найдите "собаку" внутри кавычек. Регулярное выражение с lookarounds просто должно иметь перевернутый знак, первый и второй:
\bdog\b(?!(?:[^"]*"[^"]*")*[^"]*$)

\bdog\b(?=(?:[^"]*"[^"]*")*[^"]*"[^"]*$)

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

"[^"]*(\bdog\b)[^"]*"

Но это не все совпадения, или вы можете использовать это :

"[^"]*?(\bdog\b)[^"]*?(?:(\bdog\b)[^"]*?)?"
Но это просто непрактично. для большего числа вхождений dog и вы получите результаты в переменных с возрастающими числами... И это действительно проще с lookarounds, потому что они являются утверждениями нулевой ширины, вам не нужно беспокоиться о выражении внутри lookaround, чтобы соответствовать dog или нет, иначе регулярное выражение не получило бы все вхождения dog в кавычках. Конечно, теперь эта логика может быть распространена на группы символов, такие как получение определенных шаблонов между словами, такими как start и [37]}.

Совпадающие совпадения

Если у вас есть строка типа:

abcdefghijkl

И хотите извлечь все последовательные 3 символа, возможные внутри, вы можете использовать это :

(?=(...))

Если у вас есть что-то вроде:

1A Line1 Detail1 Detail2 Detail3 2A Line2 Detail 3A Line3 Detail Detail

И хотите извлечь их, зная, что каждая строка начинается с #A Line# (где # - число):

1A Line1 Detail1 Detail2 Detail3
2A Line2 Detail
3A Line3 Detail Detail

Вы можете попробовать это, которое терпит неудачу из-за жадности...

[0-9]+A Line[0-9]+(?: \w+)+

Или это , которое при делании ленивым больше не работает...

[0-9]+A Line[0-9]+(?: \w+)+?

Но с положительным взглядом вы получите это :

[0-9]+A Line[0-9]+(?: \w+)+?(?= [0-9]+A Line[0-9]+|$)

И соответствующим образом извлекает то, что необходимо.

Еще одна возможная ситуация, когда у вас есть что-то вроде этого:

#ff00fffirstword#445533secondword##008877thi#rdword#

Который вы хотите преобразовать в три пары переменных (первая из пары-это # и некоторые шестнадцатеричные значения (6) и любые символы после них):

#ff00ff and firstword
#445533 and secondword#
#008877 and thi#rdword#

Если бы внутри не было хэшей "слова", было бы достаточно использовать (#[0-9a-f]{6})([^#]+), но, к сожалению, это не так, и вам придется прибегнуть к .*? вместо [^#]+, что еще не совсем решает проблему случайных хэшей. Однако положительные наблюдатели делают это возможным :

(#[0-9a-f]{6})(.+?)(?=#[0-9a-f]{6}|$)


Проверка И Форматирование

Не рекомендуется,но вы можете использовать положительные lookaheads для быстрой проверки. Например, следующее регулярное выражение позволяет ввести строку, содержащую по крайней мере 1 цифра и 1 строчная буква.

^(?=[^0-9]*[0-9])(?=[^a-z]*[a-z])

Это может быть полезно, когда вы проверяете длину символа, но имеете шаблоны различной длины в строке a, например, строка длиной 4 символа с допустимыми форматами, где # указывает на цифру, а дефис / тире / минус - должен быть в середине:

##-#
#-##

Регулярное выражение, подобное это делает трюк:

^(?=.{4}$)\d+-\d+

Где иначе, вы бы сделали ^(?:[0-9]{2}-[0-9]|[0-9]-[0-9]{2})$ и теперь представьте, что максимальная длина равна 15; число изменения, которые вам понадобятся.

Если вы хотите быстро и грязно переставить некоторые даты в "перепутанном" формате mmm-yyyy и yyyy-mm в более однородный формат mmm-yyyy, вы можете использовать это :

(?=.*(\b\w{3}\b))(?=.*(\b\d{4}\b)).*

Ввод:

Oct-2013
2013-Oct

Вывод:

Oct-2013
Oct-2013

Альтернативой может быть использование регулярного выражения (нормального соответствия) и обработка отдельно всех несоответствующих форматов отдельно.

Что-то еще, на что я наткнулся, так это формат индийской валюты , который был ##,##,###.### (3 цифры слева от десятичной и все остальные цифры, сгруппированные в пару). Если у вас есть вход 122123123456.764244, вы ожидаете 1,22,12,31,23,456.764244, и если вы хотите использовать регулярное выражение, этот делает это:

\G\d{1,2}\K\B(?=(?:\d{2})*\d{3}(?!\d))

((?:\G|^) в ссылке используется только потому, что \G совпадает только в начале строки и после совпадения) , и я не думаю, что это может работать без положительного lookahead, так как он смотрит вперед, не перемещая точку замена.)

Обрезка

Предположим, что у вас есть:

   this    is  a   sentence    

И хотите обрезать все пробелы одним регулярным выражением. У вас может возникнуть соблазн сделать общую замену на пробелы:

\s+

Но это дает thisisasentence. Ну, может быть, заменить на один пробел? Теперь он дает "this is a sentence" (двойные кавычки, используемые, потому что backticks ест пробелы). Однако кое-что вы можете сделать, это это :

^\s*|\s$|\s+(?=\s)

Который обязательно оставляет один пробел сзади так, что вы можете заменить ничем и получить "это предложение".

Расщепление

Ну, где-то еще, где положительные lookarounds могут быть полезны, где, скажем, у вас есть строка ABC12DE3456FGHI789 и вы хотите разделить буквы+цифры, то есть вы хотите получить ABC12, DE3456 и FGHI789. Вы можете легко использовать регулярное выражение:

(?<=[0-9])(?=[A-Z])

В то время как если вы используете ([A-Z]+[0-9]+) (то есть захваченные группы помещаются обратно в результирующий список / массив / etc, вы получите пустой и элементы тоже.

Обратите внимание, что это можно сделать и с помощью спички, с помощью [A-Z]+[0-9]+


Если бы мне пришлось упомянуть негативные lookarounds, этот пост был бы еще длиннее :)

Имейте в виду, что положительный/отрицательный lookaround является одним и тем же для движка регулярных выражений. Цель lookarounds-выполнить проверку где-то в вашем "регулярном выражении".

Один из главных интересов состоит в том, чтобы захватить что-то, не используя захват скобок (захват всего шаблона), например:

Строка: aaabbbccc

Регулярное выражение: (?<=aaa)bbb(?=ccc)

(Вы получаете результат со всем шаблоном)

Вместо: aaa(bbb)ccc

(Вы получаете результат с захватом группа.)