Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Котеров Д. В., Костарев А. Ф. - PHP 5. 2-е издание (В подлиннике) - 2008

.pdf
Скачиваний:
6287
Добавлен:
29.02.2016
Размер:
11.36 Mб
Скачать

404

Часть IV. Стандартные функции PHP

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

ры после него как номер части. Найди следующий дефис, цифры после него соответствуют номеру главы. Наконец, пропусти все подчерки и выдели весь оставшийся текст до точки как идентификатор главы".

What is the PCRE?

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

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

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

Терминология

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

Давайте немного поговорим о терминологии. Вернемся к нашим примерам, только назовем теперь "словесное утверждение" регулярным выражением, или просто вы-

ражением.

В литературе иногда для этого же употребляется термин "шаблон".

Итак, мы имеем выражение и строку. Операцию проверки, удовлетворяет ли строка этому выражению (или выражение — строке, как хотите), условимся называть сопоставлением строки и выражения. Если какая-то часть строки успешно сопоставилась с выражением, мы назовем это совпадением. Например, совпадением от сопоставления выражения "группа букв, окруженная пробелами" к строке "ab cde fgh" будет строка "cde" (ведь только она удовлетворяет нашему выражению). Возможно, дальше мы с этим совпадением захотим что-то проделать — например, заменить его на какую-то строку или, скажем, заключить в кавычки. Это типичный пример сопоставления с заменой. Все подобные возможности реализуются в PHP в виде функций, которые мы сейчас и рассмотрим.

Глава 24. Основы регулярных выражений в формате PCRE

405

Языки регулярных выражений

Существует несколько различных языков регулярных выражений, на которые можно переводить наши "словесные утверждения". Наиболее распространенные — это язык PCRE (Perl Compatible Regular Expression, регулярное выражение языка Perl), а также выражения POSIX (Portable Operating System Interface, переносимый интерфейс операционной системы), их еще иногда называют RegEx или RegExp. В PHP имеются функции для работы с обоими форматами.

Нужно учитывать, что формат POSIX на практике медленно, но верно устаревает, а язык PCRE обходит его практически по всем параметрам (и по количеству возможностей, и по скорости на большинстве выражений). Но т. к. механизмы работы анализатора PCRE и POSIX сильно различаются (первый использует аппарат недетерминированных конечных автоматов (НКА), второй же — детерминированных (ДКА); впрочем, это уже технические детали), невозможно сказать абсолютно для всех случаев, какой из них работает быстрее. Существуют выражения, записывающиеся одинаково на языках PCRE и POSIX, однако, различающиеся по скорости работы в десятки и сотни раз — как в пользу PCRE, так и в пользу POSIX. Забегая вперед, мы привели в листинге 24.2 пример как раз такого выражения.

Листинг 24.2. Файл speed.php

<?php ## Сравнение скорости PCRE и POSIX. $re = "a(((((.*)*)*)*)*)*b";

$st = "abcdefgh";

//Запускаем механизм PCRE (НКА). $t = microtime(true);

$result = preg_match("/$re/", $st); printf("PCRE($result): %.2f c<br>", microtime(true)-$t);

//Запускаем механизм POSIX (ДКА).

$t = microtime(true); $result = ereg($re, $st);

printf("POSIX($result): %.2f c<br>", microtime(true)-$t); ?>

Запустив скрипт, мы увидим примерно следующий результат:

PCRE(0): 0.84 c

POSIX(1): 0.00 c

Итак, на выражении a(((((.*)*)*)*)*)*b, примеренном к строке "abcdefgh", PCRE отстает по скорости от POSIX в несколько тысяч раз!

Выражение a(((((.*)*)*)*)*)*b означает фразу "буква 'а', затем — любое число фраз 'любое число любых символов', и потом — буква 'b'". На самом деле там должно быть "любое число фраз 'любое число фраз 'любое число фраз '...' ' '" с уровнем вложенности, равным шести (вспомните стишок "у попа была собака, он ее любил"). Вот из-за этого "стишка" и происходит замедление.

406 Часть IV. Стандартные функции PHP

Тем не менее в большинстве типовых случаев выражения PCRE показывают значительно лучшие результаты, чем POSIX. Поэтому далее мы будем рассматривать только формат PCRE.

Использование регулярных выражений в PHP

Вернемся на минуту опять к практике. Любое регулярное выражение в PHP — это просто строка, его содержащая, поэтому функции, работающие с регулярными выражениями, принимают их в параметрах в виде обычных строк.

Сопоставление

bool preg_match(string $expr, string $str [,list &$pockets])

Функция пытается сопоставить выражение $expr строке $str и в случае удачи возвращает 1, иначе — 0. Если совпадение было найдено, то в список $pockets (конечно, если он задан) записываются отдельные участки совпадения (вспомните четвертый пример: там мы выделяли номера части и главы, а также идентификатор).

Как выделять эти участки на языке PCRE, мы рассмотрим немного позже. Пока скажем только, что в $pockets[0] всегда будет возвращаться подстрока совпадения целиком.

Хотя мы пока и не сказали ни слова о синтаксисе языка PCRE, приведем несколько простейших примеров использования функции preg_match() в листинге 24.3.

Листинг 24.3. Файл ex1.php

<?php ## Пример первый.

//Проверить, что в строке есть число (одна цифра или более). preg_match('/(\d+)/s', "article_123.html", $pockets);

//Совпадение (подвыражение в скобках) окажется в $pockets[1]. echo $pockets[1]; // выводит 123

?>

Обратите внимание, что регулярное выражение начинается и заканчивается слэшами (/). Это — обязательное требование языка PCRE (но не POSIX!), ниже мы о нем еще поговорим.

Вот еще один пример (листинг 24.4).

Листинг 24.4. Файл ex2.php

<?php ## Пример второй.

//Найти в тексте адрес e-mail. \S означает "не пробел", а [a-z0-9.]+ -

//"любое число букв, цифр или точек". Модификатор 'i' после '/'

//заставляет PHP не учитывать регистр букв при поиске совпадений.

Глава 24. Основы регулярных выражений в формате PCRE

407

//Модификатор 's', стоящий рядом с 'i', говорит, что мы работаем

//в "однострочном режиме" (см. ниже в этой главе).

preg_match('/(\S+)@([a-z0-9.]+)/is', "Привет от somebody@mail.ru!", $p); // Имя хоста будет в $p[2], а имя ящика (до @) — в $p[1].

echo "В тексте найдено: ящик — $p[1], хост — $p[2]"; ?>

Функция для работы с POSIX-выражениями, аналогичная по возможностям функции preg_match(), называется ereg().

В отличие от функции замены, по умолчанию функция сопоставления работает в многострочном режиме — так, будто бы явно указан модификатор /m! Например, вызов preg_match('/a.*b/', "a\nb") вернет 0, как будто бы совпадение не обнаружено — в многострочном режиме "точка" совпадает со всеми символами, кроме символа новой строки. Чтобы добиться нужной функциональности, необходимо указать модификатор "однострочности" явно — '/a.*b/s' (мы так и делаем здесь и далее). О модификаторах мы поговорим ниже, пока же просто держите в уме эту особенность функции.

Сопоставление с заменой

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

string preg_replace(string $expr, strint $to, string $str)

Эта функция занимается тем, что ищет в строке $str все подстроки, совпадающие с выражением $expr, и заменяет их на $to. В строке $str могут содержаться некоторые управляющие символы, позволяющие обеспечить дополнительные возможности при замене. Их мы рассмотрим позже, а сейчас скажем только, что сочетание $0 будет заменено на найденное совпадение целиком.

В листинге 24.4 мы приводили пример поиска e-mail в тексте. Теперь давайте посмотрим, что можно сделать с помощью того же регулярного выражения, но только в контексте замены. Попробуйте запустить скрипт из листинга 24.5.

Листинг 24.5. Файл ex3.php

<?php ## Превращение e-mail в HTML-ссылку.

$text = "Привет от somebody@mail.ru, а также от other@mail.ru!";

$html = preg_replace(

 

'/(\S+)@([a-z0-9.]+)/is',

// найти все e-mail

'<a href="mailto:$0">$0</a>',

// заменить их по шаблону

$text

// искать в $text

);

 

echo $html;

 

?>

 

408

Часть IV. Стандартные функции PHP

Вы увидите, что на HTML-странице, сгенерированной сценарием, адреса e-mail станут активными ссылками — они будут обрамлены тегами <a>...</a>.

В отличие от функции сопоставления, замена по умолчанию работает в однострочном режиме — как будто бы указан модификатор /s! Данная особенность может породить труднообнаружимые ошибки для переменных, которые содержат символы перевода строки.

ßçûê PCRE

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

простые символы, а также управляющие символы, играющие роль их "заменителей" — их еще называют литералами;

управляющие конструкции (квантификаторы повторений, оператор альтернативы, группирующие скобки и т. д.);

так называемые мнимые символы (в строке их нет, но, тем не менее, они как бы помечают какую-то часть строки — например, ее конец).

К управляющим символам относятся следующие: ., *, +, ?, |, (, ), [, ], {, }, $, ^. Забегая вперед, скажем, что все символы, кроме этих, обозначают в регулярном выражении сами себя и не имеют каких-либо специальных назначений.

Ограничители

Как мы уже видели, любое регулярное выражение в PHP представлено в виде обыкновенной строки символов. Кроме того, язык PCRE требует, чтобы все команды внутри выражения были заключены между ограничителями — двумя слэшами, также являющимися частью строки (мы уже неоднократно с ними встречались). Например, строка "expr" является синтаксически некорректным регулярным выражением, в то время как "/expr/" — правильное.

Зачем нужны эти слэши? Дело в том, что после последнего слэша можно указывать различные модификаторы. Чуть позже мы поговорим о них подробнее, а пока опишем модификатор /i, который уже применяли ранее: он говорит PHP, что учитывать регистр символов при поиске совпадений не следует. Например, выражение /expr/i совпадает как со строкой "expr", так и со строками "eXpr" и "EXPR".

Итак, общий вид записи регулярного выражения — '/выражение/M', где M обозначает ноль или более модификаторов. Если символ / встречается в самом выражении (например, мы разбираем путь к файлу), перед ним необходимо поставить обратный слэш \, чтобы его экранировать:

if (preg_match('/path\\/to\\/file/i', "path/to/file"))

echo "совпадение!";

Глава 24. Основы регулярных выражений в формате PCRE

409

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

Альтернативные ограничители

Как видите, синтаксис записи строк в PHP требует, чтобы все обратные слэши в программе были удвоены. Поэтому мы получаем весьма неказистую конструкцию — '/path\\/to\\/file/i'. Проблема в том, что символы-ограничители совпадают с символами, которые мы ищем.

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

//Можно использовать любые одинаковые символы как ограничители...

'/path\\/to\\/file/i'

'#path/to/file#i'

'"path/to/file"i'

//А можно — парные скобки.

'{path/to/file}i'

'[path/to/file]i'

'(path/to/file)i'

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

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

echo preg_replace('[(/file)[0-9]+]i', '$1', "/file123.txt");

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

В литературе все же принято использовать / в качестве "ограничителей по умолчанию".

Отмена действия спецсимволов

Поначалу довольно легко запутаться во всех этих слэшах, апострофах, долларах...

Сложность заключается в том, что такие символы являются специальными как в PCRE, так и в PHP, а поэтому иногда их нужно экранировать не один раз, а несколько.

410

Часть IV. Стандартные функции PHP

Рассмотрим, например, регулярное выражение, которое ищет в строке некоторое имя файла, предваренное обратным слэшем \ (как в Windows). Оно записывается так:

$re = '/\\\\filename/';

Как получилось, что единственный слэш превратился в целых четыре? Давайте посмотрим.

1.Удвоенный слэш в строках PHP обозначает один слэш. Если мы вставим в программу оператор echo $re, то увидим, что будет напечатан текст /\\filename/.

2.Удвоенный слэш в PCRE означает один слэш. Таким образом, получив на вход выражение /\\filename/, анализатор поймет, что от него требуется найти подстроку, начинающуюся с одного слэша.

Давайте рассмотрим пример посложнее: будем искать любое имя каталога, после которого идет также любое имя файла. Выражение будет записываться так:

$re = '/\\S+\\\\\\S+/';

Вот уже мы получили целых шесть слэшей подряд... (Последовательность \S в PCRE обозначает любой "непробельный" символ, а + после команды — повтор ее один или более раз.)

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

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

Если какое-то регулярное выражение с "зубочистками" наотрез отказывается работать, попробуйте записать его в переменную (как в примерах выше), а после этого вывести ее в браузер:

echo "<tt>".htmlspecialchars($re)."</tt>";

Этот прием поможет увидеть выражение "глазами PCRE", уже после того, как PHP получит строковые данные выражения.

Итак, если же нужно вставить в выражение один из управляющих символов, но только так, чтобы он "не действовал", достаточно предварить его обратным слэшем. К примеру, если мы ищем строку, содержащую подстроку a*b, то должны использовать для этого выражение a\*b (помните, что в PHP эта строка будет записываться как 'a\\*b'), поскольку символ * является управляющим (вскоре мы рассмотрим, как он работает).

Простые символы (литералы)

Класс простых символов, действительно, самый простой. А именно любой символ в строке на PCRE (и, кстати, на языке POSIX тоже) обозначает сам себя, если он не является управляющим. Например, регулярное выражение at будет "реагировать" на

Глава 24. Основы регулярных выражений в формате PCRE

411

строки, в которых встречается последовательность at — в середине ли слова, в начале — не важно:

echo preg_replace('/at/', 'AT', "What is the Perl Compatible Regex?");

В результате будет выведена строка:

WhAT is the Perl CompATible Regex?

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

$re = "/a\*b/"

но можем

$re = "/a\\*b/"

В последнем случае в строке $re оказывается /a\*b/. Так как регулярные выражения в PHP представляются именно в виде строк, то необходимо постоянно помнить это правило.

Классы символов

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

Самый важный из таких знаков — точка (".") — обозначает один любой символ. Например, выражение /a.b/s имеет совпадение для строк azb или aqb, но не "срабатывает", скажем, для aqwb или ab. Позже мы рассмотрим, как можно заставить точку обозначать ровно два (или, к примеру, ровно пять) любых символов.

В PCRE (в отличие от POSIX) существует еще несколько классов:

\s — соответствует "пробельному" символу: пробелу (" "), знаку табуляции (\t), переносу строки (\n) или возврату каретки (\r);

\S — любой символ, кроме пробельного;

\w — любая буква или цифра;

\W — не буква и не цифра;

\d — цифра от 0 до 9;

\D — все, что угодно, но только не цифра.

Альтернативы

Но это далеко не все. Возможно, вы захотите искать не произвольный символ, а один из нескольких указанных. Для этого наши символы нужно заключить в квадратные скобки. К примеру, выражение /a[xXyY]c/ соответствует строкам, в которых есть подстроки из трех символов, начинающиеся с а, затем одна из букв x, X, y, Y и, наконец, буква c. Если нужно вставить внутрь квадратных скобок символ [ или ], то следует просто поставить перед ним обратный слэш (напоминаем, в строках PHP — два слэша), чтобы отменить его специальное действие.

412

Часть IV. Стандартные функции PHP

Если букв-альтернатив много, и они идут подряд, то не обязательно перечислять их все внутри квадратных скобок — достаточно указать первую из них, потом поставить дефис и затем — последнюю. Такие группы могут повторяться. Например, выраже- ние /[a-z]/ обозначает любую букву от a до z включительно, а выражение /[a-zA- Z0-9_]/ задает любой алфавитно-цифровой символ.

Существует и другой, иногда более удобный способ задания больших групп символов. В языке PCRE в скобках [ и ] могут встречаться не только одиночные символы, но и специальные выражения. Эти выражения определяют сразу группу символов. Например, [:alnum:] задает любую букву или цифру, а [:digit:] — цифру. Вот полный список таких выражений:

[:alpha:] — буква;

[:digit:] — цифра;

[:alnum:] — буква или цифра;

[:space:] — пробельный символ;

[:blank:] — пробельный символ или символы с кодом 0 и 255;

[:cnrtl:] — управляющий символ;

[:graph:] — символ псевдографики;

[:lower:] — символ нижнего регистра;

[:upper:] — символ верхнего регистра;

[:print:] — печатаемый символ;

[:punct:] — знак пунктуации;

[:xdigit:] — цифра или буква от A до F.

Как видим, все эти выражения задаются в одном и том же виде — [:что_то:]. Обратите еще раз внимание на то, что они могут встречаться только внутри квадратных скобок. Например, допустимы такие регулярные выражения:

'/abc[[:alnum:]]+/'

#

abc,

затем

одна или более буква или цифра

'/abc[[:alpha:][:punct:]0]/'

#

abc,

далее

буква, знак пунктуации или 0

но совершенно недопустимо следующее:

'/abc[:alnum:]+/' # не работает!

Внутри скобок [] также можно использовать символы-классы, описанные в предыдущем подразделе. Например, допустимо выражение:

'/abc[\w.]/'

Оно ищет подстроку "abc", после которой идет любая буква, цифра или точка.

Внутри скобок [] точка теряет свой специальный смысл ("любой символ") и обозначает просто точку, поэтому не ставьте перед ней слэш!

Отрицательные классы

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

Глава 24. Основы регулярных выражений в формате PCRE

413

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

echo preg_replace('/<[^>]+>/', '', $text);

Данный способ хорош только для XML-файлов, для которых точно известно: внутри тега не может содержаться символ >. В HTML же все несколько сложнее: например, допустима конструкция: <img src=".gif" alt="a>b">. Конечно, приведенное выше регулярное выражение для нее сработает неверно (точно так же, как и стандартная функция PHP strip_tags()).

В отрицательном классе могут быть задействованы любые символы и выражения, которые допустимы в конструкции [...].

Квантификаторы повторений

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

Ноль или более совпадений

Наиболее популярный квантификатор — звездочка (*). Она обозначает, что предыдущий символ может быть повторен ноль или более раз (т. е. возможно, и ни разу). Например, выражение /a-*-/ соответствует строке, в которой есть буква a, затем — ноль или более минусов и, наконец, завершающий минус.

В простейшем случае при этом делается попытка найти как можно более длинную строку, т. е. звездочка "поглощает" так много символов, как это возможно. К примеру, для строки a---b найдется подстрока a--- (звездочка "заглотила" 2 минуса), а не a- (звездочка захватила 0 минусов). Это — так называемая "жадность" квантификатора. Чуть ниже мы рассмотрим, как можно "убавить аппетиты" звездочки

(см. разд. "“Жадность” квантификаторов" далее в этой главе).

В регулярных выражениях стандарта POSIX квантификаторы могут быть только "жадными".

Одно или более совпадений

Возможно, вы заметили некоторую неуклюжесть в предыдущем примере. В самом деле, фактически мы составляли выражение, которое ищет строки с a и одним или более минусом. Можно было бы записать его и так: /a--*/, но лучше воспользоваться специальным квантификатором, который как раз и обозначает "одно или более совпадений" — символом плюса (+). С его помощью можно было бы выражение