The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Регулярные выражения Perl: замена n-го совпадения (perl regexp)


<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>
Ключевые слова: perl, regexp,  (найти похожие документы)
From: Сергей Мельников <subm3@cronc.com.> Newsgroups: email Date: Mon, 20 Oct 2008 17:02:14 +0000 (UTC) Subject: Регулярные выражения Perl: замена n-го совпадения Это глава из моей книги "Perl для профессиональных программистов. Регулярные выражения", которая вышла в изд-ве "Бином". У меня осталось несколько авторских экз. этой книги, желающие могут заказать. E-mail находится на моём сайте Ресурсы для вебмастеров. Замена n-го совпадения Рассмотрим замену n-го найденного фрагмента текста. use locale; my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $count=0; $s =~ s/(тел\.\s+)([\d-]+)/ ++$count == 3 ? "${1}9-9999" : "$1$2"/egi; print $s; Напечатается строка "Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 9-9999" Оператор подстановки с модификатором g заменяет все найденные фрагменты текста глобально, и ему для этого не нужен списочный контекст как оператору поиска. Мы хотели третий номер телефона заменить на 9-9999, но после каждого найденного номера выполняется замена, поэтому то, что не нужно менять, должно быть заменено на себя. Для этого мы взяли все перед номером телефона в переменную $1, а все остальное - в переменную $2. Если номер телефона не равен 3, то мы найденный фрагмент текста (который соответствует всему регулярному выражению!) меняем на строку $1$2, а в третьем случае в замене вместо номера телефона подставляем 9-9999. Для разделения имени переменной от цифр используем фигурные скобки. Не забываем также поставить модификатор e. Как быть, если надо заменить n-й от конца найденный фрагмент текста, если заранее неизвестно, сколько таких фрагментов будет найдено? Рассмотрим такую идею. Призываем в помощь конструкцию опережающей проверки и записываем регулярное выражение так, чтобы оно совпало только на n-м от конца искомом фрагменте текста, где n отсчитывается с нуля. Пусть это n хранится в переменной $n. Попробуем вначале такую программу для решения задачи: #!/usr/bin/perl -w use strict; use locale; my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $tel='тел\.\s+[\d-]+'; my $n=1; $s =~ s/(тел\.\s+)[\d-]+ # ищем слово "тел." + номер (?=(?:.*?$tel){$n} # за которым $n раз идет "тел." + номер (?!.*?$tel) # после чего не встречается "тел." +номер )/${1}9-9999/isx; print $s; Поясню, как она должна работать по первоначальному замыслу. Т.к. литерал тел\.\s+[\d-]+ в регулярном выражении будет встречаться несколько раз, то оформим его в виде переменной $tel, которую будем подставлять. Сформулируем задачу в терминах регулярных выражений: нам надо найти фрагмент, соответствующий шаблону тел\.\s+[\d-]+ за которым (фрагментом) в тексте встречается ровно $n таких же фрагментов. Номер телефона в таком фрагменте мы меняем на 9-9999. Чтобы все остальное в этом фрагменте кроме номера телефона сохранилось, мы берем это остальное в скобки и получаем подшаблон (тел\.\s+)[\d-]+, с которого начинается регулярное выражение. За фрагментом текста, соответствующим этому шаблону, должен идти фрагмент (?:.*?$tel){$n}. .* впереди него означает, что эти $n фрагментов не обязательно должны идти сразу за первым фрагментом и друг за другом, между ними могут быть посторонние включения, которые поглощаются конструкцией .*?. После того, как эти $n фрагментов поглощены, не должно стоять такого же фрагмента текста с любой позиции после этих $n фрагментов, это проверяет условие (?!.*?$tel). Оба этих подшаблона (?:.*?$tel){$n} и (?!.*?$tel) включены в позиционную проверку (?=...). Т.е. за найденным номером телефона должен быть фрагмент текста, который стоит внутри всей этой проверки (?=...). Что ж, как будто бы все верно, начинаем проверять работу этой программы. Задаем $n=0 и видим результат: "Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 9-9999" Верно! Заменился номер телефона, который стоит нулевым справа. Далее даем $n значение 1 и смотрим результат. Вот неожиданность: заменился первый телефон "Тел. 9-9999. Другой тел. 3-2233, а вот еще один тел. 4-1122" а должен был бы тот, что посередине! При $n=2 та же картина. Видимо, где-то закралась ошибка. Можете вы найти (и исправить) ее самостоятельно? Тогда читайте дальше. Разберем работу этого регулярного выражения при $n=1. После того, как нашелся первый номер телефона по подшаблону (тел\.\s+)[\d-]+, началось заглядывание вперед, и подшаблон (?:.*?$tel){$n} поглотил второй телефонный номер. За ним началась проверка (?!.*?$tel), которая закончилась неудачей, т.к. после второго телефонного номера есть еще номер. "Не беда! - говорит механизм поиска соответствия. Буду увеличивать значение минимального квантификатора .*? в подшаблоне (?:.*?$tel){$n}, авось поможет." И начинает его увеличивать и пробовать эти значения. Когда этот квантификатор захватит все до вертикальной черты в следующей строке: "Тел. 2-3344. Другой тел. 3-2233, а вот еще один т|ел. 4-1122" весь подшаблон захватит фрагмент ", а вот еще один тел. 4-1122" т.е. все до конца текста, и после этого проверка (?!.*?$tel) пройдет успешно. Налицо совпадение всего регулярного выражения, поэтому первый номер телефона будет заменен на 9-9999. Ошибка ясна: .*? начал поглощать символы, которые относились к номеру телефона, а он не должен был этого делать. Надо заменить это на правильный подшаблон, который должен поглощать символы до фрагмента тел., за которым идет номер. Когда речь шла о пропуске символов до закрывающей угловой скобки при поиске ссылки, то все было просто: [^>]*, но здесь уже не один символ, поэтому конструкция [^$tel]*, вообще говоря, не годится, у нас не просто множество символов, а часть текста. Вот практический прием пропуска символов до данного фрагмента текста: (?:(?!$tel).)*$tel Мы каждый раз в цикле проверяем, находится ли в текущей позиции фрагмент текста $tel, и если нет, то берем следующий символ точкой. А после этого цикла должен идти текст $tel. Такой цикл хотя и медлителен, но он гарантирует, что мы не проскочим искомого фрагмента текста. Вся программа теперь выглядит так: #!/usr/bin/perl -w use strict; use locale; my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $tel='тел\.\s+[\d-]+'; my $n=2; $s =~ s/(тел\.\s+)[\d-]+ # ищем слово "тел." + номер (?=(?:(?:(?!$tel).)*$tel){$n} # за которым $n раз идет "тел." + номер (?!.*?$tel) # после чего не встречается "тел." +номер )/${1}9-9999/isx; print $s; Она верно заменяет нужный телефонный номер, а если задан слишком большое значение для $n, которого нет, замена не производится. Обратите внимание, что в подшаблоне (?!.*?$tel) мы не стали заменять конструкцию .*? на новую, т.к. здесь она работает правильно: если впереди есть текст, соответствующий шаблону .*?$tel, то он будет найден и негативная опережающая проверка (?!.*?$tel) вернет ложь. Если вы в данное регулярное выражение вставите код, который печатает текущую позицию поиска, то обнаружите, что здесь происходит много лишних возвратов. Атомарная группировка в этом шаблоне пришлась бы кстати. Например, подшаблон [\d-]+ можно заключить в атомарные скобки, т.к. нет смысла возвращать цифры номера телефона, это не приведет к совпадению. Другой способ заменить n-е от конца совпадение - повторять в цикле while поиск номера телефона, взяв его в захватывающие скобки, и запоминать в массивы значения $-[1] и $+[1]. Затем отсчитать от конца массивов $n, получить смещение начала и конца нужного телефонного номера и воспользоваться функцией substr, которой можно присваивать значение.

<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>

Обсуждение [ RSS ]
  • 1, Faust44 (?), 16:53, 27/10/2008 [ответить]  
  • +/

    А ещё есть функция pos, которая возвращает позицию последнего совпадения и переменные $' $& $', содержащие подстроки до, в и после совпадения соотвественно.
     

     Добавить комментарий
    Имя:
    E-Mail:
    Заголовок:
    Текст:




    Партнёры:
    PostgresPro
    Inferno Solutions
    Hosting by Hoster.ru
    Хостинг:

    Закладки на сайте
    Проследить за страницей
    Created 1996-2024 by Maxim Chirkov
    Добавить, Поддержать, Вебмастеру