Регулярные выражения Perl и их применение

Практическое использование операторов m// и s///

Разбить на страницы
Показывать лекцию целиком

5.1. Поиск отдельных слов

В регулярных выражениях словом называется последовательность символов \w. В множество \w входят все строчные и прописные латинские буквы, десятичные цифры и знак подчерка: [a-zA-Z0-9_]. К этому множеству относятся все символы, считающиеся буквами в той локальной установке, которая используется. Для ActiveState Perl под Windows для этого достаточно написать директиву

use locale;

и все русские буквы (включая буквы ё и Ё) в кодировке Windows-1251 также будут считаться буквами.

Если вы под буквами понимаете что-то другое (например, вы не считаете символ подчерка за букву), то определите соответствующий класс. Если вас интересуют все непробельные символы, то используйте эскейп-последовательность \S.

Эскейп-последовательность \w соответствует ровно одной "букве". Для слов применяйте шаблон \w+. Слово всегда ограничено с обеих сторон мнимыми символами \b - границами слов. Действие этих якорей также зависит от локальной установки операционной системы. Для гарантии того, что совпадение со словом начнется и закончится на его границе, можно применять шаблон \b\w+\b. Чтобы уничтожить ненужные сохраненные состояния, можно также использовать атомарную группировку: \b(?>\w+)\b.

В Perl мнимые символы, которые соответствуют началу слова и его концу, не различаются - оба обозначаются как \b. В некоторых языках программирования для этих символов существуют отдельные якоря. Но можно самому определить такие мнимые символы, исходя из их смысла. Вот якорь, который соответствует началу слова:

(?<!\w)(?=\w)

Слева нет буквы, а справа она есть.

А вот якорь, который соответствует концу слова:

(?<=\w)(?!\w)

Слева есть буква, а справа буквы нет.

Я надеюсь, вы уже понимаете разницу между выражениями (?!\w) и (?=\W). В первом случае справа может и ничего не стоять (конец текста), а второй случай требует, чтобы справа стоял символ, который не является буквой.

5.2. Поиск нечувствительных к регистру совпадений

По умолчанию, модификатор i отключен и сравнение символов чувствительно к регистру. Если вам все равно, большими или маленькими буквами написано то, что вы ищете, то применяйте модификатор i: m/…/i. Бывает так, что что-то отыскивается с учетом регистра символов, а что-то - без такого учета. В этом случае можно использовать интервальное задание квантификаторов или задание их локально для данных группирующих скобок между вопросительным знаком и двоеточием. Рассмотрим такой пример:

/ab(cd(?-i)ef(?i:gh)ij)kl/i;

В этом регулярном выражении глобально действует модификатор i, т.е. сравнение производится без учета регистра символов. По такому правилу отыскиваются символы ab и cd. Затем внутри захватывающих скобок после себя конструкция (?-i) выключает модификатор i, и символы ef отыскиваются с учетом регистра. Внутри скобочной конструкции (?i:gh) опять включается режим сравнения без учета регистра символов, и символы gh отыскиваются без учета регистра. Символы ij стоят после этой конструкции, а в этом месте восстанавливается режим -i, который был задан выражением (?-i), и символы ij отыскиваются с учетом регистра. Символы kl отыскиваются опять без учета регистра, т.к. стоят за закрывающей скобкой, за которой кончается область действия выражения (?-i) и восстанавливается действие глобального модификатора i.

Также заметьте, что модификатор i зависит от локальной установки системы, ведь он действует только на сравнение букв.

5.3. Привязка к началу и концу строк и текста

Мнимые символы \A, \Z и \z не зависят от установок модификаторов и работают всегда одинаково. Не таковы якоря ^ и $, работа которых зависит от модификатора m. В зоне, где модификатор m отключен, якорь ^ соответствует якорю \A, а якорь $ - якорю \Z. Рассмотрим такой пример:

$_="a\r\n";
print '1' if m'a\r$\n';

Здесь я использовал в качестве ограничителей регулярного выражения апострофы, чтобы не было попытки интерполяции специальной переменной $\. Метасимвол $ стоит перед символом \n, который является последним в тексте, и на печать выйдет единица.

Если переставить символ $ перед символом \r:

$_="a\r\n";
print '1' if m'a$\r\n';

то поиск закончится неудачей, потому что здесь $ не стоит непосредственно перед символом \n.

Теперь вернемся к первому примеру и добавим в конце текста какой-нибудь символ:

$_="a\r\nb";
print '1' if m'a\r$\n';

В этом случае поиск вновь завершится неудачно, потому что символ \n здесь уже не последний в тексте. Но если поставить модификатор m, то поиск закончится удачей:

$_="a\r\nb";
print '1' if m'a\r$\n'm;

Теперь такой пример:

$_="a\n";
print '1' if /a\n^/m;

Единица напечатана не будет, потому что якорь ^ не совпадает после символа \n, если он стоит последним в тексте, и модификатор m здесь не поможет. Но если после \n поставить еще символ:

$_="a\n\n";
print '1' if m'a\n^'m;

то совпадение будет найдено. Теперь в последнем примере уберем модификатор m:

$_="a\n\n";
print '1' if /a\n^/;

Совпадения шаблона тексту уже не будет, т.к. без модификатора m якорь ^ совпадает только в начале текста.

5.4. Поиск множественных совпадений

Для поиска множественных совпадений можно применить модификатор g и скалярный контекст. Например:

while ('12345' =~ /\d/g) { print "Найдена еще цифра: $\n" }

Будет напечатано:

Найдена еще цифра: 1
Найдена еще цифра: 2
Найдена еще цифра: 3
Найдена еще цифра: 4
Найдена еще цифра: 5

Иногда для поиска множественных совпадений нужно искать следующее сразу от конца предыдущего совпадения, аналогично привязке к началу текста \A. Для этого есть специальный якорь \G, о котором мы поговорим позже.

Модификатор g и списковый контекст позволяет запомнить сразу все совпадения в тексте:

my @a='12345' =~ /\d/g;
print join ',',@a;

Получаем

1,2,3,4,5

5.5. Поиск n-ного совпадения

С модификатором g происходит поиск всех прототипов шаблона, но что делать, если нужно только вполне определенное, скажем, третье, совпадение? Здесь можно применить оператор цикла while:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
my $count=0;
while ($s =~ /тел\.\s+([\d-]+)/gi)
 { if (++$count == 3)
    { print "Третий номер: $1\n";
    }
 }

На печать выйдет строка

Третий номер: 4-1122

В регулярном выражении мы захватываем в переменную $1 последовательность из цифр и знака минус, которая стоит после фрагмента "тел. ". Модификатор g обеспечивает поиск всех прототипов шаблона друг за другом, сохраняя в переменной $s смещение конца предыдущего совпадения. При очередном успехе оператор поиска возвращает единицу, что заставляет цикл while продолжать работать. После того, как будет найден последний номер телефона, оператор поиска вернет пустую строку, и оператор while закончит выполнение. Также можно было бы после печати третьего телефона поставить оператор last.

Модификатор i нужен, т.к. не все фрагменты "тел." набраны строчными буквами. Но тогда нужна русская локаль, чтобы модификатор i действовал также на русские буквы.

Иначе в этом примере совпадение начнется со второго телефонного номера и на печать ничего не выйдет.

С таким же успехом можно было использовать цикл for:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
for (my $count=0; $s =~ /тел\.\s+([\d-]+)/gi;)
 { if (++$count == 3)
    { print "Третий номер: $1\n";
      last;
    }
 }

Наконец, можно использовать встроенный код Perl, но тогда надо форсировать списочный контекст выполнения оператора поиска, чтобы поиск выполнялся итеративно под действием модификатора g, иначе после нахождения первого телефона поиск закончится:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
my $count=0;
my @a=$s =~ /тел\.\s+([\d-]+)
  (?{
       if (++$count == 3)
        { print "Третий номер: $1\n";
        }
     })/gix;

Это, конечно, не так красиво из-за того, что пришлось присваивать все найденные телефоны массиву.

5.5.1. Замена 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, которой можно присваивать значение.

5.6 Применение якорей, проверки текста до и после шаблона

Для привязки к началу текста можно использовать якорь \A или ^ без модификатора m.

Вот как можно проверить, что текст не начинается с точки:

if ($text =~ /^\./) { print "Текст не должен начинаться с точки!" }

Так можно вставить по одному пробелу в начало каждой строки текста:

$_=<<EOF;
Первая строка.
Вторая строка.
Третья строка.
EOF

s/^/ /gm;

Без модификатора m пробел будет вставлен лишь в начало первой строки.

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

$_=<<EOF;
Первая строка.


Вторая строка.
Третья строка.
EOF

s/^\n//gm;
print $_;

Получаем на выходе:

Первая строка.
Вторая строка.
Третья строка.

Может показаться, что оператор s/^$//gm тоже должен удалить пустые строки, но он не сделает замены. Удалиться должен реальный символ, а не совпадение с началом и концом строки, поэтому сперва он должен быть найден. А что-то вставить в пустую строку таким оператором s вполне возможно.

При преобразовании текста в HTML нужно заменять символы на amp; Это делается просто:

s//amp;/g

ведь амперсанд не является метасимволом в регулярных выражениях.

Предположим, что нам надо найти последнее число в большом тексте. Здесь можно воспользоваться таким приемом: использовать конструкцию .* с модификатором s, которая мгновенно захватит весь текст и выйдет на его конец. Т.к. совпадение с числом не будет найдено, то квантификатор * будет отдавать по одному символу, пока не отыщется совпадение. Как будто все просто. Попробуем такой пример:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/.*(\d+)/s;
print $1;

На печать выходит лишь цифра девять: 9. Удивлены? Квантификатор вернул несколько символов, и текущая позиция в тексте оказалась на цифре 9. Этого достаточно для совпадения шаблона \d+. Чтобы получить правильный результат, надо потребовать, чтобы перед подшаблоном \d+ не было цифры:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/.*(?<!\d)(\d+)/s;
print $1;

Печатает 789.

Чтобы привязать шаблон к концу текста, можно воспользоваться якорями \z (для привязки к истинному концу текста) или \Z ( $ без модификатора m ):

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789
EOF

/(\d+)$/;
print $1;

Печатает 789. Как видим, привязка к концу текста нашла последнее число без захвата всего текста и без заглядывания назад. Но если после последнего числа могут быть символы, то регулярное выражение требуется усложнить:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/(\d+)\D*$/;
print $1;

Опять получаем 789, но поиск будет медленнее из-за увеличивающегося числа проверок.

5.6.1. Как укоротить длинные URL и длинные слова?

У вебмастеров иногда возникает задача укоротить длинные URL, которые вводят в форму участники форумов. Из-за длинных ссылок расползается дизайн страниц. Предположим, кто-то отослал на форум такой текст:

Рекомендую посетить: http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/

На самом деле ссылка может оказаться еще длинее. В HTML-коде страницы форума эта ссылка должна появиться в виде URL:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" 
      target="_blank">
http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/</a>

Внутри тега a ссылку, естественно, обрезать не надо, тем более, что в видимом тексте ее не будет, а вот то, что стоит перед тегом </a>, будет видно, и если это "слово" будет слишком длинным, то это может испортить дизайн. Надо после 50-го символа такой ссылки вставить три точки и получить такой текст:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" 
       target="_blank">
 http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.co…</a>

Другие длинные слова (не ссылки) надо также урезать. Мало ли что может ввести пользователь… Я нашел такое решение:

s/(\A|\s)(?!href=")(\S{50})\S+/$1$2.../g;

Предполагаем, что текст находится в переменной $_. Регулярное выражение начинает поиск от начала текста и каждого пробельного символа. Если после этого не стоит фрагмент href=, то проверяется, есть ли после этого "слово" длиной 50 символов, после которого стоит непробельный символ. Если да, то происходит замена всего, что найдено на $1 - пробельный символ перед длинным "словом", 50 символов с начала этого "слова" на $1, эти же 50 символов с начала длинного "слова", а последующие непробельные символы заменяются на три точки. Если бы в начале шаблона вместо (\A|\s) стояло просто (\s), то длинное "слово", стоящее в самом начале текста, не было бы укорочено.

Вот еще один вариант с использованием кода Perl в части замены:

s/((\S{50})\S+)/index($1,'href=') > -1 ? $1 : "$2..."/ge;

Ищем 50 непробельных символов, сразу за которыми есть хотя бы один непробельный символ. Эти 50 символов берем в переменную $2, а все длинное "слово" берем в переменную $1. В части замены проверяем, есть ли фрагмент текста href= в перемнной $1.

Если он есть, то заменяем найденное на $1, т.е. на себя, а если нет, то заменяем найденное на текст $2, за которым идут три точки.

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

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

$_=' <pppp>' x 13000;
$_.='<table>a';
if (/[^\s<>](?![^<>]*>)/g)
 { print "1\n".pos;
 } else
  { print "0\n";
  }

Вначале мы создаем длинную строку (больше 90000 символов) из тегов, в конец которой вставляем букву a вне тегов. В регулярном выражении мы ищем символ, отличный от пробельного и символов < и >, который не находится внутри тега. "Не находится внутри тега" означает, что сразу после этого символа не должно быть текста, соответствующего шаблону [^<>]*>. В случае нахождения такого символа программа печатает единицу и смещение этого символа от начала текста.

Вот другое решение, которое в отличие от первого настроено быстро пропускать все символы, которые нас не интересуют, т.е. пробельные и теги <> вместе с их содержимым:

/\A(?>(?:(?>\s*)<(?>[^>]*)>)*)\S/g

Оно кажется сложным из-за наличия трех атомарных группировок. Вот этот же шаблон без них:

/\A(?:\s*<[^>]*>)*\S/g

Внутри скобок мы пропускаем последовательности пробельных символов \s* и конструкции <[^>]*>. Обратите внимание, что между этими подшаблонами излишне ставить знак альтернативы |. Вначале стоит привязка \A, чтобы поиск не начался внутри тега.

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

Страницы:

5.1. Поиск отдельных слов

В регулярных выражениях словом называется последовательность символов \w. В множество \w входят все строчные и прописные латинские буквы, десятичные цифры и знак подчерка: [a-zA-Z0-9_]. К этому множеству относятся все символы, считающиеся буквами в той локальной установке, которая используется. Для ActiveState Perl под Windows для этого достаточно написать директиву

use locale;

и все русские буквы (включая буквы ё и Ё) в кодировке Windows-1251 также будут считаться буквами.

Если вы под буквами понимаете что-то другое (например, вы не считаете символ подчерка за букву), то определите соответствующий класс. Если вас интересуют все непробельные символы, то используйте эскейп-последовательность \S.

Эскейп-последовательность \w соответствует ровно одной "букве". Для слов применяйте шаблон \w+. Слово всегда ограничено с обеих сторон мнимыми символами \b - границами слов. Действие этих якорей также зависит от локальной установки операционной системы. Для гарантии того, что совпадение со словом начнется и закончится на его границе, можно применять шаблон \b\w+\b. Чтобы уничтожить ненужные сохраненные состояния, можно также использовать атомарную группировку: \b(?>\w+)\b.

В Perl мнимые символы, которые соответствуют началу слова и его концу, не различаются - оба обозначаются как \b. В некоторых языках программирования для этих символов существуют отдельные якоря. Но можно самому определить такие мнимые символы, исходя из их смысла. Вот якорь, который соответствует началу слова:

(?<!\w)(?=\w)

Слева нет буквы, а справа она есть.

А вот якорь, который соответствует концу слова:

(?<=\w)(?!\w)

Слева есть буква, а справа буквы нет.

Я надеюсь, вы уже понимаете разницу между выражениями (?!\w) и (?=\W). В первом случае справа может и ничего не стоять (конец текста), а второй случай требует, чтобы справа стоял символ, который не является буквой.

5.2. Поиск нечувствительных к регистру совпадений

По умолчанию, модификатор i отключен и сравнение символов чувствительно к регистру. Если вам все равно, большими или маленькими буквами написано то, что вы ищете, то применяйте модификатор i: m/…/i. Бывает так, что что-то отыскивается с учетом регистра символов, а что-то - без такого учета. В этом случае можно использовать интервальное задание квантификаторов или задание их локально для данных группирующих скобок между вопросительным знаком и двоеточием. Рассмотрим такой пример:

/ab(cd(?-i)ef(?i:gh)ij)kl/i;

В этом регулярном выражении глобально действует модификатор i, т.е. сравнение производится без учета регистра символов. По такому правилу отыскиваются символы ab и cd. Затем внутри захватывающих скобок после себя конструкция (?-i) выключает модификатор i, и символы ef отыскиваются с учетом регистра. Внутри скобочной конструкции (?i:gh) опять включается режим сравнения без учета регистра символов, и символы gh отыскиваются без учета регистра. Символы ij стоят после этой конструкции, а в этом месте восстанавливается режим -i, который был задан выражением (?-i), и символы ij отыскиваются с учетом регистра. Символы kl отыскиваются опять без учета регистра, т.к. стоят за закрывающей скобкой, за которой кончается область действия выражения (?-i) и восстанавливается действие глобального модификатора i.

Также заметьте, что модификатор i зависит от локальной установки системы, ведь он действует только на сравнение букв.

5.3. Привязка к началу и концу строк и текста

Мнимые символы \A, \Z и \z не зависят от установок модификаторов и работают всегда одинаково. Не таковы якоря ^ и $, работа которых зависит от модификатора m. В зоне, где модификатор m отключен, якорь ^ соответствует якорю \A, а якорь $ - якорю \Z. Рассмотрим такой пример:

$_="a\r\n";
print '1' if m'a\r$\n';

Здесь я использовал в качестве ограничителей регулярного выражения апострофы, чтобы не было попытки интерполяции специальной переменной $\. Метасимвол $ стоит перед символом \n, который является последним в тексте, и на печать выйдет единица.

Если переставить символ $ перед символом \r:

$_="a\r\n";
print '1' if m'a$\r\n';

то поиск закончится неудачей, потому что здесь $ не стоит непосредственно перед символом \n.

Теперь вернемся к первому примеру и добавим в конце текста какой-нибудь символ:

$_="a\r\nb";
print '1' if m'a\r$\n';

В этом случае поиск вновь завершится неудачно, потому что символ \n здесь уже не последний в тексте. Но если поставить модификатор m, то поиск закончится удачей:

$_="a\r\nb";
print '1' if m'a\r$\n'm;

Теперь такой пример:

$_="a\n";
print '1' if /a\n^/m;

Единица напечатана не будет, потому что якорь ^ не совпадает после символа \n, если он стоит последним в тексте, и модификатор m здесь не поможет. Но если после \n поставить еще символ:

$_="a\n\n";
print '1' if m'a\n^'m;

то совпадение будет найдено. Теперь в последнем примере уберем модификатор m:

$_="a\n\n";
print '1' if /a\n^/;

Совпадения шаблона тексту уже не будет, т.к. без модификатора m якорь ^ совпадает только в начале текста.

5.4. Поиск множественных совпадений

Для поиска множественных совпадений можно применить модификатор g и скалярный контекст. Например:

while ('12345' =~ /\d/g) { print "Найдена еще цифра: $\n" }

Будет напечатано:

Найдена еще цифра: 1
Найдена еще цифра: 2
Найдена еще цифра: 3
Найдена еще цифра: 4
Найдена еще цифра: 5

Иногда для поиска множественных совпадений нужно искать следующее сразу от конца предыдущего совпадения, аналогично привязке к началу текста \A. Для этого есть специальный якорь \G, о котором мы поговорим позже.

Модификатор g и списковый контекст позволяет запомнить сразу все совпадения в тексте:

my @a='12345' =~ /\d/g;
print join ',',@a;

Получаем

1,2,3,4,5

5.5. Поиск n-ного совпадения

С модификатором g происходит поиск всех прототипов шаблона, но что делать, если нужно только вполне определенное, скажем, третье, совпадение? Здесь можно применить оператор цикла while:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
my $count=0;
while ($s =~ /тел\.\s+([\d-]+)/gi)
 { if (++$count == 3)
    { print "Третий номер: $1\n";
    }
 }

На печать выйдет строка

Третий номер: 4-1122

В регулярном выражении мы захватываем в переменную $1 последовательность из цифр и знака минус, которая стоит после фрагмента "тел. ". Модификатор g обеспечивает поиск всех прототипов шаблона друг за другом, сохраняя в переменной $s смещение конца предыдущего совпадения. При очередном успехе оператор поиска возвращает единицу, что заставляет цикл while продолжать работать. После того, как будет найден последний номер телефона, оператор поиска вернет пустую строку, и оператор while закончит выполнение. Также можно было бы после печати третьего телефона поставить оператор last.

Модификатор i нужен, т.к. не все фрагменты "тел." набраны строчными буквами. Но тогда нужна русская локаль, чтобы модификатор i действовал также на русские буквы.

Иначе в этом примере совпадение начнется со второго телефонного номера и на печать ничего не выйдет.

С таким же успехом можно было использовать цикл for:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
for (my $count=0; $s =~ /тел\.\s+([\d-]+)/gi;)
 { if (++$count == 3)
    { print "Третий номер: $1\n";
      last;
    }
 }

Наконец, можно использовать встроенный код Perl, но тогда надо форсировать списочный контекст выполнения оператора поиска, чтобы поиск выполнялся итеративно под действием модификатора g, иначе после нахождения первого телефона поиск закончится:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122';
my $count=0;
my @a=$s =~ /тел\.\s+([\d-]+)
  (?{
       if (++$count == 3)
        { print "Третий номер: $1\n";
        }
     })/gix;

Это, конечно, не так красиво из-за того, что пришлось присваивать все найденные телефоны массиву.

5.5.1. Замена 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, которой можно присваивать значение.

5.6 Применение якорей, проверки текста до и после шаблона

Для привязки к началу текста можно использовать якорь \A или ^ без модификатора m.

Вот как можно проверить, что текст не начинается с точки:

if ($text =~ /^\./) { print "Текст не должен начинаться с точки!" }

Так можно вставить по одному пробелу в начало каждой строки текста:

$_=<<EOF;
Первая строка.
Вторая строка.
Третья строка.
EOF

s/^/ /gm;

Без модификатора m пробел будет вставлен лишь в начало первой строки.

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

$_=<<EOF;
Первая строка.


Вторая строка.
Третья строка.
EOF

s/^\n//gm;
print $_;

Получаем на выходе:

Первая строка.
Вторая строка.
Третья строка.

Может показаться, что оператор s/^$//gm тоже должен удалить пустые строки, но он не сделает замены. Удалиться должен реальный символ, а не совпадение с началом и концом строки, поэтому сперва он должен быть найден. А что-то вставить в пустую строку таким оператором s вполне возможно.

При преобразовании текста в HTML нужно заменять символы на amp; Это делается просто:

s//amp;/g

ведь амперсанд не является метасимволом в регулярных выражениях.

Предположим, что нам надо найти последнее число в большом тексте. Здесь можно воспользоваться таким приемом: использовать конструкцию .* с модификатором s, которая мгновенно захватит весь текст и выйдет на его конец. Т.к. совпадение с числом не будет найдено, то квантификатор * будет отдавать по одному символу, пока не отыщется совпадение. Как будто все просто. Попробуем такой пример:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/.*(\d+)/s;
print $1;

На печать выходит лишь цифра девять: 9. Удивлены? Квантификатор вернул несколько символов, и текущая позиция в тексте оказалась на цифре 9. Этого достаточно для совпадения шаблона \d+. Чтобы получить правильный результат, надо потребовать, чтобы перед подшаблоном \d+ не было цифры:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/.*(?<!\d)(\d+)/s;
print $1;

Печатает 789.

Чтобы привязать шаблон к концу текста, можно воспользоваться якорями \z (для привязки к истинному концу текста) или \Z ( $ без модификатора m ):

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789
EOF

/(\d+)$/;
print $1;

Печатает 789. Как видим, привязка к концу текста нашла последнее число без захвата всего текста и без заглядывания назад. Но если после последнего числа могут быть символы, то регулярное выражение требуется усложнить:

$_=<<EOF;
Первая строка 123. 
Вторая строка 456.
Третья строка 789.
EOF

/(\d+)\D*$/;
print $1;

Опять получаем 789, но поиск будет медленнее из-за увеличивающегося числа проверок.

5.6.1. Как укоротить длинные URL и длинные слова?

У вебмастеров иногда возникает задача укоротить длинные URL, которые вводят в форму участники форумов. Из-за длинных ссылок расползается дизайн страниц. Предположим, кто-то отослал на форум такой текст:

Рекомендую посетить: http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/

На самом деле ссылка может оказаться еще длинее. В HTML-коде страницы форума эта ссылка должна появиться в виде URL:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" 
      target="_blank">
http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/</a>

Внутри тега a ссылку, естественно, обрезать не надо, тем более, что в видимом тексте ее не будет, а вот то, что стоит перед тегом </a>, будет видно, и если это "слово" будет слишком длинным, то это может испортить дизайн. Надо после 50-го символа такой ссылки вставить три точки и получить такой текст:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" 
       target="_blank">
 http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.co…</a>

Другие длинные слова (не ссылки) надо также урезать. Мало ли что может ввести пользователь… Я нашел такое решение:

s/(\A|\s)(?!href=")(\S{50})\S+/$1$2.../g;

Предполагаем, что текст находится в переменной $_. Регулярное выражение начинает поиск от начала текста и каждого пробельного символа. Если после этого не стоит фрагмент href=, то проверяется, есть ли после этого "слово" длиной 50 символов, после которого стоит непробельный символ. Если да, то происходит замена всего, что найдено на $1 - пробельный символ перед длинным "словом", 50 символов с начала этого "слова" на $1, эти же 50 символов с начала длинного "слова", а последующие непробельные символы заменяются на три точки. Если бы в начале шаблона вместо (\A|\s) стояло просто (\s), то длинное "слово", стоящее в самом начале текста, не было бы укорочено.

Вот еще один вариант с использованием кода Perl в части замены:

s/((\S{50})\S+)/index($1,'href=') > -1 ? $1 : "$2..."/ge;

Ищем 50 непробельных символов, сразу за которыми есть хотя бы один непробельный символ. Эти 50 символов берем в переменную $2, а все длинное "слово" берем в переменную $1. В части замены проверяем, есть ли фрагмент текста href= в перемнной $1.

Если он есть, то заменяем найденное на $1, т.е. на себя, а если нет, то заменяем найденное на текст $2, за которым идут три точки.

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

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

$_=' <pppp>' x 13000;
$_.='<table>a';
if (/[^\s<>](?![^<>]*>)/g)
 { print "1\n".pos;
 } else
  { print "0\n";
  }

Вначале мы создаем длинную строку (больше 90000 символов) из тегов, в конец которой вставляем букву a вне тегов. В регулярном выражении мы ищем символ, отличный от пробельного и символов < и >, который не находится внутри тега. "Не находится внутри тега" означает, что сразу после этого символа не должно быть текста, соответствующего шаблону [^<>]*>. В случае нахождения такого символа программа печатает единицу и смещение этого символа от начала текста.

Вот другое решение, которое в отличие от первого настроено быстро пропускать все символы, которые нас не интересуют, т.е. пробельные и теги <> вместе с их содержимым:

/\A(?>(?:(?>\s*)<(?>[^>]*)>)*)\S/g

Оно кажется сложным из-за наличия трех атомарных группировок. Вот этот же шаблон без них:

/\A(?:\s*<[^>]*>)*\S/g

Внутри скобок мы пропускаем последовательности пробельных символов \s* и конструкции <[^>]*>. Обратите внимание, что между этими подшаблонами излишне ставить знак альтернативы |. Вначале стоит привязка \A, чтобы поиск не начался внутри тега.

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

Вернуться к учебному плану