Краткое описание регулярных выражений: POSIX и PCRE.

:

Краткое описание регулярных выражений: POSIX и PCRE

Описание базовых принципов и понятий работы регулярных выражений.
Описание синтаксиса языка регулярных выражений стандарта POSIX. Функции. Примеры.
реимущества стандарта реглярных выражений PCRE над POSIX. Рассыльщик почты с обильным использованием PCRE.

Обещал написать про регулярные выражения. Писал, писал, но что-то не то выходит. Слишком мелкая конкретика, пересказывание руководства по php. Поэтому я решил, что необходимо (приняв степенную осанку) подготовить аудиторию, так сказать, начать с малого... (" - Дорогой, а где моя мама? - Теща уже на крыше


Начну с того, что php поддерживает два стандарта регулярных выражений: POSIX и, начиная с четвертой версии, совместимые с Perl. Первый стандарт используется и сервером Apache в mod_rewrite а так же... MySQL в своих запросах (поищите слово "REGEXP" в руководстве по mysql, может сразу поймете, а я об этом позже расскажу). Второй, как ясно из названия, используется в системе perl. Два этих стандарта различаются несильно - во втором есть специальные символы, заменяющие наиболее часто используемые классы символов (например, цифры - \d, а буквы и цифры - \w) и специальные параметры шаблонов, позволяющие определять регистрозависимость поиска, привязку к концам строк и т.д (в функциях стандарта POSIX регистрозависимость реализована просто: есть функции ereg и ereg_eeplace, есть eregi (insensitive) и eregi_replace). В остальном же оба стандарта совместимы, а приемы написания шаблонов одинаковые.

Если вы работали с Norton/Volkov/Windows Commander или Far, то знаете такую вещь как wildcards. Например: delete c:\windows\*.* удаляет все файлы из указанной директории. :) В именах файлов особых изощрений делать не приходится, поэтому система простая: символ * означает любой набор символов, в том числе пустой (*.txt), символ ? - любой символ или никакого символа (document?.txt) и еще какие-то обозначения для букв и цифр (я, честно говоря, ими давно не пользовался, поэтому так не вспомню).

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

Итак, задача системы - помимо четко заданных символов ("Вася(.*)Пупкин") позволить указать пользователю поиск заданного количества заданных символов. В приведенном примере с Васей Пупкиным между словами задано любое количество любых символов. Если надо найти шесть цифр, то пишем "[0-9]{6}" (если, например, от шести до восьми цифр, тогда "[0-9]{6,8}"). К чему это все? К тому, что в отличие от wildcard из операционной системы, здесь разделены такие вещи как указатель набора символов и указатель необходимого количества: <набор символов><квантификатор> Вместо набора символов может быть использовано обозначение любого символа - точка, может быть указан конкретный набор символов (поддерживаются последовательности - упоминавшиеся "0-9"). Может быть указано "кроме данного набора символов".

Указатель количества символов в официальной документации по php называется "квантификатор". Термин удобный и не несет в себе кривотолков. Итак, квантификатор может иметь как конкретное значение - либо одно фиксированное ("{6}"), либо как числовой промежуток ("{6,8}"), так и абстрактное "любое число, в т.ч. 0" ("*"), "любое натуральное число" - от 1 до бесконечности ("+": "document[0-9]+\.txt"), "либо 0, либо 1" ("?"). По умолчанию квантификатор для данного набора символов равен единице ("document[0-9]\.txt").

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

Как всякий гибкий инструмент, регулярные выражения гибки, но не абсолютно: зона их применения ограничена. Например, если вам надо заменить в тексте одну фиксированную строку на другую, фиксированную опять же, пользуйтесь str_replace. Разработчики php слезно умоляют не пользоваться ради этого сложными функциями ereg_replace или preg_replace, ведь при их вызове происходит процесс интерпретации строки, а это серьезно потребляет ресурсы системы. К сожалению, это любимые грабли начинающих php-программистов (даже я сам, волею судьбы, сперва увидел в руководстве функцию ereg_replace, а только потом, позже, str_replace).

Пользуйтесь функциями регулярных выражений только если вы не знаете точно, какая "там" строка. Из примеров: поисковый код этого сайта, в котором из строки поиска вырезаются служебные символы и короткие слова а так же вырезаются лишние пробелы (вернее, все пробелы сжимаются: " +" заменяется на один пробел). При помощи этих функций я проверяю email пользователя, оставляющего свой отзыв. Много полезного можно сделать, но важно иметь в виду: регулярные выражения не всесильны. Например, сложную замену в большом тексте ими лучше не делать. Ведь, к примеру, комбинация "(.*)" в программном плане означает перебор всех символов текста. А если шаблон не привязан к началу или концу строки, то и сам шаблон "двигается" программой через весь текст, и получается двойной перебор, вернее перебор в квадрате. Нетрудно догадаться, что еще одна комбинация "(.*)" означает перебор в кубе, и так далее. Возведите в третью степень, скажем, 5 килобайт текста. Получается 125 000 000 000 (прописью: сто двадцать пять миллиардов операций). Конечно же, если подходить строго, там стольких операций не будет, а будет раза в четыре-восемь меньше, но важен сам порядок цифр.

Итак, принципы, достоинства и недостатки описаны, теперь надо переходить к конкретике. Два (возможно, сразу следующих) выпуска будут посвящены двум стандартам регулярных выражений - POSIX и PCRE. Описание базовых принципов и понятий работы регулярных выражений.

Продолжаем наш разговор. Предыдущий выпуск был вводным, теорией. Сегодня как бы основная часть рассказа — стандарт POSIX. В следующем выпуске я опишу различия, вернее сказать надстройки стандарта совместимого с perl. Итак, обо всем по порядку.

Набор символов

. точка любой символ
[<символы>] квадратные скобки класс символов ("любое из")
[^<символы>]   негативный класс символов ("любое кроме")
- тире обозначение последовательности в классе символов ("[0-9]" — цифры)

Особо объяснять ничего не нужно. Разве что следующее: не пользуйтесь классом символов для обозначения всего лишь одного (вместо "[ ]+" вполне сойдет " +"). Не пишите в классе символов точку — это ведь любой символ, тогда другие символы в классе будут просто лишними (а в негативном классе получится отрицание всех символов).

Квантификатор

Это, как я уже писал, указатель количества заданных символов. Квантификатором можно указать как конкретное значение, так и пределы. Если число заданных подпадает под пределы квантификатора, фрагмент выражения считается совпавшим с разбираемой строкой. Синтаксис: {<количество>} либо {<минимум>, <максимум>}

Если нужно указать только необходимый минимум, а максимума нет, просто ставим запятую и не пишем второе число: "{5,}" ("минимум 5"). Для наиболее часто употребляемых квантификаторов есть специальные обозначения:

* "звёздочка" или знак умножения {0,}
+ плюс {1,}
? вопросительный знак {0,1}

На практике такие символы используются чаще, чем фигурные скобки.

Якоря

^ привязка к началу строки
$ привязка к концу строки

Эти символы должны стоять соответственно в самом начале и в самом конце строки. Чтобы интерпретатор корректно понял символ $ в конце, желательно добавить к нему обратный слэш: ereg("foo\$", $bar)Структура

Сейчас будет сложное описание, мне оно и самому не нравится. Эта вещь необходима для сложных запросов. Например, вам надо, чтобы в тексте были либо только маленькие буквы, либо только большие, либо только цифры. Класс символов "[a-zA-Z0-9]" не подходит. Тогда пишем такое:

if (ereg("[a-z]+|[A-Z]+|[0-9]+", $text)) ...

Вертикальная черта — знак "или" регулярных выражений (знака "и", естественно, не существует — это и есть само регулярное выражение). Разделенные вертикальной чертой шаблоны в официальной документации называются альтернативными ветвями (это подразумевает ветвление, т.е. наличие вложенных альтернативных ветвей). Программа сравнивает со строкой все ветви (проходясь по их ряду слева направо), до первого совпадения (это важно учесть, если у вас сложное выражение со вложенными ветвями). Для разделения уровней и отделения этого дерева альтернатив от остального шаблона используются обычные скобки. Если те же большие/маленькие буквы/цифры надо искать внутри контейнера тегов:

if (ereg("<tag>([a-z]+|[A-Z]+|[0-9]+)</tag>", $text)) ...

Из сложного это, кажется, все. Теперь о более простом. Скобки по-научному называются subpattern (вложенный шаблон). И используются не только для сложных вариантов шаблонов, но и для гибкой замены фрагментов текста или получения их в переменную. К примеру, для печатной версии текста дублируем адреса ссылок текстом в скобках:

ereg_replace("<a href=([^>]+)>[^<]+</a>", "\\0 [\\1]", $text);

Первые скобки — первый вложенный шаблон — можно получить "на выходе" через обозначение "\n" (поскольку обратный слэш в php и многих других языках используется для спецсимволов, надо поставить перед ним еще один такой же, чтобы прогамма понимала его буквально). Под нулевым номером — вся совпавшая строка. У себя в печатной версии статьи я не пишу ссылки сразу в тексте, а делаю их список в конце примерно так:

if (ereg("<a href=([^>]+)>([^<]+)</a>", $text, $match)) {
  for ($a=0;$a<sizeof($match[0]);$a++) {
    $b = $a+1;
    $text = str_replace($match[0][$a], $match[0][$a]." [$b]", $text);
    $match[1][$a] = "$b) ". $match[1][$a];
    };
  $text .= "<br><h2>Ссылки, использованные в выпуске:</h2>". implode("<br>", $match[1]);
  };

Функция ereg (и eregi), если ей указать в третьем параметре переменную, то туда будут записаны все подстроки в виде двухмерного массива.

Это, собственно, все. Дальше нужно только уметь составлять шаблоны. Приведу несколько примеров.

  • Переписывание адресов сервером Apache (как я уже отметил, Apache работает со стандартом POSIX).
  • Поиск по базе данных: из пользовательского поискового запроса делается sql-запрос. Если отбросить создание статистики поиска (сколько найдено всего, сколько по каждому слову), то получится, что необходимо всего 6-7 строк кода. Там же описана и подсветка слов в результатах поиска. Кстати, важное замечание: перед тем, как вырезать короткие слова из строки я заменяю пробелы между словами на двойные. Почему? Потому что совпадающие с шаблоном строки не должны наезжать друг на друга.

    Объясню подробнее. Если в шаблоне нет никаких якорей, система проходится по тексту слева направо, и если найдено совпадение, кидает его в какие-то переменные, а затем перескакивает на следующий символ после совпавшего фрагмента. Мы ищем по шаблону "пробел, два непробела, пробел", а пробелы одиночные. Программа находит "пробел-короткое слово-пробел", заменяет это на один пробел, а затем перескакивает на... первую букву следующего слова. Это не пробел, поэтому даже если следующее слово тоже короткое, оно под шаблон не подойдет. Поэтому-то и надо предварительно заменить пробелы на двойные.

  • Как хранить новости в файлах и не бегать циклом по дате:
    $handle=opendir($newsdir);
    while ($file = readdir($handle)) {
      if (is_file($file) && ereg("^[0-9]{6}\.txt\$", $file))
        print ("<p align=justify><b>". 
        ereg_replace("^([0-9]{2})([0-9]{2})([0-9]{2})\.txt\$", "\\1.\\2.20\\3", $file).  
        "</b> ".implode("", file($file)). "</p>");
    closedir($handle);

    4. Проверка правильного написания email-а:

    if (!eregi("^[a-z0-9\._-]+@[a-z0-9\._-]+\.[a-z]{2,4}\$", $email))   print("Bad email: \"$email\"");

    На этом все. В следующем выпуске — стандарт PCRE, точнее дополнительные возможности, которые он предоставляет.

    И вот, наконец, серия выпусков про регулярные выражения подходит к концу. Поговорим о регулярных выражениях совместимых с Perl (Perl compatible regular expressions — PCRE).

    Самое главное их преимущество перед POSIX, как мне уже подсказывают — возможность "жадного" поиска. Вопросительный знак в PCRE выступает еще и как минимизатор квантификатора: .*? Найдет минимальную подходящую строку. Вроде бы ничего особенного? Нет, это очень особенная вещь. Например, какой пример я приводил в прошлом выпуске про печатную версию текста?

    $text = ereg_replace("<a +href=([^>]+)>[^<]+</a>", "\\0 [\\1]", $text);

    То есть, внури ссылки не должно быть тегов (например "<a href=...><b>...</b></a>"). Если же сделать так:

    $text = ereg_replace("<a +href=([^>]+)>.*</a>", "\\0 [\\1]", $text);

    Тогда мы получим... Правильно, весь текст между началом первой и концом последней ссылки. Все проблемы снимает жадный поиск.

    $text = preg_replace("/<as+href=(.*?)>.*?</a>/", "\\0 [\\1]", $text);

    Программа подберет для всех ссылок минимальную подходящую строку, т.е. только до тега "</a>". Описывать значение такой особенности PCRE нет смыла — оно огромное. :) Идем дальше.

    Цифры теперь можно обозначить не как "[0-9]", а просто "\d". Не-цифры ("[^0-9]") как "\D". Очень удобно. Вот остальные обозначения:

    \w [a-z0-9]
    \W [^a-z0-9]
    \s [ ]
    \S [^ ]

    Рекомендую заглянуть в выпуски про поиск — там эти символы используются.

    Строка шаблона, как вы уже заметили, начинается и заканчивается слэшами. Для чего нужен первый слэш, не знаю. Последний нужен для отделения шаблона от параметров. Параметры, которые я понял, таковы:

    i регистронезависимый поиск
    m многостроковый режим. По умолчанию PCRE ищет сопвадения с шаблоном только внутри одной строки, а символы "^" и "$" совпадают только с началом и концом всего текста. Когда этот параметр установлен, "^" и "$" совпадают с началом и концом отдельных строк.
    s символ "." (точка) совпадает и с переносом строки (по умолчанию — нет)
    A привязка к началу текста
    E заставляет символ "$" совпадать только с концом текста. Игнорируется, если установлен парамерт m.
    U Инвертирует "жадность" для каждого квантификатора (если же после квантификатора стоит "?", этот квантификатор перестает быть "жадным").

    Естественно, регистр в параметрах имеет значение. Остальное о них можно прочесть в руководстве по php.

    Теперь о функциях PCRE.

    Функция preg_match в отличие от ereg ищет только первое совпадение. Если нужно найти все совпадения и как-то обработать их результаты (но не напрямую через preg_replace), нужно пользоваться preg_match_all. Параметры этой функции те же.

    Из полезного отмечу функцию preg_quote, которая вставляет слэши перед всеми служебными символами (например, скобками, квадратными скобками и т.п.), чтобы те воспринимались буквально. Если у вас есть какой-либо ввод информации пользователем, и вы проверяете его через PCRE, лучше перед этим закомментировать служебные символы в пришедшей переменной (мало ли что он там напишет, это ведь по определению злобный хакер).

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

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

  • Введение в Perl. Регулярные выражения (шаблоны)
  • Фрагмент моего старого поискового кода