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

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

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

424

Часть 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 приведен скрипт, который распечатывает имена всех файлов в текущем каталоге. Чтобы имя файла попало в распечатку, оно должно начинаться на