Ключевые слова: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, которой можно присваивать значение.
А ещё есть функция pos, которая возвращает позицию последнего совпадения и переменные $' $& $', содержащие подстроки до, в и после совпадения соотвественно.