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

Самоучитель по PHP 4

.pdf
Скачиваний:
82
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

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

301

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

abc[[:alnum:]]+

//

abc,

затем

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

abc[[:alpha:][:punct]0]

//

abc,

далее

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

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

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

Еще одно привлекательное свойство выражений [:что_то:] заключается в том, что они автоматически учитывают настройки локали, а значит, правильно работают с "русскими" буквами (конечно, если перед этим была вызвана функция setlocale() с верными параметрами). Таким образом, выражение [[:alpha:]]+ удовлетворяет

302

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

любому слову как на английском, так и на русском языке. Добиться этого при помощи "обычного" использования [...] было бы очень тяжело.

Отрицательные группы

Иногда (когда альтернативных символов много) бывает довольно утомительно перечислять их всех в квадратных скобках. Особенно обидно выходит, если нас устраивает любой символ, кроме нескольких (например, кроме > и <). В этом случае, конечно, не стоит указывать 254 символа, вместо этого лучше воспользоваться конструкцией [^<>], которая обозначает любой символ, кроме тех, которые перечислены после [^ и до ]. Например, выражение a[^ \t\n\r]b "срабатывает" на все строки, содержащие буквы a и b, разделенные любым не пробельным символом.

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

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

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

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

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

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

Язык PCRE, в отличие от RegEx, позволяет ограничивать "жадность" кванти- фикаторов.

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

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

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

303

более совпадений" — символом плюса +. С его помощью можно было бы выражение записать лаконичнее: a-+, что буквально и читается как "a и один или более минусов". Вот пример выражения, которое определяет, есть ли в строке английское слово, написанное через дефис: [a-zA-Z]+-[a-zA-Z]+.

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

И уж чтобы совсем облегчить жизнь, иногда используют еще один квантификатор — знак вопроса ?. Он обозначает, что предыдущий символ может быть повторен ноль или один (но не более!) раз. Например, выражение [a-zA_Z]+\r?\n определяет строки, в которых последнее слово прижато к правому краю строки. Если мы работаем в Unix, то там в конце строки символ \r обычно отсутствует, тогда как в текстовых файлах Windows каждая строка заканчивается парой \r\n. Для того чтобы сценарий правильно работал в обоих системах, мы должны учесть эту особенность — возможное наличие \r перед концом строки.

Заданное число совпадений

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

rX{n,m} — указывает, что символ X может быть повторен от n до m раз;

rX{n} — указывает, что символ X должен быть повторен ровно n раз;

rX{n,} — указывает, что символ X может быть повторен n или более раз.

Значения n и m в этих примерах обязательно должны принадлежать диапазону от 0 до 255 включительно. В качестве тренировки вы можете подумать, как будут выглядеть квантификаторы *, + и ? в терминах {...}.

Мнимые символы

Мнимые символы — это просто участок строки между соседними символами (да, именно так, как это ни абсурдно), удовлетворяющий некоторым свойствам. Фактически, мнимый символ — это некая позиция в строке. Например, символ ^ соответствует началу строки (заметьте: не первому символу строки, а в точности началу строки, позиции перед первым символом), а $ — ее концу (опять же, позиции за концом строки).

Чтобы это понять, давайте рассмотрим выражение ^abc, которое соответствует любой строке, начинающейся с abc, и выражение abc$, соответствующее строке с abc на "хвосте". Наконец, выражение ^abc$ сопоставимо только со строкой abc, и в этом смысле оно эквивалентно сравнению на равенство.

304

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

Существуют еще два мнимых символа, задающих начало и конец слова. Первый из них обозначается как [[:<:]] и указывает на позицию перед первой буквой очередного слова. Последний записывается в виде [[:>:]] и сигнализирует о позиции после последнего символа слова. Под словом здесь понимается фрагмент строки, удовлетворяющий выражению [[:alnum:]]+, т. е., любая последовательность из букв и цифр.

Язык RegEx поддерживает только четыре уже рассмотренных нами мнимых символа. Этого нельзя сказать о формате PCRE, в котором, наоборот, количе- ство таких символов доведено до абсурда.

Вот пример использования мнимых символов:

$st=" string "; if(ereg("[[:<:]]([[:alnum:]]+)[[:>:]]",$st,$Pock))

echo "Найдено слово: $Pock[1]";

Оператор альтернативы

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

Но в языке RegEx есть возможность задавать альтернативы не одиночных символов, а сразу их групп. Это делается при помощи оператора |.

Вот несколько примеров его работы.

rВыражение 1|2|3 полностью эквивалентно выражению [123], но сопоставление происходит несколько медленнее.

rВыражению aaa|^a|z$|zzz соответствуют строки, в которых есть подстрока aaa, либо которые начинаются на a, либо оканчиваются на z, либо, наконец, содержат подстроку zzz.

rВыражению abc1|abc22|abc333 соответствуют строки, в которых встречаются подстроки abc1, abc22 или abc333 (а возможно, и все три одновременно).

Группирующие скобки

Последний пример наводит на рассуждения о том, нельзя ли как-нибудь сгруппировать отдельные символы, чтобы не писать по несколько раз одно и то же. В нашем примере строка abc встречается в выражении аж 3 раза. Но мы не можем написать выражение так: abc1|22|333, потому что оператор |, естественно, пытается применить себя к как можно более длинной последовательности команд.

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

305

Именно для цели управления оператором альтернативы (но не только) и служат группирующие круглые скобки (...). Нетрудно догадаться по смыслу, что выражение из последнего примера можно записать с их помощью так: abc(1|22|333).

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

"Карманы"

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

Вот пример, проясняющий ситуацию. Пусть нам в строке задана дата в формате DD- MM-YYYY, и в ней могут встретиться паразитные пробелы в начале и конце. Нас интересует, что же все-таки за дату нам передали. То есть, мы точно знаем, что эта строка — именно дата, но вот где в ней день, где месяц и где год?

Посмотрим, что же предлагает нам RegEx и PHP для решения рассматриваемой задачи. Для начала установим, что все правильные даты должны соответствовать выражению

^ *(([0-9]+)-([0-9]+)-([0-9]+)) *$

Для простоты мы не проверяем, что длина каждого поля не должна превышать 2 (для года — 4) символа. Все строки, не удовлетворяющие этому выражению, заведомо не являются датами.

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

Обратите еще раз внимание на порядок нумерации карманов она идет по номеру открывающейся скобки.

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

Как получить содержимое наших карманов? Очень просто: как раз тот список, который передается по ссылке функции ereg() третьим параметром, и есть карманы.

306

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

Исходя из этого, имеем следующую программу на PHP, выполняющую требуемые действия:

$str=" 15-16-2000 "; // к примеру

// Разбиваем строку на куски при помощи ereg

ereg("^ *(([0-9]+)-([0-9]+)-([0-9]+)) *$",$str,$Pockets); // Теперь разбираемся с карманами

echo "Дата без пробелов: $Pockets[1] <br>" echo "День: $Pockets[2] <br>";

echo "Месяц: $Pockets[3] <br>"; echo "Год: $Pockets[4] <br>";

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

^ *([0-9]+) *[-./] *([0-9]+) *[-./] *([0-9]+) *$

Использование карманов в функции замены

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

Предположим, нам нужно все слова в строке, начинающиеся с "доллара" $, сделать "жирными", — обрамить тэгами <b> и </b>, — для последующего вывода в браузер. Это может понадобиться, если мы хотим текст некоторой программы на PHP вывести так, чтобы в нем выделялись имена переменных. Очевидно, выражение для обнаружения имени переменной в строке будет таким: \$[a-zA-Z_][[:alnum:]]*.

Но как нам использовать его в функции ereg_Replace()? Вот фрагмент программы, которая делает это:

$str="<? $a=10; for($i=0; $i<10; $i++) echo $i; ?> // к примеру $str=ereg_Replace("(\\$[a-zA-Z_][[:alnum:]]*)","<b>\\1</b>",$str);

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

Нетрудно догадаться, как "оно" работает: просто во время замены везде вместо сочетания \1 подставляется содержимое кармана номер 1.

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

307

Использование карманов в функции сопоставления

И даже на том, что было описано выше, возможности карманов не исчерпываются. Мы можем задействовать содержимое карманов и в функции ereg() — раньше, чем закончится сопоставление. А именно, управлять ходом поиска на основе данных в карманах.

В качестве примера рассмотрим такую далеко не праздную задачу. Известно, что в строке есть подстрока, обрамленная какими-то HTML-тэгами (например, <b> или <pre>), но неизвестно, какими. Требуется поместить эту подстроку в карман, чтобы в дальнейшем с ней работать. Разумеется, закрывающий тэг должен соответствовать открывающему — например, к тэгу <b> парный — </b>, а к <pre> </pre>.

Задача решается с помощью такого регулярного выражения:

<([[:alnum:]]+)>([^<]*)</\1>

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

Вот фрагмент программы, который все описанное делает тремя строчками:

$str = "Hello, this <b>word</b> is bold!"; if(ereg("<([[:alnum:]]+)>([^<]*)</\\1>",$str,$Pockets))

echo "Слово '$Pockets[2]' обрамлено тэгом '<$Pockets[1]>'";

Дополнительные функции

bool eregi(string $expr, string $str [,list &$Matches])

То же, что и ereg(), только без учета регистра символов.

Хотя регистр и не учитывается при поиске, в карманах $Matches все найден- ные подстроки все же запишутся с точным сохранением регистра букв.

string eregi_replace(string $expr, strint $str, string $strToChange)

То же, что и ereg_replace(), но без учета регистра буквенных символов.

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

int quotemeta(string $str)

Часто бывает нужно гарантировать, чтобы в какой-то переменной-строке ни один символ не мог трактоваться как метасимвол. Этого можно добиться, предварив каждый из них наклонной чертой, что и делает функция quotemeta(). А именно, она "заслэшивает" следующие символы: . , \\, +, *, ? , [ ^ ] , ( $ ).

Перед | слэш почему-то не ставится. Будьте особо внимательны!

list split(string $pattern, string $string [,int $limit])

Эта функция очень похожа на explode(). Она тоже разбивает строку $string на части, но делает это, руководствуясь регулярным выражением $pattern. А именно, те участки строки, которые совпадают с этим выражением, и будут служить разделителями. Параметр $limit, если он задан, имеет то же самое значение, что и в функции explode() — а именно, возвращается список из не более чем $limit элементов, последний из которых содержит участок строки от ($limit-1)-го совпадения до конца строки.

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

list spliti(string $pattern, string $string [,int $limit])

Аналог функции split(), который делает то же самое, только при сопоставлении с регулярным выражением не учитывается регистр символов.

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

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

Имя и расширение файла

Задача: для имени файла в $fname установить расширение out независимо от его предыдущего расширения.

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

309

Решение:

$fname=ereg_Replace( '([[:alnum:]])(\\.[[:alnum:].]*)?$', '\\1.out',

$fname );

Обратите внимание на довольно интересную структуру этого выражения: мы не можем просто "привязать" его к концу строки при помощи $, что обусловлено спецификой работы RegEx. Мы также привязываем начало выражения к любой букве или цифре, которой оканчивается имя файла.

Имя каталога и файла

Цель: разбить полное имя файла $path на имя каталога $dir и и имя файла $fname. Средства:

$fname = ereg_Replace(".*[\\/]","",$path);

$dir = ereg_Replace("[\\/]?[^\\/]*$","",$path);

Проверка на идентификатор

Задача: проверить, является ли строка $id идентификатором, т. е. состоит ли она исключительно из алфавитно-цифровых символов (чтобы сделать задачу более интересной, договоримся также, что первым символом строки не может быть цифра).

Решение:

if(eregi("[a-z_][[:alnum:]]*",$id)) echo "Это идентификатор!";

Модификация тэгов

Задача: в тексте, заданном в $text, у всех тэгов <img> заменить в src расширение файла рисунка на gif, вне зависимости от того, какое расширение было у него до этого и было ли вообще.

Решение:

$text=eregi_Replace( '(<img[^>]*src="?[[:alnum:]/\\]*)(\\.[[:alnum:]]*)?', '\\1.jpg',

$text );

310

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

Преобразование гиперссылок

Задача: имеется текст, в котором иногда встречаются подстроки вида протокол://URL, где протокол — один из протоколов http, ftp или gopher, а URL какой-нибудь адрес в Интернете. Нужно заместить их на HTML-эквиваленты <a href=…>…</a>.

Решение:

$w="[:alnum:]";

 

$p="[:punct:]";

 

$text=eregi_Replace(

 

"((https?|ftp|gopher)://".

// протокол

"[$w-]+(\\.[$w-]+)*".

// имя хоста

"(/[$w+&.%]*(\\?[$w?+&%]*)?)?". // имя файла и параметры ")",

'<a href="\\1">\\1</a>', $text

);

Преобразование адресов E-mail

Задача: имеется текст, в котором иногда встречаются строки вида пользователь@хост, т. е. E-mail-адреса в обычном формате (или хотя бы большинство таких E-mail). Необходимо преобразовать их в HTML-ссылки.

Решение:

$text=eregi_Replace(

 

'([[:alnum:]-.]+@'.

// пользователь

'[[:alnum:]-]+(\\.[[:alnum:]-]+)*'.

// домен

'(\\?([[:alnum:]?+&%]*)?)?'.

// необязательные параметры

')',

 

'<a href="\\1">\\1</a>',

 

$text

 

);

 

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