Программирование на PHP. Шестой выпуск. Регулярные выражения - специальные функции. Примеры.
Сегодня мы продолжим тему, начатую в предыдущем выпуске и поговорим о функциях PHP для работы с регулярными выражениями. Но сначала немного информации, которая, безусловно, заинтересует каждого кто программирует на PHP.
Новости
В мире PHP ожидается сразу 2 революции :-) И это радует, потому как обещает нам еще больше мощи и удобства в программировании на нашем любимом языке! Но обо всем по порядку.
-
В недрах www.php.net зреет новая версия PHP! Причем не очередная версия из серии 4.0.x, а новая версия 4.1.0! Судя по смене minor version вместо номера micro version нас ждут значительные изменения и дополнения. Стоит вспомнить о том, что, к примеру, PHP3 так и не дожил до смены номера версии на 3.1.x (последняя версия имеет номер 3.0.18). Что именно готовят нам разработчики - пока неизвестно, никакой информации о новой версии на официальном сайте нет. Но особо нетерпеливые могут "пощупать" новую версию уже сейчас, скачав ее по этому адресу:
http://www.php.net/~zeev/php-4.1.0RC2.tar.gz
Как видите - это версия 4.1.0 release candidate
2. Естественно, что никаких windows binaries там нет, только исходники (причем, скорее всего в виде, пригодном для сборки только под Unix). Если у вас есть возможность скачать/собрать эту версию - вам повезло, если же нет - придется ждать официального выпуска новой версии.
Вообще с новыми версиями PHP творится что-то странное... Возможно это отчасти объясняется тем, что разработчики языка заняты созданием этой новой версии и не хотят отвлекаться на мелочи, возможно еще чем-то - не знаю. Но судите сами. Последняя версия PHP, доступная для скачивания с официального сайта - 4.0.6. В то же время на сайте www.php4win.com мы с удивлением можем обнаружить версию... 4.0.8! :-) Правда, это т.н. "версия для разработчиков", но возникает закономерный вопрос: "А где, в таком случае версия 4.0.7"? Ответ лично мне неизвестен... Кстати, если кто-то хочет скачать и попробовать версию 4.0.8 - он может взять ее здесь.
-
Еще более важная новость - компания Zend Technologies объявила о разработке Zend Engine 2.0! Если кто-то вдруг не знает, то Zend Engine - это "сердце и мозги" PHP, его ядро. Поэтому информация о разработке новой версии ядра так важна - это обещает нам действительно нечто совершенно новое, то, чего в PHP еще не было. Любой желающий может познакомиться с описанием нововведений, планируемых в новой версии ядра, скачав документ: Zend Engine version 2.0. Feature Overview and Design (в формате PDF) или прочитав "выжимку" из этого документа
здесь. Здесть я приведу лишь краткий список основных нововведений:
- Новая объектная модель. Многие замечают, что сейчас объекты в PHP реализованы несколько "странно" и неудачно. Новая объектная модель будет более похожа на ту, что реализована в Java и у нас наконец-таки появятся деструкторы, защищенные переменные, множественное наследование и т.п.
- Поддержка исключений. Будут реализованы такие операторы как
try , catch и throw , подобно тому как они реализованы в C++ и Java.
- Улучшенная поддержка национальных символов и Unicode.
Остается только ждать, когда все эти вкусности будут реализованы на практике (авторы говорят, что на это потребутеся несколько месяцев). Вполне возможно, что после этого нас ожидает уже PHP 5.0!
А теперь вернемся непосредственно к теме этого выпуска.
Регулярные выражения
Функции PHP для работы с регулярными выражениями
В PHP существует несколько функций для работы с регулярными выражениями. Все они используют один и тот же парсер регулярных выражений для своей работы, но при этом преследуют различные цели. Ниже мы рассмотрим все эти функции. Я буду приводить описание синтаксиса каждой функции в том виде, в котором она описана в PHP Manual, чтобы вам легче было разобраться.
Синтаксис:
int preg_match (string pattern, string subject [, array matches])
Эта функция предназначена для проверки того, совпадает ли заданная строка (subject ) с заданным регулярным выражением (pattern ). В качестве результата функция возвращает 1 , если совпадения были найдены и 0 , если нет. Если при вызове функции был задан необязательный параметр matches , то после работы функции ему будет присвоен массив, содержащий результаты поиска по заданному регулярному выражению. Заметьте, что вне зависимости от того, сколько именно совпадений было найдено при поиске - вам будет возвращено только первое совпадение. Рассмотрим пример того, как это работает:
<?php
$str = "123 234 345 456 567"; // Строка для поиска
$result = preg_match('/\d{3}/',$str,$found); // Производим поиск
echo "Matches: $result<br>"; // Выводим количество найденных совпадений
print_r($found); // Выводим результат поиска
?>
Результатом работы этой программы будет:
Matches: 1
Array
(
[0] => 123
)
Если вы внимательно прочитали предыдущий выпуск и понимаете, как работают регулярные выражения, то вы должны заметить, что реально функция preg_match() обнаружила в заданной строке 5 совпадений с заданным выражением, но вернула только первое из них. Казалось бы, что в этом случае было бы логичнее возвращать результаты поиска в виде строки, а не в виде массива, но это не так. Вспомните, что регулярное выражение может содержать в себе внутренние регулярные выражения, которые также возращают результат. А для того, чтобы вернуть результаты поиска по всем регулярным выражениям нам как раз и требуется массив. Для того, чтобы проиллюстрировать сказанное выше давайте немного изменим регулярное выражение и посмотрим на результат:
<?php
$str = "123 234 345 456 567";
// Теперь мы не просто ищем трехзначное число,
// но и получаем его среднюю цифру
$result = preg_match('/\d(\d)\d/',$str,$found);
echo "Matches: $result<br>";
print_r($found);
?>
Результат будет следующим:
Matches: 1
Array
(
[0] => 123
[1] => 2
)
Как видите - здесь присутствуют результаты поиска по всем имеющимся регулярным выражениям.
Синтаксис:
int preg_match_all (string pattern, string subject, array matches [, int order])
Эта функция очень похожа на предыдущую и предназначена для тех же самых целей. Единственное ее отличие от preg_match() состоит в том, что она осуществляет "глобальный" поиск в заданном тексте по заданному регулярному выражению и, соответственно, находит и возвращает все имеющиеся совпадения. Посмотрим, как отличается работа этой функции на том же самом примере:
<?php
$str = "123 234 345 456 567";
$result = preg_match_all('/\d{3}/',$str,$found);
echo "Matches: $result<br>";
print_r($found);
?>
Результат работы:
Matches: 5
Array
(
[0] => Array
(
[0] => 123
[1] => 234
[2] => 345
[3] => 456
[4] => 567
)
)
Как видите - здесь мы получили все найденные совпадения и их количество в качестве результата.
Необходимо обратить ваше внимание на дополнительный параметр, появившийся в этой функции по сравнению с preg_match() : order . Значение этого параметра определяет структуру выходного массива с найденными совпадениями. Его значение может быть одним из перечисленных ниже:
PREG_PATTERN_ORDER - результаты поиска будут сгруппированы по номеру регулярного выражения, которое возвратило этот результат (это значение используется по умолчанию).
PREG_SET_ORDER - результаты поиска будут сгруппированы по месту их нахождения в тексте
Для того, чтобы лучше понять разницу между этими значениями, посмотрим на результат работы одного и того же скрипта при использовании каждого из них:
Сначала посмотрим на то, как выглядит результат при использовании PREG_PATTERN_ORDER :
<?php
$str = "123 234 345 456 567";
$order = PREG_PATTERN_ORDER ;
$result = preg_match_all('/\d(\d)\d/',$str,$found,$order);
print_r($found);
?>
Результат:
Array
(
[0] => Array
(
[0] => 123
[1] => 234
[2] => 345
[3] => 456
[4] => 567
)
[1] => Array
(
[0] => 2
[1] => 3
[2] => 4
[3] => 5
[4] => 6
)
)
Как видите - массив результатов содержит внешние индексы, соответствующие номерам регулярных выражений, от которых получен результат (индекс 0 имеет основное регулярное выражение). По этим индексам в массиве расположены массивы, содержащие непосредственно найденную информацию, причем индекс в этом внутреннем массиве соответствует "порядковому номеру" данного фрагмента в исходном тексте.
Теперь попробуем то же самое, но с PREG_SET_ORDER :
<?php
$str = "123 234 345 456 567";
$order = PREG_SET_ORDER ;
$result = preg_match_all('/\d(\d)\d/',$str,$found,$order);
print_r($found);
?>
Результат:
Array
(
[0] => Array
(
[0] => 123
[1] => 2
)
[1] => Array
(
[0] => 234
[1] => 3
)
[2] => Array
(
[0] => 345
[1] => 4
)
[3] => Array
(
[0] => 456
[1] => 5
)
[4] => Array
(
[0] => 567
[1] => 6
)
)
Как видите - здесь основной массив содержит результаты поиска, сгруппированные по порядку их нахождения в тексте, причем каждый результат представляет собой массив с результатами поиска по этому найденному фрагменту для всех имеющихся регулярных выражений.
Синтаксис:
mixed preg_replace (mixed pattern, mixed replacement, mixed subject [, int limit])
Эта функция позволит вам произвести замену текста по регулярному выражению. Как и в предыдущих функциях, здесь производится поиск по регулярному выражению pattern в тексте subject , и каждый найденный фрагмент текста заменяется на текст, заданный в replacement . Задание необязятельного параметра limit позволит ограничить количество заменяемых фрагментов в тексте.
Например, нам необходимо "сжать" текст, убрав из него все лишние пробелы и символы перевода строки:
<?php
$text = "there is\t\n\t\t some text \n \t just \n\n\n for test";
echo "<b>Перед заменой:</b>\n$text\n\n";
$text = preg_replace("/(\n \s{2,})/"," ",$text);
echo "<b>После замены:</b>\n$text";
?>
Результатом работы данной программы будет следующий текст:
Перед заменой:
there is
some text
just
for test
После замены:
there is some text just for test
Как видите - всего одна строчка позволила нам решить достаточно нетривиальную в обычной практике задачу. Объяснять само регулярное выражение я не буду, если вы внимательно прочитали предыдущий выпуск - понять его вам будет несложно.
Однако основная прелесть этой функции, которая и придает ей всю ее мощь - это тот факт, что вы можете ссылаться на результаты поиска при генерации замещающего текста. В качесте примера покажу, как можно очень быстро и элегантно решить задачу, которая возникает достаточно часто - конвертация дат из одного формата в другой. Как вы знаете, на Западе обычно используется формат mm/dd/yyyy , тогда как у нас обычно - dd.mm.yyyy . Следующий пример осуществляет конвертацию дат между этими форматами в заданном тексте:
<?php
$text = 'Today is 11/16/2001';
$text = preg_replace("/(\d{2})\/(\d{2})\/(\d{4})/","\\2.\\1.\\3",$text);
echo $text;
?>
Результат работы этой программы:
Today is 16.11.2001
Обратите внимание на текст, используемый для замены. В нем использованы т.н. backreferences, т.е. ссылки на найденный ранее текст. Всего таких ссылок может быть не более 100 с номерами от 0 до 99 (соответственно в тексте они выглядят как \0 , \1 , \2 ... \99 ). Backreference с номером 0 будет заменена на весь найденный текст, \1 - на текст, найденный первым внутренним регулярным выражением, \2 - вторым и т.д. Номерв внутренним регулярным выражениям присваиваются по мере их находжения в тексте, т.е. слева-направо. В нашем случае \1 - это месяц, \2 - день, \3 - год.
Помимо стандартного синтаксиса регулярных выражений, в PHP, совместно с функцией preg_replace() используется еще один дополнительный модификатор - 'e '. Его использование заставляет PHP рассматривать текст замены не как текст, а как PHP код, что дает возможность еще больше расширить сферу применения этой функции в вашем коде. Следующий пример демонстрирует использование этого модификатора - он производит замену всех целых десятичных чисел в тексте на их шестнадцатиричные эквиваленты:
<?php
$text = "123 234 345 456 567";
$text = preg_replace("/\d+/e","'0x'.dechex('\\0')",$text);
print_r($text);
?>
Результатом работы этой программы будет:
0x7b 0xea 0x159 0x1c8 0x237
И еще одно. Функция preg_replace() также умеет работать с массивами регулярных выражений. Т.е. это позволит вам осуществить поиск и замену сразу по множеству условий! В качестве примера приведу фрагмент кода, описанный в PHP Manual и осуществляющий конвертацию HTML документа в текст при помощи всего лишь одного вызова preg_replace() !
// $document should contain an HTML document.
// This will remove HTML tags, javascript sections
// and white space. It will also convert some
// common HTML entities to their text equivalent.
$search = array ("'<script[^>]*?>.*?</script>'si", // Strip out javascript
"'<[\/\!]*?[^<>]*?>'si", // Strip out html tags
"'([\r\n])[\s]+'", // Strip out white space
"'&(quot #34);'i", // Replace html entities
"'&(amp #38);'i",
"'&(lt #60);'i",
"'&(gt #62);'i",
"'&(nbsp #160);'i",
"'&(iexcl #161);'i",
"'&(cent #162);'i",
"'&(pound #163);'i",
"'&(copy #169);'i",
"'&#(\d+);'e"); // evaluate as php
$replace = array ("",
"",
"\\1",
"\"",
"&",
"<",
">",
" ",
chr(161),
chr(162),
chr(163),
chr(169),
"chr(\\1)");
$text = preg_replace ($search, $replace, $document);
Сами по себе регулярные выражения очень просты, интересно лишь их совместное использование для решения общей задачи.
Синтаксис:
mixed preg_replace_callback (mixed pattern, mixed callback, mixed subject [, int limit])
Эта функция является расширенной версией функции preg_replace() (хотя, казалось бы, чего еще можно пожелать?). Единственным отличием ее от preg_replace() является то, что в качестве текста для замены в ней задается не сам текст, а имя функции, которая будет производить обработку найденного текста и возвращать замещающий текст. Т.е. с использованием этой функции мощь инструментария PHP по обработке текста становится поистине безграничной! В качестве примера хочу привести фрагмент кода, который выполняет работу, аналогичную той, что производится механизмом сессий в PHP: добавление дополнительного аргумента (идентификатора сессии) к каждой ссылке внутри HTML документа.
<?php
// Список тегов и аттрибутов, к котроым необходимо
// добавить дополнительный параметр.
// Формат строки:
// <имя тега>[ <имя аттрибута>]+
// Т.е. сначала идет имя тега, а затем, через пробел,
// имена одного или нескольких аттрибутов.
$tagsList = array(
'a href',
'area href',
'frame src',
'input src',
'img src',
'form action'
);
// Идентификатор сессии
$sid = 12345;
// HTML документ для обработки. Здесь, в качестве примера
// мы берем его из внешнего файла, но вообще-то метод
// получения исходного документа может быть различным.
$document = join('',file('document.html'));
// Начинаем обработку всех тегов, указанных в массиве $tagsList
foreach($tagsList as $tag)
{
// Разделяем список аттрибутов на составляющие
$attrs = explode(' ',$tag);
// Получаем имя тега (в массиве $attrs остается лишь список аттрибутов)
$tag = array_shift($attrs);
// Выполняем "патч" всех имеющихся в документе ссылок, содержащихся
// в каждом из аттрибутов текущего тега
foreach($attrs as $attr)
$document = preg_replace_callback("/<".$tag.".+?".$attr."=[\'\"](.+?)[\'\"]/si",
'callback',$document);
};
// Выводим документ и выходим
echo $document;
exit();
// Эта функция будет вызываться для каждой найденной
// ссылки в тексте HTML документа.
// На входе она получает результат поиска (массив,
// аналогичный возвращаемому функцией preg_match()).
// На выходе из функции должна быть строка с текстом замены.
function callback($data)
{
// Регулярное выражение, использованное для поиска находит полные
// HTML теги, содержащие аттрибуты, в которых могут находиться
// URL адреса. Поскольку текст, возвращаемый данной функцией будет
// использован для замещения всего найденного фрагмента текста -
// нам необходимо взять полный текст, чтобы не потерять его при
// дальнейшей обработке. Он будет возвращен без изменений, если
// окажется, что аттрибут не содержит URL адреса.
$href = $data[0];
// Используем функцию PHP для разбора URL адреса на составляющие.
// В качестве "исходного материала" передаем содержимое интересующего
// нас аттрибута, найденного внутренним регулярным выражением.
// Подробнее о том, что возвращает эта функция см. PHP Manual.
$parts = parse_url($data[1]);
// Мы должны добвлять идентификатор сессии только к ссылкам, которые
// являются "локальными" для данного сайта. Т.е. мы не должны обрабатывать:
// - полные URL адреса (<a href="http://www.php.net/">)
// - указатели на "якоря" внутри страницы (<a href="#part2">)
if ((!isset($parts['scheme'])) && // Если URL содержит идентификатор
(!isset($parts['host'])) && // протокола или имя домена - это
// полный URL адрес.
(substr($data[1],0,1)!='#')) // Если URL начинается с символа '#'
// то это ссылка на "якорь" внутри страницы
{
// Берем путь к странице, указанный в URL и добавляем разделитель для параметров
// потому что нам необходимо будет добавить по крайней мере 1 параметр
$href = $parts['path'].'?';
// Если в этом URL уже были какие-либо параметры - добавляем их и добавляем
// разделитель. Заметьте, что в качестве разделителя используется &, а не &,
// это позволяет нам добиться совместимости с XHTML.
if (isset($parts['query']))
$href .= $parts['query'].'&';
// Добавляем наш собственный параметр - идентификатор сессии
$href .= 'sid='.$GLOBALS['sid'];
// Если в оригинальном URL была ссылка на фрагмент документа - возвращаем ее
// на место.
if (isset($parts['fragment']))
$href .= '#'.$parts['fragment'];
// "Вставляем" новый URL на место того, который был там раньше
$href = str_replace($data[1],$href,$data[0]);
};
// Возвращаем результат
return($href);
};
?>
Пример может показаться немного громоздким, но это исключительно из-за обилия комментариев.
Синтаксис:
array preg_split (string pattern, string subject [, int limit [, int flags]])
Данная функция выполняет действие, аналогичное функциям split() и explode() - разбивает строку на части по какому-либо признаку и возвращает массив, содержащий части строки. Однако ее возможности по заданию правил разбиения больше, чем у этих функций, потому что в ее основе лежит механизм регулярных выражений, в мощи которого, я надеюсь, вы уже смогли убедиться. Если говорить более конкретно, то строка subject разбивается на части по разделителю, заданному регулярным выражением pattern . При этом количество фрагментов может быть ограничего необязятельным параметром limit . Кроме того эта функция поддерживает необязательный параметр flags , который позволяет в некоторой степени контролировать процесс разбиения строки.
Параметр flags может принимать следующие значения (или их комбинации с использованием знака ' '):
PREG_SPLIT_NO_EMPTY - возвращать только непустые части строк, полученные в результате разбиения.
PREG_SPLIT_DELIM_CAPTURE - возвращать также результаты поиска по внутренним регулярным выражениям.
Рассмотрим пару примеров. Для начала - выражение, которое разбивает произвольный текст на отдельные слова:
<?php
$text = join('',file('my_text.txt'));
$words = preg_split("/\s+/s",$text);
print_r($words);
?>
Как видите - мы получаем содержимое файла 'my_text.txt ' в виде строки, разбиваем его на отдельные слова и выводим содержимое массива слов, чтобы убедиться, что все работает правильно.
Еще один пример производит разбиение заданного слова на буквы (он описан в PHP Manual):
<?php
$str = 'string';
$chars = preg_split('//',$str,-1,PREG_SPLIT_NO_EMPTY);
print_r($chars);
?>
Значение -1 для параметра limit означает отсутствие лимита.
Синтаксис:
string preg_quote (string str [, string delimiter])
Эта функция - единственная, не относящаяся непосредственно к механизму регулярных выражений. Ее назначение - "квотинг" символов, имеющих специальное значение в синтаксисе регулярных выражений. Обычно это символы:
. \ + * ? [ ^ ] $ ( ) { } = ! < > :
Все эти символы, встречающиеся в строке будут "отквочены" путем добавления символа '\ ' непосредственно перед каждым из них. Модифицированная таким образом строка будет возвращены в качестве результата.
Эта фцнкция также имеет необязательный параметр delimiter . Если этот параметр задан, то символ, переданный в качестве этого параметра тоже будет "отквочен" данной функцией.
Синтаксис:
array preg_grep (string pattern, array input)
Действие этой функции похоже на действие команды grep в Unix. Она ищет текст по регулярному выражению pattern , в массиве input и возвращает новый массив, содержащий только элементы, в которых были найдены совпадения с заданным регулярным выражением. К примеру у нас есть файл, содержащий в каждой строке числовую и текстовую информацию. Нам необходимо получить из этого файла только строки, содержащие числа:
Файл data.txt:
123
abc
php4
Код:
<?php
// Считываем содержимое файла в массив
$data = file('data.txt');
// Получаем массив, содержащий цифровую информацию
$numbers = preg_grep("/\d+/",$data);
// Выводим результат работы
print_r($numbers);
?>
Результат работы будет:
Array
(
[0] => 123
[2] => php4
)
Как видите - мы получили все строки, содержащие цифры. Если же нам, например нужно получить только цифры - то выражение необходимо немного изменить: /^\s*\d+\s*$/ .
Заключение
В течение последних двух выпусков мы рассмотрели работу с регулярными выражениями в PHP. Это очень выжный материал который мы часто будем использовать в дальнейшем. Если вы усвоили его - тогда мы можем двигаться дальше.
Александр Грималовский (Flying)
Пятый выпуск. Введение в регулярные выражения. Синтаксис.
Седьмой выпуск. Работа с шаблонами.
|