Изобретение

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

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

Пожалуйста, обратите внимание, что в регулярном выражении используются пробелы для улучшения читабельности. В регулярном выражении пробелы обычно используются как символы в строке, поэтому, чтобы эти шаблоны работали, требуется флаг (?ix).

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

Объяснение

Начнем с простой задачи. Если в начале строки есть c, нужно найти в ней только слова и цифры (подсвечены красным):

c word = word + key
c 12 = word + word
word & word = word + word
12 = word + word

Фактически мы должны найти только слова типа word и цифры 2 и 0 только в первых двух строках.

Казалось бы, в чем проблема? Ввел что-нибудь вроде \w+ (фундаментальное выражение поиска букв вроде А-Я) и нашел что надо...

Но как же условие? Ведь нам нужно учесть букву c в начале строки. Попробуем использовать синтаксис условий в RegEx: (?(condition)|(true)(false)). Ввводим на каком-нибуль сайте вроде regex101.com и... RegEx если что и захватит, так это некоторую часть выражения. Хотя по правде он даже не найдет эту часть, ведь перед буквами стоят символы вроде = + &, т.е. RegEx не сработает.

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

В решении данной задачи невозможно использовать look ahead/behind (слова стоят далеко от кавычек), условия (?(condition)|(true)(false)) и подгруппы с квантификатором ( )+ потому что согласно цитате с regex101.com

a repeated capturing group will only capture the last iteration

что на нашем прекрасном языке звучит как

повторяющаяся захватывающая группа захватит только последнюю итерацию

Проще говоря, никаких тебе циклов и тернарных операторов, может пора подключать Python?

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

condition \K  # Найти условие и пропустить

|  			  # Начало цикла

(?<=\G)  		# Убеждаемся что условие найдено; каждая следующая итерация идет с этой позиции RegEx и с позиции предыдущей итерации
separator*?  	# Нежадный: разделитель между словами
\K  			# Пропустить все что было прежде
expression  	# Выражение: \w+ или .+ или \d+ ...

Идея такова: встретив condition , RegEx пропускает его \K и продолжает поиск с его позиции (?<=\G) . Проходит мимо нежадного разделителя слов separator, пропускает его \K и наконец захватывает нужное expression.

Дойдя до конца, все повторяется вновь с позиции последнего найденного слова (?<=\G). Но, чтобы цикл шел верным путем и продолжал шагать по строке, необходимо добавить перед (?<=\G) символ или |.

Обратите внимание на символ \K, суть которого важно запомнить и уметь применять самостоятельно: он означает, что все, что было найдено прежде, ныне не имеет значения и исчезает из финального варианта. Сдвиг каретки/курсора, если хотите. Позволяет найти условие, отсечь его из результата и вернуть нужное. Главное помните: \K не работает в обычных захватывающих группах ( ), только в незахватывающих и атомных группах: (?:) и (?>). Но в примерах я вообще не стал использовать группы. И это тоже работает!

Символ \K стоит после условия и разделителя. Повторю еще раз: найдя condition, мы первый раз пропускаем условие-шаблон, и пройдя через separator между словами и мы с каждой итерацией будем пропускать разделитель-шаблон. Они нам не нужны, нам нужны слова. Это лишь вспомогательные конструкции.

Теперь конструируем RegEx согласно шаблону (DEMO):

c \K  		# Условие: буква "c"

|  			# Начало цикла

(?<=\G)  		# Убеждаемся что условие найдено; каждая следующая итерация идет с этой позиции RegEx и с позиции предыдущей итерации
.*?  			# Нежадный разделитель: 1 и более любых символов
\K  			# Пропустить все что было прежде
\w+  			# Жадное выражение: любые буквы, цифры

Как получился такой шаблон? Condition у нас буква c, дальше ничего из шаблона не менялось, потом separator у нас любой символ .*, затем сам шаблон поиска букв \w который будет циклично искать буквы до конца всей строки.

Усложним эту задачу: если в начале строки есть c, а затем любые кавычки " ', нужно найти в кавычках только слова и цифры (подсвечены зеленым):

c"word & word" = word + word
c"12 = word" + word
c word & word = word + word
c 12 = word + word

Задача вроде похожа, а значит и шаблон будет не сильно отличаться от предыдущего. Но появилось существенное НО: мы больше не должны жадно хватать все слова из строки. Мы должны остановиться именно тогда, когда первый луч света освободит нас от работы с RegEx когда после слов появится кавычка. Т.е. "bla bla bla" STOOOP. Еще раз: встретили кавычку, подхватили все слова после нее, встретили кавычку вновь и остановились.

Значит теперь у нас есть условие остановки цикла. Шаблон для подобной задачи выглядит следующим образом:

condition \K  # Найти условие и пропустить

|  			  # Начало цикла

(?<=\G)  	# Убеждаемся что условие найдено; каждая следующая итерация идет с этой позиции RegEx и с позиции предыдущей итерации
stop*?  		# Символ остановки всего выражения: формат [^exclude]
\K  			# Пропустить все что было прежде
expression  	# Выражение: \w+ или .+ или \d+ ...

Теперь конструируем RegEx согласно шаблону. Шаблон аналогичен предыдущему, но к условию добавлены кавычки ["']: c ["']

Появляется условие остановки регулярного выражения: [^"'](здесь символ ^ означает, что нужно найти любые символы, кроме кавычек.). После этого поиск завершается. Теперь мы создаем конструируем в соответствии с шаблоном (DEMO):

c ["'] \K  # Условие: c" или c'            

|  		   # Начало цикла

(?<=\G)  		# Убеждаемся что условие найдено
[^"']*?  		# Кавычки после которых завершается поиск
\K  			# Пропустить все что было прежде
\w+  			# Жадное выражение: любые буквы, цифры

Попробуйте решить эти задачи не используя данные шаблоны. Я буду очень рад, если вы найдете иное оптимальное решение без Python!

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

`{string} with {exluded} words 12 nums`
`string {with} {exluded} words 12 nums`
"quoted {string} with {exluded} words and 12 nums"
"quoted string {with} exluded {words} and {12} nums"

Значит мы должны изменить шаблон так, чтобы у него было условие остановки, условие обхода и наконец само захватывающее выражение. В данном случае должно быть два разных условия остановки: условие остановки и повтора цикла если обнаружены скобки { }; условие остановки выражения если обнаружены кавычки `:

# Условие после которого запускается 2 часть выражения
^condition # символ ^ означает начало строки

|  		   # Начало цикла

(?<=\G)  	# Убеждаемся что условие найдено

(?>      	# Атомная группа
    skip 		# условие обхода: например {.*}
 	|  			# ИЛИ
    stop    	# условие остановки: например [^"']
) \K      	# Пропустить все что было прежде

expression  # Выражение: \w+ или .+ или \d+ ...

Тут надо сразу рядом показать результат (DEMO) и объяснить его идею:

^[`]\K # Находит одиночные/двойные кавычки, убирает их из результата

|  		# Начало цикла

(?<=\G)  	# Убеждаемся что условие найдено

(?>      	# Атомная группа
    {.*?}   	# Пропускает содержимое скобок { }
 	|			# ИЛИ
    [^`]    	# Останавливается после вторых кавычек
) \K      	# Пропускает все что было прежде
[^{}`]+  	# Ищет 1 и более символов КРОМЕ { } `

Идея такова: встретив condition RegEx начинает с его позиции (?<=\G), идет дальше, останавливается если обнаружена кавычка, обходит мимо группу, сбрасывает текущую позицию и наконец захватывает нужное expression. Дойдя до конца, все RegEx повторяется вновь с позиции последнего найденного слова (?<=\G). И так до тех пор, пока не встретит главное условие остановки.

Атомная группа (?>...) здесь важна для скорости поиска. Дело в том, что RegEx часто перебирает все варианты поиска подстрок по шаблону. Но как только эта группа найдет содержимое скобок, RegEx не будет искать 100500 вариантов как бы получше ухватить строку и все ее слова в скобках. Проще говоря: нашли, остановились на этом этапе и поехали дальше. Без лишних циклов и поисков.

Ограничения

Я буду очень рад, если вы найдете другое оптимальное решение! Пожалуйста, помогите мне улучшить данные шаблоны. У них есть существенные проблемы с оптимизацией: если не найдено condition, для каждого символа проверяется alternation (?<=\G); нет пропуска неподходящих строк; не работают флаги (*SKIP)(*F). Не смотря на быструю скорость работы, количество шагов стремится к 100.000.

Комментарии (1)


  1. Alexandroppolus
    15.01.2025 12:45

    Усложним эту задачу: если в начале строки есть c, а затем любые кавычки " ', нужно найти в кавычках только слова и цифры (подсвечены зеленым):

    Cделал просто с lookbehind - https://regex101.com/r/KY6Lba/1 . Не знаю, оптимальней это или нет, но в js в регексах, кажется, нет \G и \K