Примеры регулярных выражений

Обсуждение любых программ, не имеющих отношения к Total Commander.

Модераторы: Avada, CaptainFlint, Nik, d-view, Моторокер

Аватара пользователя
Batya
Сообщения: 2236
Зарегистрирован: Ср дек 15, 2004 09:51
Откуда: Москва, Россия

Примеры регулярных выражений

Сообщение Batya »

Комрады, прошу помощи. Что-то мозг закипает.
Есть текст вида
_|_123_|_124_|_125_|_123_|_126_|_127
_|_123_|_128_|_129
Задача - если в тексте встречается такой же текст, как в начале строки по шаблону "^(_\|_\d+_\|)", заменить на перенос строки такой текст в любом месте строки. Т. е. применительно к указанному примеру заменять нужно исключительно "_|_123_|", и должно получиться:
_124_|_125
_126_|_127

_128_|_129
Заранее число, стоящее в начале строки неизвестно. Но заменять надо только блоки, содержащие число из начала строки.
Соответственно, аналогичный результат должен получиться и, например, для текста:
_|_78_|_124_|_125_|_78_|_126_|_127
_|_78_|_128_|_129
Если принципиально, использую поиск в Notepad++.
Нет, я не сплю. Я просто медленно моргаю.
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

Batya писал(а): Что-то мозг закипает
Мозг действительно закипает :)
Batya писал(а):Если принципиально, использую
А принципально использовать регулярные выражения? Может быть, для данной задачи лучше использовать скрипт?

Я сам никогда не пользовался, но есть, вроде, такие продвинутые вещи, как проверки и заглядывания вперед и назад (assertions: lookahead, lookahead), может быть, они подойдут для решения этой задачи?
Аватара пользователя
Batya
Сообщения: 2236
Зарегистрирован: Ср дек 15, 2004 09:51
Откуда: Москва, Россия

Сообщение Batya »

На работе используется Notepad++. Необходимо оперативно по определённым правилам менять текст в несохранённом документе. Для этого хочу записать Notepad++-макрос. Вот для записи макроса и пытаюсь правильно составить регулярное выражение поиска и замены.
Ключевые моменты - текст, как я говорил, не сохранён в виде файла; буфер обмена при использовании макроса трогать крайне нежелательно.
Ну и для себя пытаюсь разобраться в ранее неосвоенных тонкостях регулярных выражений.
Нет, я не сплю. Я просто медленно моргаю.
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

Batya
Я понял.

Почему бы не подключить к этому делу ИИ? Он меня уже не раз выручал. Главное [s]держать его в узде[/s] вопросы правильные к нему формулировать :D
Аватара пользователя
yozhik
Сообщения: 353
Зарегистрирован: Вс май 04, 2014 20:34
Откуда: Электросталь

Сообщение yozhik »

Batya

Обновлено: Прошу прощения, не вчитался! Корректный ответ ниже.

Сейчас нет возможности опробовать (Notepad++ не пользуюсь), но судя по справке должно работать вот это:

Код: Выделить всё

Найти:
(?m-s)^(_\|_\d+_\|)(.+?\r\n)\1
Заменить:
\2
Если в файле окончания строк LF, тогда в поиске \r\n заменить на \n.
Попробуйте, пожалуйста, если не сработает, то скачаю портабельный Notepad++, погоняю.
Последний раз редактировалось yozhik Сб июл 18, 2026 14:29, всего редактировалось 1 раз.
Amo ergo sum
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

yozhik
А где пояснения в стиле ИИ? :D

Кстати, он мне говорит, что за одну итерацию невозможно заменить все \1. Только программным путем :?
Аватара пользователя
yozhik
Сообщения: 353
Зарегистрирован: Вс май 04, 2014 20:34
Откуда: Электросталь

Сообщение yozhik »

Orion9 писал(а):А где пояснения в стиле ИИ?
Это для молодых. Бате не надо, он насквозь видит.
Orion9 писал(а):он мне говорит, что за одну итерацию невозможно заменить все \1.
Это он так денег просит. Культурно намекает.

Регулярка то рабочая, в EditPad Pro исправно отрабатывает:
Изображение

Но каждый редактор надо отдельно проверять. В AkelPad, например, backreferenc'ы в строке поиска не всегда срабатывают.
Amo ergo sum
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

yozhik писал(а):Это для молодых. Бате не надо, он насквозь видит.
Не беда. Как вы знаете, [s]болвана[/s] ИИ всегда можно рубануть в лоб прямым вопросом:

[cut]объясни выражение (?m-s)^(_\|_\d+_\|)(.+?\r\n)\1

Это регулярное выражение предназначено для поиска двух идущих подряд строк, которые начинаются с одного и того же префикса-идентификатора. При этом первая строка полностью захватывается, а от второй строки захватывается только этот повторяющийся префикс.
Давайте разберем выражение по деталям:
## 1. Флаги и модификаторы (?m-s)

* ?m (Multiline) — включает многострочный режим. Символ ^ теперь означает начало каждой отдельной строки в тексте, а не начало всего документа.
* -s (no Singleline) — отключает режим, при котором точка совпадает с переносом строки. Здесь точка . означает любой символ, кроме переноса строки.

## 2. Поиск префикса ^(_\|_\d+_\|)

* ^ — ищет строго в начале строки.
* (...) — это первая захватывающая группа (Group 1). Текст, который сюда попадет, движок regex запомнит.
* _\|_ — ищет символы _|_. Символ вертикальной черты | экранирован обратным слэшем \|, чтобы он воспринимался как обычный текст, а не как логическое «ИЛИ».
* \d+ — ищет одну или более цифр подряд (например, 1, 42, 2026).
* _\| — ищет символы _|.
* Итог группы 1: Ищется маркер вида _|_42_| в самом начале строки.

## 3. Захват остатка первой строки (.+?\r\n)

* (...) — это вторая захватывающая группа (Group 2).
* .+? — ленивый поиск любых символов до конца текущей строки.
* \r\n — символ перевода строки (Windows-формат).
* Итог группы 2: Сюда сохраняется всё содержимое первой строки (вместе с её финальным переносом), кроме начального префикса.

## 4. Проверка дубликата \1

* \1 — это обратная ссылка (backreference) на Группу 1. Движок требует, чтобы в этом месте текста (то есть в начале следующей строки) находились ровно те же самые символы, которые были найдены в пункте 2.

------------------------------
## 📝 Визуальный пример работы
Допустим, у нас есть такой текст:

_|_105_| Текст первой строки.
_|_105_| Текст второй строки.
_|_200_| Другой текст.

Это регулярное выражение совпадет со следующим фрагментом:

1. ^(_\|_\d+_\|) найдет _|_105_| на первой строке и запомнит как \1.
2. (.+?\r\n) найдет Текст первой строки.[разрыв строки] и запомнит как \2.
3. \1 перейдет на вторую строку и проверит, начинается ли она с _|_105_|. Так как это совпадает, весь этот блок будет выделен.

Если вы будете использовать этот шаблон для замены (например, в AkelPad), вы можете заменить найденное на $1$2 (или \1\2), чтобы удалить префикс-дубликат со второй строки, объединив или очистив текст.[/cut]
yozhik писал(а):Это он так денег просит. Культурно намекает.
:lol:

Да нет. Видимо, это я неправильно задачу понял. Я думал, что нужно искать и заменять в каждой строке отдельно, т.е. если есть текст из трех строк:

Код: Выделить всё

_|_78_|_124_|_125_|_78_|_126_|_127
_|_78_|_128_|_129
_|_122_|_123_|_124_|_122_|_127_|_128
То буду заменены только строки 1 и 3, т.к. они содержат блоки с которого они начинаются. А если нужно заменить во всем тексте блоки с которого начинается этот текст, то не понимаю, зачем вообще регулярные выражения. Можно просто выделить начало текста и заменить. Или там сотни замен нужно делать каждый день?
Аватара пользователя
yozhik
Сообщения: 353
Зарегистрирован: Вс май 04, 2014 20:34
Откуда: Электросталь

Сообщение yozhik »

Orion9 писал(а):Видимо, это я неправильно задачу понял.
Нет, это я её неправильно понял, точнее поторопился и просто не вчитался.

Ограничение регулярок в том, что для замены нужно знать точное кол-во совпадений. Т.е. я могу найти неопределённое число совпадений, но если мне нужно каждое из них заменить, то в замене я должен сослаться на каждое совпадение по его номеру, иначе никак.

Поэтому к Бате вопрос: этот шаблон стабильный или варьируется?

Код: Выделить всё

_|_123_|_124_|_125_|_123_|_126_|_127
_|_123_|_128_|_129

другими словами

(искомое) ... (искомое) ...
(искомое) ...
Т.е. «искомое» всегда ли 2жды в первой строке и 1жды во второй? Если да, то поможет вот это:

Код: Выделить всё

Найти:
(?m-s)^(_\|_\d+_\|)(.+?)\1(.+?\r\n)\1
Заменить:
\r\n\2\r\n\3\r\n
Если нет, то решение — перечислить в макросе по очереди все возможные варианты. Например:

Код: Выделить всё

Для:
(искомое) ... (искомое) ... (искомое) ...
(искомое) ... (искомое) ...

Найти/Заменить:
(?m-s)^(_\|_\d+_\|)(.+?)\1(.+?)\1(.+?\r\n)\1(.+?)\1
\r\n\2\r\n\3\r\n\4\r\n\5\r\n

Для:
(искомое) ... (искомое) ... (искомое) ...
(искомое) ... 

Найти/Заменить:
(?m-s)^(_\|_\d+_\|)(.+?)\1(.+?)\1(.+?\r\n)\1
\r\n\2\r\n\3\r\n\4\r\n

Для:
(искомое) ... (искомое) ...
(искомое) ... 

Найти/Заменить:
(?m-s)^(_\|_\d+_\|)(.+?)\1(.+?\r\n)\1
\r\n\2\r\n\3\r\n

Для:
(искомое) ... 
(искомое) ... 

Найти/Заменить:
(?m-s)^(_\|_\d+_\|)(.+?\r\n)\1
\r\n\2\r\n

и т.п.
Ну, или скрипт писать, да, программно.
Amo ergo sum
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

yozhik писал(а):Поэтому к Бате вопрос:
Согласен, нужно уточнить задачу. Но раз уж Batya озаглавил тему так, что можно делиться собственным неудачным (или удачным?) опытом, то помню, когда сталкнулся в справке с такими примерами:

Код: Выделить всё

RegEx  Находит  
foob.*r      foobar, foobalkjdflkj9r и foobr  
foob.+r      foobar, foobalkjdflkj9r, но не foobr  
foob.?r      foobar, foobbr и foobr, но не foobalkj9r  
fooba{2}r    foobaar  
fooba{2}r `  foobaar, foobaaar, foobaaaar` и т. д.  
fooba{2,3}r  foobaar, или foobaaar, но не foobaaaar  
(foobar){8,10} 8, 9 или 10 экземпляров foobar (() это Группа) 
Очень мне не понравился этот fooba{2}r `. Хотелось бы услышать пояснений, но не догадался тогда у болвана спросить :D

[cut="почему находит foobaar, foobaaar, foobaaaar`"]Если ваше регулярное выражение fooba{2}r находит слова с тремя, четырьмя или более буквами «а» (foobaaar, foobaaaar), это происходит по одной из двух причин:
## 1. Отсутствие строгих границ слова (Самая частая причина)
Движок регулярных выражений ищет частичное совпадение внутри строки. Квантификатор {2} требует, чтобы букв «а» было минимум две.
Когда движок видит слово foobaaar:

* Он проверяет префикс foob.
* Затем он видит группу aa (первые две буквы «а») — это условие a{2} выполнено.
* Но третья буква «а» для него становится частью следующего символа. Однако в вашем шаблоне дальше идёт буква r, а не a. Движок спотыкается.
* Тогда он включает механизм бэктрекинга (отката): он сдвигает своё окно поиска на одну букву вправо. Он берёт вторую и третью буквы «а» как выполнение условия a{2}, а за ними как раз идёт нужная буква r .
* В итоге внутри слова foob[aaa]r он успешно выделяет подстроку foob[aar] .

## 2. Специфика старого движка AkelPad
Если вы тестируете это в AkelPad, то его встроенный (старый стандартный) движок регулярных выражений имеет упрощенный синтаксис. В некоторых его версиях квантификатор {n} работает не как «строго n раз», а как «n или более раз» (то есть аналогично {n,}).
------------------------------
## 🛠️ Как это исправить?
Чтобы заставить движок искать строго две буквы «а» и игнорировать слова, где их больше, используйте один из вариантов:

1. Добавить негативную проверку (Lookbehind/Lookahead):
Это запретит буквам «а» стоять до или после нашей пары.

fo(?<!a)ba{2}(?!a)r `

2. Для AkelPad (через явное указание не-буквы):
Если вы ищете слова целиком, ограничьте их метасимволом границы слова \b:

\bfooba{2}r\b `

Но учтите, если в движке AkelPad баг с квантификатором, то для строго двух «а» надежнее написать их вручную без фигурных скобок: \bfoobaar\b [/cut]
Аватара пользователя
yozhik
Сообщения: 353
Зарегистрирован: Вс май 04, 2014 20:34
Откуда: Электросталь

Сообщение yozhik »

Orion9 писал(а):fooba{2}r `
Ох, так ведь это ж очепятка :) У Сорокина такое встречается, к сожалению. Надо смотреть исходники, и в них сразу видны сбои автоматики:
https://github.com/andgineer/TRegExpr/b ... ain=1#L332

Здесь должно было быть fooba{2,}r . Запятая после двойки у Сорокина потерялась, но сохранилась, например, здесь и здесь (поиск на странице по слову "foobalkjdflkj9r", так быстрее).
болван писал(а):Однако в вашем шаблоне дальше идёт буква r, а не a. Движок спотыкается. ...
Он берёт вторую и третью буквы «а» как выполнение условия a{2}, а за ними как раз идёт нужная буква r .
А болван то юморист! Хорошо насмешил. Занимательные такие антропоморфизмы. И большой плюс болвана в том, что когда вокруг смеются, ему не обидно :)
Amo ergo sum
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

yozhik писал(а):Ох, так ведь это ж очепятка
:shock:

У него-то, может, опечатка. А меня картина мира перевернулась. Две ночи не мог уснуть, всё думал над дилеммой.
yozhik писал(а):У Сорокина такое встречается, к сожалению. Надо смотреть исходники, и в них сразу видны сбои автоматики:
Смотреть исходники - это последнее, что бы мне пришло в голову :lol:
yozhik писал(а):А болван то юморист!
Не только. Он еще и друг человека... И человек - бовану друг :D
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

yozhik
Чуть не забыл сказать спасибо за разъяснение по опечатке :)

А болван, да — радует своим креативом. Недавно задавался смежным вопросом, так почему бы его еще не помучать?

[cut]как оптимизировать и ускорить выражение (?i)(button|cmd|param|path|menu|iconic)(\d+?)=(.*?)(\R|\Z)

Для оптимизации этого выражения нужно устранить избыточный бэктрекинг (откаты движка), который вызывают ленивые квантификаторы (+? и *?), стоящие перед перебором вариантов и концом строки.
Вот максимально ускоренная и очищенная версия выражения:

(?i)\b(button|cmd|param|path|menu|iconic)(\d++)=(.*)

## 🛠 Что конкретно изменено и почему это увеличит скорость:

1. Замена (\R|\Z) на автоматический конец строки:
Вам не нужно вручную перебирать символ переноса \R или конец текста \Z. По умолчанию точка . (в обычном режиме, без флага s) не совпадает с переносом строки. Поэтому выражение (.*) само дойдет строго до конца текущей строки (или конца файла), работая в разы быстрее.
2. Замена ленивого *? на жадный .*:
Ленивый квантификатор (.*?) заставлял движок после каждого считанного символа проверять: «А не наступил ли еще перенос строки?». Жадный (.*) сразу забирает весь остаток строки до конца без лишних проверок.
3. Сверхжадный (обладающий) квантификатор \d++:
Знак ++ запрещает движку возвращаться и «отдавать» цифры назад, если дальнейшая часть выражения не совпала. Это на корню отсекает бэктрекинг на неудачных совпадениях.
4. Добавление границы слова \b:
Движок не будет пытаться проверить список слов (button, cmd...) внутри других длинных слов (например, в слове com**mand**). Поиск начнется только с начала слова.

------------------------------
## 📊 Сравнение структуры (было / стало)

Код: Выделить всё

| Параметр | Исходное выражение | Оптимизированное выражение |
|---|---|---|
| Поведение группы цифр | Ленивое (\d+?) — медленный пошаговый перебор | Сверхжадное (\d++) — мгновенный захват |
| Поведение значения | Ленивое ((.*?)) — постоянные спотыкания | Жадное ((.*)) — линейное чтение до упора |
| Проверка конца строки | Тяжелая группа альтернатив (\R|\Z) | Естественная граница точки . |
сможет ли оптимизированное выражение обработать многострочный файл в TRegExpr

Нет, в исходном виде эта оптимизированная строка вызовет ошибку синтаксиса или отработает некорректно в библиотеке TRegExpr. [1]
Дело в том, что TRegExpr (популярная библиотека для Delphi и Lazarus) имеет упрощенный, классический Perl-подобный синтаксис. В ней отсутствует поддержка сверхжадных (possessive) квантификаторов (таких как ++). [2, 3, 4]
Чтобы выражение гарантированно и быстро обрабатывало многострочный текст в TRegExpr, его нужно адаптировать под этот движок.
## 🛠️ Адаптированная версия для TRegExpr:

(?mi)^(button|cmd|param|path|menu|iconic)(\d+)=(.*)

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

1. Добавлен флаг многострочности (?mi)
Флаг m критически важен. Без него метасимвол начала строки ^ проверял бы только самое начало файла, пропуская все остальные строки. [5]
2. Добавлен якорь начала строки ^
Поскольку мы убрали \b, символ ^ жестко связывает поиск с началом каждой строки. Движок мгновенно отбросит строки, которые начинаются не с ключевых слов. [5]
3. Исправлен квантификатор \d+
Так как синтаксис \d++ библиотека TRegExpr не поймет, мы возвращаем стандартный жадный \d+. В связке с символом = он отработает максимально быстро и без лишних откатов. [2]
4. Безопасный жадный захват (.*)
В TRegExpr точка . по умолчанию не включает в себя символ перевода строки \n. Поэтому жадный квантификатор (.*) дойдет строго до конца текущей строки и остановится, не перескакивая на следующую.
[/cut]
Аватара пользователя
Batya
Сообщения: 2236
Зарегистрирован: Ср дек 15, 2004 09:51
Откуда: Москва, Россия

Сообщение Batya »

Спасибо за ответы!
Найти:
(?m-s)^(_\|_\d+_\|)(.+?\r\n)\1
Заменить:
\2
К сожалению, не то :(
этот шаблон стабильный или варьируется?
Шаблон варьируется.
Постараюсь описать в более широком смысле...
Есть некоторое приложение, которое формирует логи работы. При этом часть логируемых данных записывается в некотором табличном виде. Суть задачи - скопировать из большого лога часть текста, описывающего таблицу, и не сохраняя в отдельный файл в Notepad++ макросом преобразовать этот текст таким образом, чтобы его потом скопировать и вставить в Excel уже в виде нормальной таблицы.
В исходном логе строки таблицы могут быть на отдельных строках, а могут быть склеены. Ключевая особенность в том, что начало табличного текста и начало каждой строки начинается с одинакового фрагмента, который меняется для каждого лога. Количество строк всегда разное. Количество склеенных строк (с удалённым переносом строк) заранее не известно. Проблема именно в том, что иногда случайны образом переносы строк удалены, и строки склеены.
Примеры:

Код: Выделить всё

_|_78_|_чило124_|_текст125_|_78_|_текст126_|_число127
_|_78_|_ля-ля-ля128_|_129
_|_78_|_123_|_124_|_78_|_127_|_128

Код: Выделить всё

_|_78_|_чило124_|_текст125
_|_78_|_текст126_|_число127
_|_78_|_ля-ля-ля128_|_129
_|_78_|_123_|_124_|_78_|_127_|_128

Код: Выделить всё

_|_78_|_чило124_|_текст125_|_78_|_текст126_|_число127_|_78_|_ля-ля-ля128_|_129_|_78_|_123_|_124_|_78_|_127_|_128
Сам по себе начальный фрагмент полезной информации не несёт. Его можно заменить, а можно и оставить. В итоге хочу, чтобы получилось так:

Код: Выделить всё

чило124_|_текст125
текст126_|_число127
ля-ля-ля128_|_129
123_|_124
127_|_128
или так

Код: Выделить всё

_|_78_|_чило124_|_текст125
_|_78_|_текст126_|_число127
_|_78_|_ля-ля-ля128_|_129
_|_78_|_123_|_124
_|_78_|_127_|_128
После замены убрать лишние пустые строки - не проблема. Возможное попадание хороших данных в совпадение с начальным фрагментом и замена их на перенос строки - не страшно.
Думаю, что задачу можно решить макросом на vba для вставки в Excel, но хотелось именно решить через регулярные выражения - давно мечтал их полностью освоить.
Последний раз редактировалось Batya Пн июл 20, 2026 12:16, всего редактировалось 1 раз.
Нет, я не сплю. Я просто медленно моргаю.
Аватара пользователя
Orion9
Сообщения: 1283
Зарегистрирован: Пн янв 01, 2024 17:18

Сообщение Orion9 »

Batya
Как я и говорил, тут регэкспресы особо не нужны. Можно просто тройной заменой обойтись. Сначала заменить _|_78_| на \n. Потом \n_ на \n. Потом \n\n на \n. Результат будет ровно, как вы хотите.
Ответить