Котеров Д. В., Костарев А. Ф. - PHP 5. 2-е издание (В подлиннике) - 2008
.pdf424 |
Часть IV. Стандартные функции PHP |
зуя одну лишь функцию preg_replace(), невозможно. На помощь придет процедура preg_replace_callback(), которую мы вскоре рассмотрим (см. разд. "Замена совпаде-
ний" далее в этой главе).
На самом деле, предыдущий абзац нуждается в уточнении. Чтобы избежать проблем с апострофами, разработчики PHP предприняли довольно неуклюжую попытку: перед подстановкой $1, $2 и т. д. к их содержимому применяется функция addslashes(), которая добавляет слэши перед кавычками и апострофами.
Давайте рассмотрим несколько примеров и убедимся, что даже такое ухищрение разработчиков PHP не ведет ни к чему хорошему.
Пусть строка подстановки "'head $1 tail'", а $1 содержит "cat's". Тогда после подстановки получим корректный код на PHP: 'head cat\'s tail'. Казалось бы, все хорошо.
Пусть строка подстановки прежняя, "'head $1 tail'", а $1 содержит не апостроф, а кавычку: 'cat"s'. Получившийся код — 'head cat\"s tail', а он генерирует строку, содержащую последовательность \" (т. к. \" в строках, заключенных в апострофы, никак не интерпретируется). Это уже некорректно. Значит, использование такой строки подстановки недопустимо.
Попробуем поменять строку — будем использовать кавычки внутри апострофов (до этого мы указывали апострофы внутри кавычек). К сожалению, даже написав '"head $1 tail"', мы не избавимся от проблем. Теперь кавычки и апострофы будут обрабатываться правильно, но если в $1 попадется строка, содержащая доллар, он будет воспринят как имя переменной. Например, при $1, равном '$some', получится код "head $some tail", что при выполнении сгенерирует предупреждение: неопределенная переменная $some.
Итак, мы видим, что ни один из способов задания строки в программе на PHP не подходит в случае использования модификатора /e. Следовательно, данного модификатора лучше избегать, используя вместо него вызов preg_replace_callback().
Незахватывающий поиск
Когда некоторое регулярное выражение или его часть внутри круглых скобок совпадает с подстрокой, оно "захватывает" эту подстроку, так что подвыражения, следующие далее, уже ее "не видят". Такое поведение не является обязательным: в PCRE существует целый ряд конструкций, позволяющих сравнивать подстроки без захвата.
Данные конструкции чем-то напоминают мнимые символы ^, $ и \b. Действительно, они фактически совпадают не с подстрокой, а с позицией в строке. Про такую ситуацию говорят: инструкция имеет нулевую ширину совпадения, имея в виду тот факт, что, обрамив конструкцию круглыми скобками, мы всегда получим в кармане строку нулевой длины. Нулевую ширину имеют все мнимые символы, а также конструкции, перечисленные далее.
Позитивный просмотр вперед
Пожалуй, самая простая конструкция — это оператор просмотра вперед. Записывается он так (без пробелов):
(?=подвыражение)
Глава 24. Основы регулярных выражений в формате PCRE |
425 |
На подвыражение в скобках не накладывается никаких ограничений: это может быть полноценное регулярное выражение.
Когда в выражении встречается такая конструкция, текущая позиция считается допустимой, если с нее начинается подстрока, совпадающая с подвыражением в скобках. При этом "захвата" символов не происходит, и следующая конструкция будет работать с той же самой позицией в строке, которой только что "дали добро".
Например, рассмотрим регулярное выражение |(\S+)(?=\s*</)|. Оно совпадет со словом, сразу после которого идет закрывающий HTML-тег (возможно, с промежуточными пробелами). При этом ни сам тег, ни пробелы в совпадение не войдут.
Негативный просмотр вперед
Существует также возможность негативного просмотра вперед — проверки, чтобы с текущей позиции не начиналось некоторое подвыражение. Записывается это так:
(?!подвыражение)
Например, если мы хотим захватить все знаки пунктуации, кроме точки и запятой, мы можем использовать регулярное выражение:
/ |
|
(?![.,]) |
# дальше идет НЕ точка и НЕ запятая |
([[:punct:]]+) |
# ...а какая-то другая пунктуация |
/x |
|
Как видите, конструкцию (?!...) удобно использовать для быстрой проверки текущей позиции в регулярном выражении. Этим она очень напоминает инструкцию continue, которая "отфильтровывает" неподходящие элементы в цикле.
Позитивный просмотр назад
Просматривать строку без захвата символов можно не только вперед, но и назад. Для этого применяется следующий оператор:
(?<=подвыражение)
Как он работает? Давайте рассмотрим такое регулярное выражение:
/ |
|
(?<=<) |
# слева идет "<" — начало тега... |
(\w+) |
# дальше — имя тега |
/x |
|
Посмотрим, как оно применяется к строке "gutten <tag>". Анализатор идет по строке, вначале он на букве g. Анализатор смотрит, есть ли слева от этой позиции символ <. Его нет, так что просмотр продолжается — на букве u. Так он доходит до буквы t, и вот в этот момент оказывается, что слева от нее стоит символ <! Квантификатор + быстро "докручивает" оставшиеся буквы, и результат — слово "tag" — попадает в карман.
Предыдущий абзац может навести на мысль, что внутри (?<=...) можно использовать не любые регулярные выражения. Действительно, если там написать, скажем, "a.*", получится, что на каждом шаге анализатор вынужден будет "бегать" по всей под-
426 |
Часть IV. Стандартные функции PHP |
строке, от начала анализируемой и до текущей позиции, в поисках буквы a и любого количества символов после нее. Это недопустимые затраты времени, поэтому на подвыражение внутри оператора просмотра назад налагается одно ограничение: оно должно быть либо фиксированной "ширины", либо же представлять собой альтернативу, каждый элемент которой также имеет фиксированную ширину. Например, следующее выражение допустимо:
/ (?<= < | \[) (\w+)/x
(Пробелы за счет модификатора /x не имеют здесь никакого значения.) А такое — уже не работает:
/ (?<= <.*?>) (\w+)/x
Негативный просмотр назад
Негативный просмотр назад отличается от позитивного только тем, что делает все в точности наоборот. Записывается он так:
(?<!подвыражение)
Вот пример из документации PHP. Выражение /(?<!foo)bar/ совпадает со строкой "boobar", но не совпадает — со строкой "foobar".
Другие возможности PCRE
Мы рассмотрели в этой главе лишь некоторую часть операторов и метасимволов PCRE, доступных программисту. За рамками остались совсем уж редко употребляемые операции, вроде однократных подмасок и условных срабатываний. При желании вы можете прочитать о них в документации Perl (PCRE — это ведь регулярные выражения Perl), а также на сайте PHP (доступен перевод на русский язык): http://ru.php.net/manual/ru/pcre.pattern.syntax.php.
Функции PHP
Если вы помните, в самом начале главы мы дали краткое описание функциям preg_match() и preg_replace(), чтобы создавать хоть какие-то примеры кода. Настало время ознакомиться с этими (а также с некоторыми другими) функциями подробнее.
Поиск совпадений
Все функции поиска по регулярному выражению по умолчанию работают в многострочном режиме, как будто бы указан модификатор /m. Рекомендуется явно использовать /s, когда это необходимо.
bool preg_match(string $expr, string $str [,list &$pockets] [,int $flags] [,int $offset])
Как видно из прототипа, функция preg_match() имеет куда более богатые возможности, чем те, что мы использовали до сих пор. Первые три аргумента нам уже хорошо знакомы: это регулярное выражение, строка, в которой производится поиск, а также
Глава 24. Основы регулярных выражений в формате PCRE |
427 |
необязательная переменная, в которую будут записаны все совпадения скобочных выражений внутри $expr. Функция возвращает 1 в случае обнаружения совпадения и 0 — в противном случае. (Не true и false, а именно 1 или 0.) Если регулярное выражение содержит ошибки (например, непарные скобки), то будет сгенерировано соответствующее предупреждение.
Необязательный параметр $offset может указывать позицию, с которой нужно начинать просмотр строки. (Если отсутствует, подразумевается начало.)
Параметр $flags на настоящий момент может принимать только одно значение — PREG_OFFSET_CAPTURE. Он заставляет PHP немного изменить формат списка $pockets: теперь вместе с совпавшим текстом сохраняется также и позиция совпадения в исходной строке. Листинг 24.12 иллюстрирует сказанное.
Листинг 24.12. Файл exA.php
<?php ## Использование PREG_OFFSET_CAPTURE. $st = '<b>жирный текст</b>';
$re = '|<(\w+).*?>(.*?)</\1>|s'; preg_match($re, $st, $p, PREG_OFFSET_CAPTURE); echo "<pre>"; print_r($p); echo "</pre>";
?>
Результат работы этой программы выглядит примерно так:
Array(
[0]=> Array(
[0]=> <b>жирный текст</b>
[1]=> 0
)
[1]=> Array(
[0]=> b
[1]=> 1
)
[2]=> Array(
[0]=> жирный текст
[1]=> 3
)
)
Как видите, массив $pockets по-прежнему содержит несколько элементов, однако, если раньше это были обычные строки, то с использованием PREG_OFFSET_CAPTURE — списки из двух элементов: array(подстрока, смещение).
int preg_match_all(string $expr, string $str, list &$pockets [,int $flags] [,int $offset])
Если функция preg_match() ищет только первое совпадение выражения в строке, то preg_match_all() ищет все совпадения. Смысл аргументов почти тот же. Функция возвращает число найденных подстрок (или 0, если ничего не найдено).
Формат результата, который окажется в $pockets (на этот раз аргумент уже обязателен), существенно зависит от параметра $flag, принимающего целое значение
428 |
Часть IV. Стандартные функции PHP |
(обычно используют константу). Однако в любом случае в $pockets окажется двумерный массив.
Перечислим возможные константы для параметра $flag.
PREG_PATTERN_ORDER
Список $pockets содержит элементы, упорядоченные по номеру открывающей скобки. Иными словами, к массиву нужно обращаться так: $pockets[B][N], где B — порядковый номер открывающей скобки в выражении, а N — номер совпа-
дения, если их было несколько. Например, в $pockets[0] будет содержаться список подстрок, полностью совпавших с выражением $expr в строке $str, в $pockets[1] — список совпадений, которым соответствует первая открывающая скобка (если она есть), и т. д. данный режим подразумевается по умолчанию.
PREG_SET_ORDER
Нам кажется, что это наиболее интуитивный режим поиска. Список $pockets оказывается отсортированным по номеру совпадения. Иными словами, сколько раз выражение $expr совпало в строке $str, столько элементов и окажется в $pockets. При этом каждый элемент будет иметь точно такую же структуру, как и при вызове обычной функции preg_match() — а именно, это список совпавших скобочных выражений (нулевой элемент — все выражение, первый — первая скобка и т. д.). Обращение к массиву организуется так: $pockets[N][B], где N — порядковый номер совпадения, а B — номер скобки.
PREG_OFFSET_CAPTURE
Это не отдельное значение флага, а просто величина, которую можно прибавить к PREG_PATTERN_ORDER или PREG_SET_ORDER. Она заставляет PHP возвращать цифровые смещения найденных элементов вместе с их значениями — точно так же, как было описано выше для функции preg_match().
Чтобы познакомиться на практике с различными способами сохранения результата, запустите программу из листинга 24.13.
Листинг 24.13. Файл match_all.php
<?php ## Различные флаги preg_match_all(). Header("Content-type: text/plain"); $flags = array(
"PREG_PATTERN_ORDER", "PREG_SET_ORDER", "PREG_SET_ORDER|PREG_OFFSET_CAPTURE",
);
$re = '|<(\w+).*?>(.*?)</\1>|s';
$text = "<b>текст</b> и еще <i>другой текст</i>"; echo "Строка: $text\n";
echo "Выражение: $re\n\n"; foreach ($flags as $name) {
preg_match_all($re, $text, $pockets, eval("return $name;")); echo "Флаг $name:\n";
Глава 24. Основы регулярных выражений в формате PCRE |
429 |
print_r($pockets);
echo "\n";
}
?>
Данный скрипт использует функцию eval(), которую мы описывали в предыдущей главе. Это сделано исключительно из соображений лаконичности кода. К сожалению, объем результата, генерируемого данным скриптом, не позволяет вставить его в книгу целиком. Вместо этого приведем фрагмент, соответствующий наиболее полезному флагу — PREG_SET_ORDER.
Array(
[0]=> Array(
[0]=> <b>текст</b>
[1]=> b
[2]=> текст
)
[1]=> Array(
[0]=> <i>другой текст</i>
[1]=> i
[2]=> другой текст
)
)
Замена совпадений
Все функции замены по регулярному выражению по умолчанию работают в однострочном режиме, как будто бы указан модификатор /s. Рекомендуется явно использовать /m, когда это необходимо.
mixed preg_replace(mixed $expr, mixed $to, mixed $str [,int $limit])
Вкратце действие функции таково: берется регулярное выражение $expr, ищутся все его совпадения в строке $str и заменяются на строку $to. Перед заменой специальные символы $0, $1 и т. д., а также \0, \1 и т. д. интерполируются: вместо них подставляются подстроки, соответствующие скобочным выражениям внутри $expr (соответственно, "нулевого" уровня — все совпадение, первого уровня — первая открывающая скобка и т. д.). Функция возвращает результат работы.
Если указан параметр $limit, то будет произведено не более $limit поисков и замен. Это удобно, если, например, нам нужно произвести однократную замену в строке — только первого совпадения.
Что еще изменилось с момента первого описания этой функции в начале главы? Конечно же, три первых параметров из string превратились в mixed. То есть они могут быть не только строками, но и массивами (точнее, списками). Это дает функции поистине колоссальные возможности.
Собственно, семантика аргументов этой функции почти в точности совпадает с семантикой функции str_replace(), которую мы рассматривали ранее.
430 |
Часть IV. Стандартные функции PHP |
Рассмотрим вначале, что происходит, если $str представляет собой список строк. Нетрудно догадаться: в этом случае замена производится в каждом элементе данного списка, и результат, также в виде списка, возвращается.
Если же $expr является списком регулярных выражений, а $to — обычной строкой, то все выражения из $expr будут поочередно найдены в $str и заменены на фиксированную строку $to.
Наконец, если и $expr, и $to являются списками, тогда PHP действует так. Он попарно извлекает элементы из $expr и $to и производит замену: $expr[$i] => $to[$i], где $i пробегает все возможные значения. Если очередного элемента $to[$i] не окажется (массив $to короче, чем $expr), то произойдет замена на пустую строку.
Ранее мы говорили, что модификатор /e в регулярных выражениях заставляет функцию выполнить заменяемую строку, как код на PHP, и использовать полученный результат для подстановки. Мы также указали на некоторые проблемы модификатора и пообещали их решить. Чем мы сейчас и займемся.
mixed preg_replace_callback(mixed $expr, string $funcName, mixed $str [, int $limit])
Вместо того чтобы сразу заменять найденные совпадения на строковые значения, эта процедура запускает функцию, чье имя передано ей в параметре $funcName, и передает ей содержимое карманов. Результат, возвращенный функцией, используется для подстановки.
Давайте напишем полностью корректный код, который переводит все теги в HTMLдокументе в верхний регистр (листинг 24.14).
Листинг 24.14. Файл replace_callback.php
<?php ## Функция preg_replace_callback() в действии.
//Пользовательская функция. Будет вызываться для каждого
//совпадения с регулярным выражением.
function toUpper($pockets) {
return $pockets[1].strtoupper($pockets[2]).$pockets[3];
}
$str = '<hTmL><bOdY bgcolor="white">Three captains, one ship.</bOdY></html>'; $str = preg_replace_callback('{(</?)(\w+)(.*?>)}s', "toUpper", $str);
echo htmlspecialchars($str); ?>
Мы получим такой результат:
<HTML><BODY bgcolor="white">Three captains, one ship.</BODY></HTML>
Как видите, все теги были корректно преобразованы.
Простор для творчества с использованием функции preg_replace_callback() поистине неисчерпаем. Собственно, она "умеет" все то же, что умеет preg_replace(). Вот только некоторые вещи, которые можно делать с ее помощью:
автоматически проставлять атрибуты width и height у тегов <img>, полученные в результате перехвата выходного потока скрипта (функции ob_start() и т. д.);
Глава 24. Основы регулярных выражений в формате PCRE |
431 |
реализовывать "умную" замену "псевдотегов" с параметрами (например, [font size=10]), что обычно требуется в форумах и гостевых книгах;
выполнять подстановки PHP-кода в различные шаблоны и т. д.
Разбиение по регулярному выражению
list preg_split(string $expr, string $str [,int $limit] [,int $flags])
Эта функция очень похожа на функцию explode(). Она тоже разбивает строку $str на части, но делает это, руководствуясь регулярным выражением $expr. А именно те участки строки, которые совпадают с этим выражением, и будут служить разделителями. Параметр $limit, если он задан, имеет то же самое значение, что и в функции explode(): возвращается список из не более чем $limit элементов, последний из которых содержит участок строки от ($limit — 1)-го совпадения до конца строки.
Параметр $flag может принимать перечисленные ниже значения (можно также указывать несколько значений, сложив их или воспользовавшись оператором |).
PREG_SPLIT_NO_EMPTY
Из результирующего списка будут удалены элементы, равные пустой строке.
PREG_SPLIT_DELIM_CAPTURE
В список будут включены не только участки строк между совпадениями, но также и сами совпадения. Это очень удобно, если где-то ниже в программе необходимо определить, какое именно совпадение вызвало разбиение строки в данной позиции.
PREG_SPLIT_OFFSET_CAPTURE
Этот вездесущий флаг делает все то же самое: вместо того, чтобы возвращать массив строк, функция вернет массив списков. Каждый такой список — это пара: (подстрока, позиция), где позиция — это смещение очередного "кусочка" строки относительно начала $str.
Наверное, вы уже догадались, что функция preg_split() работает гораздо медленнее, чем explode(). Однако она, вместе с тем, имеет впечатляющие возможности, в чем мы очень скоро убедимся. И все же не стоит применять preg_split() там, где прекрасно подойдет explode(). Чаще всего этим грешат программисты, имеющие некоторый опыт работы с Perl, потому что в Perl для разбиения строки на составляющие есть только функция split().
Выделение всех уникальных слов из текста
Представьте, что перед нами некоторый довольно длинный текст в переменной $text. Необходимо выделить из него все слова и оставить из них только уникальные. Результат должен быть представлен в виде списка. Решение этой задачи может потребоваться, например, при написании индексирующей поисковой системы на PHP.
Воспользуемся функцией preg_split() — листинг 24.15.
432 |
Часть IV. Стандартные функции PHP |
Листинг 24.15. Файл uniq.php
<?php ## Выделение уникальных слов в тексте.
//Эта функция выделяет из текста в $text все уникальные слова и
//возвращает их список. В необязательный параметр $nOrigWords
//помещается исходное число слов в тексте, которое было
//до "фильтрации" дубликатов.
function getUniques($text, &$nOrigWords=false) { // Сначала получаем все слова в тексте.
$words = preg_split("/([^[:alnum:]]|['-])+/s", $text); $nOrigWords = count($words);
//Затем приводим слова к нижнему регистру. $words = array_map("strtolower", $words);
//Получаем уникальные значения.
$words = array_unique($words); return $words;
}
// Пример использования функции. setlocale(LC_ALL, '');
$fname = "largetextfile.txt"; $text = file_get_contents($fname); $uniq = getUniques($text, $nOrig); echo "Было слов: $nOrig<br>";
echo "Стало слов: ".count($uniq)."<hr>"; echo join(" ", $uniq);
?>
Данный пример довольно интересен, т. к. имеет довольно большую функциональность при небольшом объеме. Его "сердце" — функции preg_split() и array_unique(), встроенные в PHP.
Обратите внимание, что в программе нет ни одного цикла. Всю работу берут на себя функции PHP. Это еще раз подчеркивает тот факт, что в PHP существуют средства практически на все случаи жизни.
Как вы думаете, сколько в среднем слов отсеется, как дубликаты, в типичном файле? Возьмем, например, файл с диалогами на английском языке, занимающий 55 Кбайт (этот файл имеется в архиве с исходными кодами, доступном на сайте книги). При запуске скрипт рапортует:
Было слов: 10339
Стало слов: 1621
Как видите, число слов уменьшилось более чем в 6 раз!
Экранирование символов
Ранее мы неоднократно пользовались тем фактом, что спецсимволы вроде +, * и т. д. необходимо экранировать обратными слэшами, если мы хотим, чтобы они
Глава 24. Основы регулярных выражений в формате PCRE |
433 |
потеряли свое "специальное" назначение. Если мы задаем регулярное выражение для поиска в явном виде, никаких проблем нет: мы можем расставить "зубочистки" вручную. Но как быть, если выражение формируется динамически?
string preg_quote(string $str [,string $bound])
Функция принимает на вход некоторую строку и экранирует в ней следующие символы:
. \ + * ? [ ^ ] $ () { } = ! < > | :
Дополнительно также экранируется символ, заданный в $bound (если указан). Как видите, в списке, перечисленном выше, популярный ограничитель / не упоминается. Именно его и нужно писать в $bound в большинстве случаев.
Давайте рассмотрим пример использования этой функции. Пусть у нас в переменной $highlight хранится некоторое слово (или фраза с пробелами). Мы бы хотели выделить эту фразу жирным шрифтом в тексте HTML-страницы (например, это может пригодиться для подсвечивания найденного контекста в результатах поискового скрипта). Задача осложняется тем, что во фразе могут присутствовать пробелы, которым в тексте соответствуют сразу несколько разных пробельных символов. Кроме того, фраза может содержать спецсимволы регулярных выражений, которые необходимо трактовать как обычные знаки.
Следующий фрагмент решает задачу.
// Формируем регулярное выражение для поиска.
//Сначала экранируем все спецсимволы. $re = preg_quote($highlight, "/");
//Затем заменяем пробельные символы на \s+ — это позволит совпадать
//пробелам в $highlight с любыми пробельными символами в $text.
$re = preg_replace('/\s+/', '\\s+', $re);
// Подсвечиваем слово.
echo preg_replace("/($re)/s", '<b>$1</b>', $text);
Фильтрация массива
ВОС Unix существует очень полезная утилита grep. Она принимает на свой вход текстовые строки, сверяет каждую из них с некоторым регулярным выражением и печатает только те строки, для которых нашлось совпадение.
ВPHP существует похожая функция, и называется она preg_grep().
array preg_grep(string $expr, array $input [, int $flags])
Данная функция возвращает только те строки из массива $input, для которых было обнаружено совпадение с регулярным выражением $expr. Ключи массива при этом сохраняются.
Параметр $flags, если он указан, может принимать одно-единственное значение: PREG_GREP_INVERT. Нетрудно догадаться, что оно делает: заставляет функцию "инвертировать" результат работы, т. е. вернуть несовпавшие строки из массива $input.
В листинге 24.16 приведен скрипт, который распечатывает имена всех файлов в текущем каталоге. Чтобы имя файла попало в распечатку, оно должно начинаться на