Статьи

Радость регулярных выражений [1]

Недавно меня спросили, знаю ли я какие-нибудь хорошие учебники по регулярным выражениям (желательно на PHP). Вопрос пришел от кого-то достаточно умного, чтобы «получить» регулярные выражения, но они не смогли найти доступную помощь.

Большинство руководств по регулярным выражениям, которые я видел, организованы вокруг обучения синтаксису, которое может быстро привести к умственной перегрузке. Примеры обычно вращаются вокруг таких строк, как «aaabbababa»… »- отлично, если вы пишете веб-сканер для шведской поп-музыки , но сбивает с толку всех остальных. И хотя в Интернете есть регулярные выражения копирования и вставки , если вы не знаете, что делаете, их использование может быть хуже, чем вовсе. Они отвечают вашим потребностям? Упс! Следите за дырой в безопасности …

Итак, рассмотрим учебник «Ещё одно регулярное выражение», в котором основное внимание уделяется выполнению (в PHP) и медленному представлению синтаксиса regexp (сокращение для регулярного выражения). Это будет охватывать несколько постов в блоге (будет обновлять содержимое ниже) и постепенно становиться «более интересным» — не все для начинающих, но если вы будете в курсе, надеюсь, вы сможете это понять. И хотя это «Regexes and PHP», синтаксис regex, который я буду использовать, в значительной степени переносим на другие языки программирования.

содержание

Часть 1

Часть 2 здесь

Ресурсы

Перед тем, как погрузиться, я выберу одну или две ссылки. Одно из лучших учебных пособий по регулярным выражениям для PHP здесь — оно использует подход «инкрементного синтаксиса», но может быть достаточно дружественным. Это также довольно всеобъемлющее. Здесь вы не найдете ничего нового с точки зрения синтаксиса регулярных выражений: просто пересказ истории и, возможно, несколько интересных примеров.

В Википедии, конечно, есть масса информации и ссылок на еще больше информации, но, вероятно, больше, чем вы захотите переварить за один раз.

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

Наконец, Regex Coach (спасибо Maarten за подсказку, которую я написал, не пытаясь) является отличным инструментом, не только для обучения, но также для отладки регулярных выражений и получения представления о производительности (то есть профессионалы также могут найти это полезным).

Некоторый фон

PHP поставляется с двумя наборами функций и синтаксиса регулярных выражений — расширенных регулярных выражений POSIX и совместимых с Perl расширений регулярных выражений .

Когда-то давно лежащий в основе код для этих расширений был другим, но в наши дни оба используют один и тот же механизм PCRE — это входит в состав загружаемых вами дистрибутивов PHP. Обсуждение здесь будет сосредоточено исключительно на Perl-совместимом синтаксисе — он более мощный и стал более или менее стандартом — как только вы его узнаете, вы обнаружите, что он в значительной степени поддерживается большинством всех популярных языков программирования, от Java до Javascript .

И обратите внимание, что PHP не единственный проект, использующий библиотеку PCRE. Хотя некоторые языки создали свою собственную реализацию с нуля, вы обнаружите, что PCRE также используется в Apache, Ruby и многих других проектах с открытым исходным кодом, которым требуется мощная поддержка регулярных выражений при минимальных усилиях.

Зачем мне регулярные выражения?

… потому что они в значительной степени необходимы для чего угодно, кроме самой тривиальной обработки текста. Под «обработкой текста» я подразумеваю все, что вы анализируете или изменяете строку символов, например заменяете такие символы, как <и> на & lt; и & gt;, разбиение строки, содержащей точки с запятой, на список строк меньшего размера, подсчет количества раз, которое встречается конкретное слово и т. д. Для простых типов задач такого типа вы вполне можете выжить с помощью базовой функциональности строки, но сложнее задача, тем сложнее становится работать без регулярных выражений. Рассмотрите возможность проверки того, что предоставленный пользователем URL соответствует синтаксису RFC 2396 , например, с очень простыми только основными строковыми функциями. С регулярными выражениями это выполнимо .

Будучи убеждена? Возможно нет. Так что насчет страха и отвращения к «почему регулярные выражения?»: Без регулярных выражений вы не можете написать безопасное веб-приложение . Хотя PHP предоставляет другие инструменты, которые можно использовать для простого тестирования ввода, довольно скоро у вас возникнет проблема, для которой имеют смысл только регулярные выражения.

В противном случае — хотите верьте, хотите нет — они облегчают вашу жизнь. Например, если вы планируете написание синтаксического анализатора BBCode или задачу извлечения всех ссылок из документа HTML — регулярные выражения могут сделать это быстрым (примеры в другое время).

Понимание концепции

Возможно, самое высокое препятствие с регулярными выражениями является концептуальным — только что они?

Один из самых смелых ответов — это предметно-ориентированный язык — «мини» язык программирования, разработанный специально для описания и сопоставления текста. Возможно, не такое полезное описание для начинающих …

Другой способ думать о регулярных выражениях по аналогии. Большинство людей, которые собрали какое-то базовое веб-приложение, управляемое базой данных, знакомы с SQL как языком для извлечения (ВЫБОР) данных из вашей СУБД (например, MySQL). Регулярные выражения можно рассматривать как то же самое, что и SQL, но вместо того, чтобы извлекать данные из базы данных, вы используете их для извлечения данных из блока текста. И так же, как вы встраиваете операторы SQL в свой код (если вы не выполняете какой-то ORM ), вы делаете то же самое с регулярными выражениями — где вы можете вызвать mysqli_query () для выполнения вашего оператора SQL, вы вызываете функции, такие как preg_match () выполнить ваше регулярное выражение.

Конечно, вы можете зайти слишком далеко с аналогиями, поэтому я на этом остановлюсь. Суть в том, что регулярные выражения — это инструкции для вашего движка регулярных выражений , рассказывающие, как найти нужные символы из заданного блока текста.

Учиться путем практики…

Как и любой другой язык, лучший способ выучить регулярные выражения — это практика и терпение. Точка, в которой вы начинаете чувствовать себя уверенно, — это когда вы запомнили большую часть синтаксиса и можете читать регулярные выражения, не обращаясь к документации.

Для этого начнем изучать синтаксис, используя примеры, относящиеся к сети (которые вы, возможно, могли бы использовать повторно). Будут другие подходы (включая решения, которые избегают регулярных выражений), но цель состоит в том, чтобы иллюстрировать регулярные выражения, так что терпите меня.

Положительное соответствие

Проще всего начать с некоторых регулярных выражений, которые буквально соответствуют тексту, который вы ищете, без какого-либо дополнительного синтаксиса регулярных выражений.

Так что пример немного надуманный, но в любом случае … У вас есть форма, спрашивающая пользователя, прочитали ли они «Условия регистрации», и ваш ответ хранится в переменной $answer . Теперь вы хотите проверить, ответили ли они «да» на вопрос — все остальное считается «нет». Используя функцию preg_match (), вы можете сделать это следующим образом…

if ( preg_match('/yes/', $answer) ) { print "Say YES!!!n"; } else { print "what do you mean no?!?n"; }
if ( preg_match('/yes/', $answer) ) { print "Say YES!!!n"; } else { print "what do you mean no?!?n"; } 

Теперь позвольте мне ошеломить вас некоторыми деталями. Этот код спрашивает: «Могу ли я найти строку« да »в любом месте строки $answer ?».

Регулярное выражение является первым аргументом preg_match()'/yes/' . В PHP регулярные выражения всегда помещаются в строковые переменные PHP (как и в SQL). В отличие от некоторых других языков, таких как Javascript и Perl, регулярные выражения также могут быть «литералами», например (Javascript);

if ( /yes/.exec(answer) ) { alert("Say YES!!!"); }
if ( /yes/.exec(answer) ) { alert("Say YES!!!"); } 

В PHP это означает, что вам нужно быть немного осторожнее, когда дело доходит до обратной косой черты, а также знать, как PHP анализирует строки .

Разделители выражений и модификаторы шаблонов

Так что же здесь делают две косые черты?

if ( preg_match('/yes/', $answer) ) {
if ( preg_match('/yes/', $answer) ) { 

Это разделители выражений, обозначающие начало и конец регулярного выражения. В этом примере непонятно, зачем они нужны, но цель состоит в том, чтобы разрешить включение модификаторов шаблона в конце выражения. Модификаторы шаблонов — это «глобальные инструкции» для механизма регулярных выражений, которые говорят ему изменить его поведение по умолчанию. Я рассмотрю модификаторы шаблонов более скоро, но один из примеров — это модификатор /i , который указывает движку выполнять сопоставление без учета регистра, например

if ( preg_match('/yes/i', $answer) ) { // etc.
if ( preg_match('/yes/i', $answer) ) { // etc. 

Поместив модификатор шаблона /i в конец выражения, теперь я могу сопоставить строки «yes» и «YES» (и «YeS» или другие комбинации верхнего и нижнего регистра).

Обратите внимание, что разделитель выражений не обязательно должен быть косой чертой — вы также можете использовать почти все, кроме обратной косой черты или буквенно-цифрового символа. Вам просто нужно убедиться, что вы используете один и тот же разделитель на каждом конце шаблона. Например;

if ( preg_match('%yes%i', $answer) ) { // etc.
if ( preg_match('%yes%i', $answer) ) { // etc. 

Я использовал символ «%» вместо прямой косой черты, чтобы разделить выражение. Это может быть полезно, когда шаблон, который вы хотите найти, содержит разделитель (обычно, если вы сопоставляете что-то вроде URL-адреса пути файловой системы) — просто измените разделитель, вместо того, чтобы экранировать символы в выражении (подробнее о убегая в другой раз).

preg_match () возвращаемое значение

Согласно руководству по PHP, preg_match () возвращает количество раз, которое ему удалось сопоставить шаблон, который вы ему дали (первый аргумент «/ yes /»), с искомой строкой (второй аргумент $answer ). Так что, если он не смог сделать какие-либо совпадения, он возвращает целое число 0, что приведет к сбою PHP, if условие. preg_match() также прекращает поиск в момент первого успешного совпадения, поэтому будет возвращать максимум 1. Теперь вы можете задаться вопросом: если результат равен 0 или 1, почему руководство не говорит просто 0 или 1? Пункт, который он пытается передать, — preg_match() останавливается, как только он находит совпадение — это может быть важно, когда вы выполняете регулярные выражения в больших документах, где производительность может быть значительной: если вы хотите проверить, что документ содержит слово, и слово оказывается в первом абзаце, вы не хотите, чтобы механизм регулярных выражений сканировал весь документ, когда он уже найден

Обратите внимание, что 0 и 1 — не единственные возвращаемые значения — если что-то пойдет не так (например, шаблон не соответствует синтаксису регулярных выражений), он вернет FALSE (плюс вы получите грубое предупреждение об ошибке) — убедитесь, что вы тщательно проверили, Вы генерируете шаблоны на лету.

Подробнее о получении реальных совпадений из preg_match() другой раз

preg_match_all ()

В отличие от preg_match_all () , продолжается до тех пор, пока не изучит весь текст, который вы ищете. Это может быть проиллюстрировано следующим;

<?php $answer1 = "no"; $answer2 = "yes"; $answer3 = "yes yes"; print preg_match('/yes/', $answer1)."n"; // Displays '0' print preg_match('/yes/', $answer2)."n"; // Displays '1' print preg_match('/yes/', $answer3)."n"; // Displays '1' print preg_match_all('/yes/', $answer1, $m)."n"; // Displays '0' print preg_match_all('/yes/', $answer2, $m)."n"; // Displays '1' print preg_match_all('/yes/', $answer3, $m)."n"; // Displays '2'
<?php $answer1 = "no"; $answer2 = "yes"; $answer3 = "yes yes"; print preg_match('/yes/', $answer1)."n"; // Displays '0' print preg_match('/yes/', $answer2)."n"; // Displays '1' print preg_match('/yes/', $answer3)."n"; // Displays '1' print preg_match_all('/yes/', $answer1, $m)."n"; // Displays '0' print preg_match_all('/yes/', $answer2, $m)."n"; // Displays '1' print preg_match_all('/yes/', $answer3, $m)."n"; // Displays '2' 

Подробнее о preg_match() и preg_match_all() другой раз (например, о том, как извлечь из них сопоставленный текст).

Точное совпадение

Пока что я только смог подтвердить, что $answer содержит «да» гдето внутри. Тот
означает, что если пользователь предоставит ответ типа «Ba yes ian spam filter», он пройдет мой тест. Я действительно хочу быть на 100% уверенным, что пользователь точно сказал «да» условиям. Поэтому мне нужно немного больше синтаксиса шаблона, а именно два
метасимвола

if ( preg_match('/^yes$/', $answer) ) { // etc.
if ( preg_match('/^yes$/', $answer) ) { // etc. 

Метасимвол ^ означает «утверждать, что мы сопоставляем с начала $answer », а метасимвол $ означает «утверждать, что мы сопоставляем до конца $answer ». Так что же
выражение сейчас говорит что-то вроде;

Сопоставьте слово «да», но не сопоставляйте больше ничего

Лучше не зацикливаться на философском значении термина «метасимвол» — просто запомните эти два — ^ устанавливает начало строки, а $ — конец — вместе они помогают вам точно сопоставить целую строку.

Сказка

Вы также можете использовать их отдельно. Еще один надуманный пример (обещаю, что это будет последний): у вас есть сайт, где пользователи могут публиковать сказки, и вы хотите, чтобы каждая история начиналась «Однажды»;

if ( !preg_match('/^Once upon a time/', $story) ) { die("This is not how a real fairy starts!n"); }
if ( !preg_match('/^Once upon a time/', $story) ) { die("This is not how a real fairy starts!n"); } 

Затем, чтобы убедиться, что они заканчиваются словами «долго и счастливо», вы добавляете…

if ( !preg_match('/happily ever after$/', $story) ) { die("Don't give me sob stories!n"); }
if ( !preg_match('/happily ever after$/', $story) ) { die("Don't give me sob stories!n"); } 

Больше мета-символов в данный момент.

Заметка о тактике

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

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

Если вы обнаружите, что ваши регулярные выражения растут, вы можете сделать их более читаемыми с помощью модификатора шаблона /x , который позволяет разбить регулярное выражение на несколько строк и включать комментарии — я проиллюстрирую это в другой раз, а также подходы, которые могут поможет вам обработать текст с помощью регулярных выражений поэтапно.

Подтверждение имени пользователя

Переходя к примеру гораздо ближе к дому, одна классическая ошибка новичков при добавлении системы аутентификации пользователей в веб-приложение заключается в том, что пользователи могут выбирать любое имя пользователя, которое им нравится при регистрации. Довольно скоро появляется какой-то умный парень, который регистрирует себя как «администратор» (обратите внимание на начальный символ пробела) и начинает создавать непонятные посты по всему сайту, а в худшем случае использует плохо сконструированный код.

В общем, хорошей идеей является строго ограничивать ключевые идентификаторы, такие как имена пользователей, так что это хорошая возможность представить специальный тип метасимволов регулярных выражений: символ
класс В дополнение к «встроенным» метасимволам, таким как символы ^ и $ вы видели, вы также можете определить свои собственные метасимволы, используя класс символов , который используется для представления одного символа. Прыгая прямо к примеру …

if ( !preg_match('/^[a-zA-Z0-9_]+$/', $username) ) { die("Invalid username: only alpha numeric characters allowed."); }
if ( !preg_match('/^[a-zA-Z0-9_]+$/', $username) ) { die("Invalid username: only alpha numeric characters allowed."); } 

Мой класс символов здесь [a-zA-Z0-9_] — он соответствует любому символу, который соответствует одному из следующих условий;

  • Это строчные буквы между «а» и «z»
  • … Или заглавные буквы между «A» и «Z»
  • … или это цифра от 0 до 9
  • … или это символ подчеркивания ‘_’.

Знак минус «-», который появляется в классе символов, указывает диапазон, и вы заметите, что между, скажем, «a» и «z» в таблице символов ASCII у вас есть все строчные буквы алфавита, красиво отсортировано.

Количественная длина

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

Метасимвол + относится к предыдущему символу (или метасимволу) в шаблоне и изменяет его значение на «один или несколько из этого символа» — он количественно определяет его длину. Итак, мой пример …

if ( !preg_match('/^[a-zA-Z0-9_]+$/', $username) ) { // etc.
if ( !preg_match('/^[a-zA-Z0-9_]+$/', $username) ) { // etc. 

… Требует, чтобы имена пользователей были длиной не менее одного символа, но не накладывает ограничений на максимальную длину. Теперь это на самом деле не такая уж умная идея — имена пользователей, вероятно, должны иметь длину не менее 5 символов, чтобы их можно было прочитать, и, учитывая ограничения по пространству в столбце VARCHAR и разрешения экрана, возможно, имеет смысл установить максимальную длину; скажем 20 символов.

Вместо того, чтобы использовать квантификатор + , я могу использовать квантификатор min / max, который я определяю сам, используя фигурные скобки { } ;

if ( !preg_match('/^[a-zA-Z0-9_]{5,20}$/', $username) ) { // etc.
if ( !preg_match('/^[a-zA-Z0-9_]{5,20}$/', $username) ) { // etc. 

Как и квантификатор + , мин / макс квантификаторы применяются к предыдущему символу (или метасимволу) в шаблоне.

И здесь вы начинаете видеть некоторую силу регулярных выражений над альтернативными подходами. Моя проверка имени пользователя теперь просматривает не только содержимое имени пользователя (какие символы оно содержит), но также его длину с помощью одного оператора.

Точно так же, как вы знаете, квантификаторы min / max позволяют вам выполнять другие проверки длины, в зависимости от того, пропущены ли вы min или max, например:

# Username must be _at least_ 5 characters long but no max limit... if ( !preg_match('/^[a-zA-Z0-9_]{5,}$/', $username) ) { // etc.
# Username must be _at least_ 5 characters long but no max limit... if ( !preg_match('/^[a-zA-Z0-9_]{5,}$/', $username) ) { // etc. 

…и…

# Username must be _exactly_ 10 characters long... if ( !preg_match('/^[a-zA-Z0-9_]{10}$/', $username) ) { // etc.
# Username must be _exactly_ 10 characters long... if ( !preg_match('/^[a-zA-Z0-9_]{10}$/', $username) ) { // etc. 

Хорошо — достаточно для первой части. Пока ничего особенного — больше регулярных выражений в следующий раз …