2009 г.
Учимся регулярно выражаться
Илья Лебедев,
Debugger.ru
Примечание редакции. Существует много диалектов языка регулярных выражений. Выражения из этой статьи используют синтаксис, принятый в Perl, в том числе недоступные в других диалектах функции. Похожий диалект вне языка Perl известен как "Perl-совместимые регулярные выражения" (PCRE, Perl-Compatible Regular Expressions).
В большинстве текстовых редакторов можно провести поиск по слову, его
части и учитывать регистр. Чаще всего, этих возможностей достаточно,
поскольку приходится обрабатывать относительно простые тексты - мозг
человека сразу оценивает полученное совпадение и принимает решения.
Рано или поздно наступает момент, когда человеческих ресурсов не
хватает для обработки всей поступающей информации, или требуется ввести
полностью автоматизированную обработку данных, например, для публикации
новостей из разных источников на своём сайте. Здесь вступают в работу
автоматизированные системы, в которых нашли широкое применение
регулярные выражения. Их использование позволяет в десятки раз
уменьшить объём кода, необходимого для обработки текстов.
При первом знакомстве регулярные выражения вызывают самые различные
реакции, но общее в них то, что человека отталкивает сложность
понимания смысла этих «высказываний». Данная статья призвана объяснить,
как прочитать выражение и понять его смысл.
- оператор – символы, используемые в регулярных выражениях; они управляют поиском, но не участвуют в совпадении.
- совпадение – один или больше символов и операторов регулярного выражения, которые совпали с проверяемым текстом.
- квантификатор – оператор регулярного выражения, определяющий количество экземпляров повторяющегося совпадения.
- жадный (другое определение - «максимальный»)
– захватывает все доступные совпадения, после чего может их возвратить,
если потребуется операторам справа. По умолчанию, все квантификаторы -
«жадные».
- не жадный (другое определение - «минимальный») – захватывает совпадение только в том случае, если нет совпадения справа.
- захватывающий – жадный, никогда не возвращает захваченные совпадения.
- последовательное совпадение – последовательность одинаковых совпадений.
- символы
- <символ> – печатный символ: буквы, цифры, знаки препинания и т.д.
- . – произвольный символ
- подстрока – литеральный (такой, в котором нет операторов) набор символов.
- управление совпадением (квантификаторы)
- * – ноль или больше последовательных совпадений
- + – одно или больше последовательных совпадений
- ? – ноль или одно совпадение
- *? – не жадные ноль или больше последовательных совпадений
- +? – не жадные одно или больше последовательных совпадений
- *+ – захватывающие ноль или больше последовательных совпадений
- ++ – захватывающие одно или больше последовательных совпадений
- {n1,n2} – от n1 до n2 последовательных совпадений
- {n} – ровно n совпадений
- формирование последовательности – совпасть может только вся последовательность
- ( – начнём захватывать совпадения в последовательность и сохраним её в памяти (сохраняющие скобки)
- (?: – начнём захватывать совпадения в последовательность, но не сохраняем её (несохраняющие скобки)
- ) – завершим захватывать совпадения.
- | – альтернативная последовательность при отсутствии совпадения с последовательностью слева
- наборы символов – совпадает любой из символов
- [ – откроем набор символов
- ] – закроем набор символов
- - – укажем диапазон символов
- ^ – набор содержит все символы, кроме перечисленных
- позиционная проверка
- (?= – начнём проверку на наличие совпадения справа
- (?! – начнём проверку на отсутствие совпадения справа
- (?< = – начнём проверку на наличие совпадения слева
- (?<! – начнём проверку на отсутствие совпадения слева
- ) – завершим проверку
- ссылки на предыдущие совпадения
- \0..\9 – порядковый номер последовательности в сохраняющих скобках
Так выглядит перевод конструкций регулярных выражений на «человеческий»
язык. В дальнейшем, при разборе примеров, я дам им развернутое
описание.
Проговаривание вслух и запись правил - основная проблема,
возникающая при освоении техники разбора текста с помощью регулярных
выражений. При отсутствии опыта трудно сформулировать словесную запись
правил. Тем не менее - это наиболее эффективный путь составления
регулярных выражений.
Основным правилом при составлении регулярных выражений является их
запись в развёрнутом виде на листе бумаги или на экране. Так легче
всего определить, насколько верно будет обработан текст.
Другое правило заключается в составлении выражения от общего к
частному. При его соблюдении значительно сокращается время написания
выражения и количество ошибок.
Сформулируем условия для успешного составления регулярного выражения
- У выражения должна быть «литературная» форма.
- Словесное описание должно быть логичным.
- Составление выражения должно идти:
- от простого к сложному.
- от общего к частному.
Несмотря на трудоёмкость подобной записи,
она повышает скорость разработки и отладки правил разбора текстов и
эффективность их применения.
Выделить в HTML разметке содержимое определённых блоков с установленными атрибутами:
- абзац (тег <p>) с CSS классом content
- элемент списка (тег <li>) с CSS классом content
<p>Абзац 1</p>
<p class="content">Абзац 2</p>
<ul>
<li>Элемент 1</li>
<li class="content">Элемент 2</li>
</ul>
<p class="content">Абзац 2</p>
<li class="content">Элемент 2</li>
- Составим регулярное выражение для выделения всех тегов.
- Дополним его для выделения только парных тегов.
- Дополним его для выделения только заданных тегов.
- Напишем выражение для поиска пар 'имя_атрибута = «значение»'.
- Дополним основное выражение для выделения заданных тегов с определёнными атрибутами.
- Добавим модификаторы поиска.
Обычно начинают одновременно производить все 6
шагов что вызывает серьезные проблемы при отладке. Я предлагаю
действовать постепенно. Шаг за шагом.
Приведу решение сразу:
#<(p|li)\s+[^>]*?class\s*=\s*(['"])content\2[^>]*>((?:(?!</\1>).)*)</\1>#is
Согласитесь, выглядит оно почище «китайской грамоты». Тем не менее, следуя описанию, Вы увидите, что всё не так уж и сложно.
Итак, начнём:
Запишем правила разбора по-русски:
- Найдём подстроку '<'
- Начнём захватывать символы в последовательность
- Захватим одну или более букву алфавита
- Завершим захватывать совпадения
- Захватим 0 или более символов, не совпадающих с набором символов '>'
- Захватим подстроку '>'
- Начнём захватывать символы в последовательность
- Захватим 0 или более символов, не совпадающих с набором символов '>'
- Завершим захватывать совпадения
Теперь, когда задача точно описана, можно приступить к записи её в виде регулярного выражения:
- <
- (
- \w+
- )
- [^>]*
- >
- (
- [^<]*
- )
У нас получилось следующее выражение:
<(\w+)[^>]*>([^<]*)
Оно имеет 2 недостатка:
- захватывает все теги, а не только парные.
- некорректно отрабатывает вложенные теги.
Запишем правила разбора формальным языком:
- Найдём подстроку '<'
- Начнём захватывать символы в последовательность
- Захватим одну или более букву алфавита
- Завершим захватывать совпадения
- Захватим 0 или более символов, не совпадающих с набором символов '>'
- Захватим подстроку '>'
- Начнём захватывать символы в последовательность
- Начнём захватывать символы в несохраняющую последовательность
- Начнём проверку на отсутствие удачного совпадения справа последовательности из
- '</'
- совпадение найденное на шагах 2-3 (ссылка на последовательность 1)
- '>'
- Завершим проверку.
- Захватим любой символ
- Завершим захватывать совпадения.
- Произведём захват 0 или более раз
- Завершим захватывать совпадения.
- Захватим подстроку '</'
- Захватим совпадение найденное на шагах 2-3 (ссылка на последовательность 1)
- Захватим подстроку '>'
Теперь, когда задача точно описана, можно приступить к записи её в виде регулярного выражения:
- <
- (
- \w+
- )
- [^>]*
- >
- (
- (?:
- (?!
- </
- \1
- >
- )
- .
- )
- *
- )
- </
- \1
- >
Итак, у нас получилось следующее выражение:
<(\w+)[^>]*>((?:(?!</\1>).)*))</\1>
Оно захватывает любые парные теги вместе с содержимым.
Используя регулярное выражение, полученное на предыдущем шаге, мы можем
выделить из текста сразу несколько типов тегов, используя конструкцию
«альтернативная последовательность при отсутствии совпадения слева». В
описании используем термин «альтернативная последовательность».
Добавим выделение из текста всего содержимого абзацев и пунктов списка:
- Найдём подстроку '<'
- Начнём захватывать символы в последовательность
- подстроку 'p'
- Добавим альтернативную последовательность
- подстроку 'li'
- Завершим захватывать совпадения
- Произведём проверку на удачное совпадение справа набора символов '\s>'
- Захватим 0 или более символов, не совпадающих с набором символов '>'
- Захватим подстроку '>'
- Начнём захватывать символы в последовательность
- Начнём захватывать символы в несохраняющую последовательность
- Начнём проверку на отсутствие удачного совпадения справа последовательности из
- '</'
- совпадение найденное на шагах 2-3 (ссылка на последовательность 1)
- '>'
- Завершим проверку
- Захватим любой символ
- Завершим захватывать совпадения
- Захватим последовательность 0 или более раз
- Завершим захватывать совпадения
- Захватим подстроку '</'
- Захватим совпадение найденное на шагах 2-3 (ссылка на последовательность 1)
- Захватим подстроку '>'
Пункты 7 и 8 были добавлены для того, чтобы
выражение не захватывало теги, начало которых совпадает с выделяемыми
тегами. Например, чтобы при поиске тега <p> не были захвачены теги <param>.
Переводим её в операторы регулярного выражения:
- <
- (
- p
- |
- li
- )
- (?=[\s>])
- [^>]*
- >
- (
- (?:
- (?!
- </
- \1
- >
- )
- .
- )
- *
- )
- </
- \1
- >
Новое регулярное выражение:
<(p|li)(?=[\s>])[^>\w]*>((?:(?!</\1>).)*))</\1>
Теперь в тексте будут выделены только теги p и li и всё их содержимое.
- справа от имени атрибута должен быть пробел
- слева от значения должен быть пробел или закрывающая тег скобка
- значение должно быть заключено в одинарные или двойные кавычки
- между знаком равенства, именем атрибута и его значением могут быть пробелы
Опишем задачу формальным языком:
- Найдём 1 или больше символов \s
- Захватим 1 или больше символов \w
- Захватим 0 или больше символов \s
- Захватим подстроку '='
- Захватим 0 или больше символов \s
- Начнём захватывать символы в последовательность
- Захватим подстроку из набора '»'
- Завершим захватывать совпадения.
- Захватим 0 или больше символов, не входящих в набор символов, найденный на шагах 6-7 (ссылка на последовательность 1)
- Захватим символ, найденный на шагах 6-7 (ссылка на последовательность 1)
Переводим в операторы регулярного выражения:
- \s+
- \w+
- \s*
- =
- \s*
- (
- ['»]
- )
- [^\1]*
- \1
Получается следующее регулярное выражение:
\s+\w+\s*=\s*(['"])[^\1]*\1
Модифицируем его, чтобы выражение совпадало только с именем атрибута 'class' и его значением 'content':
\s+class\s*=\s*(['"])content\1
Опишем задачу формальным языком:
- Найдём подстроку '<'
- Начнём захватывать символы в последовательность
- Захватим подстроку 'p'
- Добавим альтернативную последовательность
- Захватим подстроку 'li'
- Завершим захватывать совпадения
- Захватим 1 или больше символов \s
- Захватим минимальные 0 или больше символов, не совпадающих с набором символов '>'
- Добавим регулярное выражение с шага 4: class\s*=\s*(['»])content\1
- Захватим 0 или более символов, не совпадающих с набором символов '>'
- Захватим подстроку '>'
- Начнём захватывать символы в последовательность
- Начнём захватывать символы в несохраняющую последовательность
- Начнём проверку на отсутствие удачного совпадения справа последовательности из
- '</'
- совпадение, найденное на шагах 2-3 (ссылка на последовательность 1)
- '>'
- Завершим проверку
- Захватим любой символ
- Завершим захватывать совпадения
- Захватим последовательность 0 или более раз
- Завершим захватывать совпадения
- Захватим подстроку '</'
- Захватим совпадение, найденное на шагах 2-3 (ссылка на последовательность 1)
- Захватим подстроку '>'
Переводим её в операторы регулярного выражения:
- <
- (
- p
- |
- li
- )
- \s+
- [^>]*?
- class\s*=\s*(['»])content\2
- [^>]*
- >
- (
- (?:
- (?!
- </
- \1
- >
- )
- .
- )
- *
- )
- </
- \1
- >
Результирующее выражение:
<(p|li)\s+[^>]*?class\s*=\s*(['"])content\2[^>]*>((?:(?!</\1>).)*)</\1>
Практически во всех языках, где имеется поддержка регулярных выражений,
возможно выбрать ограничители выражения. Самые распространённые это: / / и # #.
В принципе, можно использовать практически любые пары символов, если
это поддерживается интерпретатором. При выборе ограничителей лучше
исходить из того, какие символы присутствуют в регулярном выражении.
Выбирать лучше те, которых в выражении нет. В противном случае придётся
экранировать эти символы, что сделает выражение более запутанным.
В нашем случае стандартные / / не подходят, поскольку они есть внутри регулярного выражения. Поэтому я предлагаю использовать ограничители # #.
Информацию по всем модификаторам поиска я советую смотреть в специальной справочной литературе, например, в документации по PHP и Perl. Здесь же мы используем i - поиск без учёта регистра и s - режим совпадения символа «.» с переводами строк.
#<(p|li)\s+[^>]*?class\s*=\s*(['"])content\2[^>]*>((?:(?!</\1>).)*)</\1>#is
Как видите, эта задача решается достаточно просто. При написании статьи
я её выбрал потому, что на форумах очень часто задают вопрос «как
выбрать содержимое определённого тега» и «как разобрать HTML разметку». Решение перед вами.
Спасибо всем, кто помог мне в написании статьи: