В этой лекции будет кратко рассказано о мощном средстве обработки текста - регулярных выражениях, эффективной поддержкой которых всегда славился язык Perl. Регулярные выражения - это отдельный язык для работы с текстовой информацией, который встроен в язык Perl так, что пользоваться ими можно легко и удобно.
Цель лекции: познакомиться с правилами описания регулярных выражений и со средствами работы с ними в Perl. Научиться применять регулярные выражения для поиска, извлечения и замены текстовой информации.
Когда некоторые говорят, что программы на Perl похожи на бессмысленную кучу символьного мусора, то это впечатление, скорее всего, возникло от вида какого-нибудь длинного регулярного выражения, а они действительно могут выглядеть как загадочный или бессмысленный набор символов, например:
m/]+?HREF\s*=\s*["']?([^'" >]+?)['"]?\s*>/ig
(Это всего-навсего шаблон для поиска гиперссылок в HTML-странице.) Но в этой лекции вы узнаете, что регулярные выражения - совсем не ужасные и отнюдь не хаотичные, а наоборот, очень даже логичные и упорядоченные, что употреблять их не так уж сложно, а записывать их можно вполне наглядным способом. Как сказал Джеффри Фридл в своей знаменитой книге, переведенной на русский язык: "Регулярные выражения также можно сравнить с иностранным языком - когда вы начинаете изучать язык, он перестает казаться белибердой".
Начнем с того, что регулярные выражения (regular expression, сокращенно - regexp, regex или RE) - это отдельный язык описания образцов для обработки текста, не имеющий непосредственного отношения к Perl. Регулярные выражения использовались в Unix задолго до создания Perl, а сейчас библиотеки для работы с ними имеются в C++, C#, Java, JavaScript, PHP, Python, Ruby, Visual Basic и других языках. Поддержка регулярных выражений есть в некоторых редакторах, почтовых программах и системах управления базами данных. Другое дело, что широкое распространение Perl в свое время сделало регулярные выражения популярными на разных платформах. А в ходе развития языка Perl была отточена система обозначений для регулярных выражений, ставшая фактическим стандартом. Многие считают, что благодаря Perl регулярные выражения из математической теории превратились в рабочий инструмент тысяч и тысяч программистов. Это произошло потому, что в Perl механизмы работы с регулярными выражениями встроены в ядро языка, поэтому применять их естественно, легко и удобно. А благодаря эффективной реализации "движка" регулярных выражений, в Perl они обрабатываются чрезвычайно быстро. Регулярные выражения выполняют львиную долю работ по обработке текстовой информации и используются в Perl несколькими способами:
[x]. для поиска в тексте строк по определенному образцу;
[x]. для разделения текста на части по указанному набору разделителей;
[x]. для извлечения из строки подстрок, соответствующих заданному шаблону;
[x]. для замены в тексте найденных соответствий на новые значения.
Основная идея регулярных выражений состоит в нахождении в тексте соответствия определенному образцу, который может задаваться литералом или шаблоном. Вся текстовая строка считается соответствующей образцу, если ему соответствует какая-то ее часть.
Наверное, чаще всего регулярные выражения используются в операции сопоставления (match operator), которая проверяет, соответствует ли текст указанному образцу. Образец (pattern) - это символьная последовательность для сопоставления, записанная в специальной нотации. Простейший образец - это строковый литерал, представляющий собой последовательность символов, которая будет отыскиваться в тексте. В скалярном контексте операция сопоставления возвращает '1', если образец в строке найден, и пустую строку "', если соответствие образцу не найдено. Для указания, к какой строке применить операцию сопоставления, используется операция привязки =~ к строке:
'В строке образец есть' =~ /образец/; # образец найден
Обычно поиск образца выполняется с учетом регистра, но можно игнорировать регистр при сопоставлении строки с образцом, если в операции сопоставления задать модификатор /i (ignore case). Для корректной обработки национальных букв должна быть включена прагма use locale. Например:
use locale;
'В строке образец есть' =~ /Образец/; # образец НЕ найден!
'В строке образец есть' =~ /Образец/i; # образец найден
Результат операции сопоставления в тексте можно присвоить скалярной переменной или использовать в любой из условных конструкций, например:
$text = 'Черный кот в темной комнате'; # ищем в этом тексте
$found = $text =~ /кот/; # в $found будет '1'
print 'Кошки нет!' unless $text =~ /кошка/; # вернет ''
Последнее предложение можно переписать, применив операцию отрицательной привязки к строке (!~), которая инвертирует (меняет на обратный) результат операции сопоставления:
print 'Кошки нет!' if $text !~ /кошка/;# вернет '1'
Если операция привязки к строке не используется, образец отыскивается в переменной по умолчанию $_. Выражение перед поиском интерполируется, поэтому весь образец поиска или его часть может содержаться в переменной. Например:
$_ = 'Счастье - это когда тебя понимают.'; # переменная поиска
$pattern = 'Счастье'; # образец для сопоставления
print "$pattern найдено!" if /$pattern/;
В составе образца поиска могут применяться не только переменные, но и escape-последовательности, известные нам из лекции 2, например:
print 'В строке обнаружена табуляция' if $string =~ m{\t};
Для успешного сопоставления строки образцу достаточно найти в строке первое совпадение. В этом примере образец совпадет с началом подстроки 'которого':
$text = 'У которого из котов зеленые глаза?'; # ищем здесь
$any = $text =~ /кот/; # образец совпал с 'которого'
Чтобы найти именно подстроку 'кот', перед которой стоит пробел, нужно задать более точный образец для сопоставления:
$cat = $text =~ / кот/; # образец совпадет с ' кот'
В операции сопоставления программист может задавать ограничители для образца: в этом случае перед ограничителями указывается буква m// (Операцию сопоставления часто именно так и называют: операция m//.) В качестве ограничителей могут выступать различного вида скобки или парные небуквенные символы, например:
m($pattern) m{$pattern} m[$pattern] m<$pattern>
m|$pattern| m!$pattern! m"$pattern" m#$pattern#
Задать собственные ограничители бывает особенно полезно, когда в шаблон поиска входит наклонная черта. Из двух приведенных вариантов второй смотрится гораздо понятнее:
/\/usr\/bin\/perl/
m{/usr/bin/perl}
Недаром обилие левых и правых наклонных черт в первом варианте называют "ученическим синдромом зубочисток" (LTS - Learning Toothpick Syndrome). В приводимых до сих пор примерах операцию сопоставления с литералом в качестве образца вполне можно заменить вызовом функции index(). Самое интересное начинается тогда, когда в образце поиска применяются метасимволы для сопоставления с шаблоном.
Очень часто требуется искать в тексте не конкретные строки, а символьные последовательности, определенные приблизительно: "число в скобках", "четвертое слово с начала строки", "список из пар имя = значение, разделенных запятыми" и тому подобное. В таких случаях в качестве аргумента поиска задается шаблон, который описывает такую последовательность. Шаблон - это образец, в котором, помимо литеральных значений, содержатся метасимволы. Метасимволы (metacharacter) - это знаки, имеющие специальное значение при записи образцов. Вот какие метасимволы применяются при записи регулярных выражений:
{} [] () ^ $ . | * + ? \
При необходимости включить в образец поиска один из этих знаков не как метасимвол, а как обыкновенный символ, нужно отменить его особое значение ("экранировать"), поставив перед ним обратную косую черту (backslash):
$text =~ m"\." # содержится ли в тексте точка?
Как метасимвол точка обозначает в регулярном выражении один любой символ, кроме знака перевода новой строки (\n). Например, для поиска похожих слов можно составить такой шаблон:
/само.а./ # соответствуют: 'самовар', 'самокат', 'самосад'...
# НЕ соответствуют: 'самолюб', 'самогон', 'самоход'...
В регулярном выражении можно задать несколько вариантов образца, любой из которых будет считаться соответствием строки образцу. Варианты образца - это набор возможных альтернатив, разделенных знаком "вертикальная черта" ('|'), который называется "метасимвол альтернатив" (alternation metacharacter). Поиск считается успешным, если найдено соответствие любой из альтернатив, например:
$text = 'Черная кошка в темной комнате'; # будем искать здесь
print "Нашли кошку!" if $text =~ /кот|кошка|котенок/;
Сравнение текста с вариантами образца выполняется слева направо, поэтому, если начало альтернатив совпадает, более длинную альтернативу нужно помещать в начало списка вариантов. Иначе всегда будет найдена более короткая. Значит шаблон в предыдущем примере правильнее записать в виде /котенок|кот|кошка/, чтобы в первую очередь поискать котенка, а затем - кота:
$text = 'Черный котенок в темной комнате'; # ищем здесь
print "Нашли котенка!" if $text =~ /кот.нок|кот|кошка/;
Чтобы сделать образец более универсальным, в первой альтернативе литерал заменен на шаблон с метасимволом "точка", чтобы находились соответствия слову "котенок" в любом написании - через "е" и через "ё". Часто применение регулярного выражения с альтернативами выглядит гораздо изящнее, чем длинное условное выражение:
return if $command =~ /exit|quit|stop|bye/i;
Если в образце после выбора из нескольких альтернатив применяются другие шаблоны или литералы, то конструкцию выбора нужно заключить в круглые группирующие скобки. Например:
$lotr =~ /(Bilbo|Frodo) Baggins/; # один из хоббитов
С помощью метасимволов можно обозначить в шаблоне один символ из заданного набора. Для этого нужно определить класс символов, указав в квадратных скобках набор символов, включаемых в класс. Классы символов похожи на шаблон с вариантами, в котором альтернативами могут быть только отдельные символы. Ради примера запишем шаблон для слов, отличающихся первой буквой из указанного набора:
/[вклрт]от/ # соответствуют: 'вот','кот','лот','рот','тот'
Вот пример шаблона с несколькими классами символов, каждый из которых представляет одну букву в последовательности из четырех символов:
/[мс][ул][хо][ан]/ # соответствуют: 'муха', 'слон'
# а также: 'суоа', 'млхн', 'слоа' и так далее
В классе символов вместо перечисления можно указывать диапазон от начального до конечного символа, разделенных минусом:
[0-9] вместо [0123456789]
[A-Z] вместо [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Указывая несколько диапазонов в одном классе, запишем шаблон для шестнадцатеричной цифры:
/[0-9a-fA-F]/# соответствуют: '5', 'b', 'D' и так далее
Чтобы включить в символьный класс знак '-', нужно поместить его в начале или в конце перечисленных в классе символов или экранировать обратной чертой. Помещенные в символьный класс, все метасимволы (кроме ']') рассматриваются как обычные символы. Поэтому так могут выглядеть шаблоны для поиска знака препинания или одной из скобок:
[-.,;:!?] # знаки препинания
[()[\]{}] # скобки: \] представляет скобку ']'
Иногда требуется выразить понятие "все, кроме указанных символов": для этого в описании класса символов сразу после открывающей квадратной скобки ставится метасимвол отрицания ('^'). Например, так можно записать шаблоны для "любого символа, кроме знаков препинания" или "любого нецифрового символа":
[^-.,;:!?] # все, кроме этих знаков препинания
[^0-9] # не цифры
Чтобы включить в символьный класс символ '^', нужно поставить его не первым в списке символов или отменить его специальное значение с помощью символа '\':
[*^] или так: [\^]
Для сокращенной записи классов символов в регулярных выражениях предусмотрены специальные обозначения, состоящие из латинской буквы с обратной косой чертой перед ней. Вот они:
\d - любая десятичная цифра, то есть [0-9]
\D - любой символ, кроме цифры: [^0-9] или [^\d]
\w - символ, пригодный для записи идентификатора:[a-zA-Z0-9_]
\W - противоположность символа \w, то есть [^\w]
\s - пробельный символ: пробел, \t, \n, \r или \f
\S - любой не пробельный символ, то есть [^\s]
С помощью этих метасимволов можно составлять гораздо более интересные образцы. Например, проверим, содержится ли в тексте число из четырех цифр, окруженное любыми пробельными символами:
$text = "Альбом 'Dire Straits'\tГод 1978\tВремя 41:21";
$text =~ m{\s\d\d\d\d\s}; # найдет ' 1978\t'
Записывать несколько метасимволов подряд для указания в шаблоне последовательности из однотипных символов утомительно и неудобно, да и ошибиться при этом легко. Облегчить жизнь составителям регулярных выражений помогают квантификаторы.
Квантификатор (quantifier) - это обозначение числа повторений предыдущего шаблона при поиске соответствия. Количество повторений может задаваться одним или парой десятичных чисел в фигурных скобках:
{n} повторяется точно n раз
{n,} повторяется n и более раз
{n,m} повторяется от n до m раз включительно
Квантификатор, также иногда называемый множителем, указывается сразу после конструкции в шаблоне, которую нужно повторить несколько раз, например:
/\d{5}/ # ровно пять цифр, то есть: \d\d\d\d\d
/\s{1,}/ # один и более пробельных символов
/[A-Z]{1,8}/ # от 1 до 8 заглавных латинских букв
Опишем с применением квантификаторов шаблон для поиска в тексте последовательности, похожей на телефонный номер, в следующем формате:
символ + \+
код страны: не менее 1 цифры \d{1,}
открывающая скобка ( \(
код города: 3 цифры и более \d{3,}
закрывающая скобка ) \)
номер абонента: от 4 до 7 цифр \d{4,7}
Перед знаками "+", "(" и ")" ставится обратная наклонная черта, чтобы они не воспринимались как метасимволы. Вот какое регулярное выражение получится в результате:
m"\+\d{1,}\(\d{3,}\)\d{4,7}"
Для наиболее часто встречающихся квантификаторов предусмотрены удобные односимвольные сокращения:
* повторяется 0 или более раз: то же, что {0,}
? повторяется не более 1 раза: то же, что {0,1}
+ повторяется как минимум 1 раз: то же, что {1,}
Составим регулярное выражение с использованием односимвольных квантификаторов, чтобы найти в тексте "идентификатор, перед которым могут стоять пробельные символы и за которым стоит хотя бы один из перечисленных знаков препинания":
m/\s*\w+[-.,;?]+/ # соответствует, например: ' count--;'
Если квантификатор нужно применить к нескольким шаблонам, то нужно сгруппировать шаблоны, заключив их в круглые скобки. Составим регулярное выражение для поиска IP-адреса, которое находит число, состоящее из одной цифры и более (\d+), за которой может стоять точка (\.?), причем эта последовательность повторяется ровно четыре раза ({4}):
$pattern = '(\d{1,3}\.)\d{1}'; # шаблон для IP-адреса
$text = 'address=208.201.239.36,site=www.perl.com';
$text =~ m/$pattern/; # соответствие: '208.201.239.36'
Программисты шутят: "При составлении шаблона главное, чтобы регулярное выражение соответствовало тому, что нужно, и не соответствовало тому, что не нужно". В следующем примере мы будем искать "более одного символа, за которыми идет буква 'й' и пробел", ожидая, что будет найдено слово 'Какой '. Но нас ожидает неприятный сюрприз:
my $text = 'Какой хороший компакт-диск!';
$text =~ /.+й\s/; # жадный квантификатор
# найдено соответствие: 'Какой хороший '
Это произошло потому, что по умолчанию квантификаторы подразумевают максимальную последовательность символов, соответствующих указанному шаблону. Такое поведение квантификаторов называется "жадным" (greedy quantifier). Чтобы заставить квантификатор вести себя не "жадно", а "лениво" (lazy quantifier), нужно поставить сразу после него символ '?'. Тогда квантификатор будет описывать минимальную последовательность символов, соответствующих образцу. Исправленный с учетом этого образец найдет то, что нужно:
$text =~ /.+?й\s/; # ленивый квантификатор
# найдено соответствие: 'Какой '
Таким же образом можно ограничивать "жадность" и других квантификаторов, заставляя их прекращать поиск как можно раньше, что обычно и требуется в большинстве ситуаций.
Часто нам бывает небезразлично, в каком месте содержимое строки совпадет с шаблоном. Мы бы хотели уточнить: "в начале строки", "в конце слова" и так далее. Для того чтобы более точно задать положение в тексте, где должно быть найдено соответствие, в регулярных выражениях можно указывать так называемые утверждения. Утверждение (assertion) не соответствует какому-либо символу, а совпадает с определенной позицией в тексте. Поэтому их можно воспринимать как мнимые символы нулевого размера. Чаще всего используются следующие утверждения (другие приведены в таблице 8.1):
^ позиция в начале строки
$ позиция в конце строки (или перед \n в конце строки)
\b граница слова: позиция между \w и \W или \W и \w
\B любая позиция, кроме границы слова \b
Вот пример шаблонов поиска, где уточняется, что нужно проверить наличие числа в определенном месте строки:
$log = '20060326 05:55:25 194.67.18.73 ... 200 797';
print "Число в начале\n" if $log =~ /^\d+/;
print "Число в конце\n" if $log =~ /\d+$/;
Утверждение, которое используется для фиксирования части образца относительно положения в строке, иногда называется якорем (anchor). Якори применяются, чтобы указать, в каком именно месте строки нужно искать соответствие образцу.
Когда операция сопоставления находит в строке соответствие указанному регулярному выражению, она присваивает результаты своей работы нескольким специальным переменным:
[x]. в переменную $` помещается часть строки до найденного соответствия;
[x]. в переменную $& помещается часть строки, соответствующая образцу;
[x]. в переменную $' помещается часть строки после найденного соответствия;
[x]. в переменную $+ помещается последнее найденное совпадение для последнего шаблона в скобках.
Если поиск окончился неудачей, то этим переменным новые значения не присваиваются. Посмотрим на примере, что сохранится в этих переменных после поиска такого соответствия:
$htm= "Регулярные выражения";
$htm =~ m|HREF=["'](\S+?)["']>|; # поиск URL сайта
При успешном совпадении с шаблоном в специальные переменные будут помещены такие значения:
$` = '
$& = 'HREF='http://regexp.ru/'>'
$' = 'Регулярные выражения
$+ = 'http://regexp.ru/'
Значениями этих переменных можно пользоваться при успешном сопоставлении с образцом, например:
print $& if $text =~ m/$pattern/; # выведет соответствие
В регулярном выражении можно указать, что при успешном сопоставлении строки с шаблоном найденные соответствия нужно сохранить для дальнейшей обработки. С этой целью запоминаемые части шаблона нужно заключить в круглые скобки. Это также называется захватом значений. Найденные совпадения для всех заключенных в скобки частей шаблона будут доступны через специальные переменные с именами $1, $2 и так далее. Составим регулярное выражение для поиска и сохранения в служебных переменных информации о сайте в том же тексте:
$pattern = q|HREF=["'](\S+?)["']>([^<]+?)|; # шаблон
$htm =~ m/$pattern/; # поиск соответствия в $htm
# в $1 = 'http://regexp.ru/'
# в $2 = 'Регулярные выражения'
Сохраненные совпадения доступны и во время обработки регулярного выражения, но через переменные с именами \1, \2 и так далее. Эти переменные называются обратными ссылками (backreference) на найденные соответствия. Так, например, можно найти два одинаковых слова, стоящих в тексте друг за другом через пробелы (возможно, по ошибке):
my $string = "Уже скоро скоро наступит весна!";
my $pattern = '(\S+)\s+\1';
# (\S+) сохранит значение 'скоро' в \1
$string =~ m/$pattern/; # соответствие: 'скоро скоро'
Операция сопоставления, употребленная в списочном контексте, возвращает список найденных соответствий, для которых было предусмотрено сохранение значений. Поэтому удобно сохранять найденные значения в массиве или в списке скалярных переменных. Например, извлечем из текстовой строки последовательность цифр, похожую на время:
my $text = 'Начало в 12:25:00.'; # строка с данными
my $pattern = '(\d\d):(\d\d):(\d\d)'; # образец для поиска
my @time = $text =~ m/$pattern/; # сохраним в массиве
my ($hh, $mm, $ss) = $text =~ m/$pattern/; # и в списке
Можно находить любое количество соответствий образцу в одной операции сопоставления. Это делается с помощью модификатора глобального поиска.
До сих пор операция сопоставления прекращала работу и возвращала результат, когда находилось первое соответствие строки указанному шаблону. Если для операции сопоставления указать модификатор /g (global), то она будет искать в строке все соответствия образцу, организуя неявный цикл обработки регулярного выражения. Например, так можно найти все числа в строке с помощью одного шаблона:
my @numbers = 'Не 12.5, а 25!' =~ /(\d+)/g; # глобальный поиск
# в @numbers будет (12, 5, 25)
Ранее в этой лекции уже упоминался модификатор /i, устанавливающий поиск с игнорированием разницы между заглавными и строчными буквами. Перечислим модификаторы для операции сопоставления:
[x]. /g - искать в тексте все соответствия образцу (Global);
[x]. /i - искать соответствие образцу без учета регистра букв (case-Insensitive);
[x]. /s - рассматривать текст как одну строку (Single-line);
[x]. /m - рассматривать текст как многострочный (Multi-line) с учетом \n ;
[x]. /o - один раз откомпилировать регулярное выражение (Once);
[x]. /x - использовать расширенный синтаксис регулярных выражений (eXtended).
Из всех модификаторов, пожалуй, самый интересный - последний, который позволяет записывать регулярные выражения в структурированном и понятном для человека виде и даже сопровождать комментариями! Так, например, можно более понятно и красиво переписать регулярное выражение, приведенное в начале лекции:
m/ # начало регулярного выражения