Регулярные выражения - специальные функции. Примеры.
Сегодня мы продолжим тему, начатую в предыдущем выпуске и поговорим о функциях PHP для работы с регулярными выражениями. Но сначала немного информации, которая, безусловно, заинтересует каждого кто программирует на PHP.
Новости
В мире PHP ожидается сразу 2 революции :-) И это радует, потому как обещает нам еще больше мощи и удобства в программировании на нашем любимом языке! Но обо всем по порядку.
В недрах http://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. В то же время на сайте http://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, то после работы функции ему будет присвоен массив, содержащий результаты поиска по заданному регулярному выражению. Заметьте, что вне зависимости от того, сколько именно совпадений было найдено при поиске - вам будет возвращено только первое совпадение. Рассмотрим пример того, как это работает:
Если вы внимательно прочитали предыдущий выпуск и понимаете, как работают регулярные выражения, то вы должны заметить, что реально функция 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() состоит в том, что она осуществляет "глобальный" поиск в заданном тексте по заданному регулярному выражению и, соответственно, находит и возвращает все имеющиеся совпадения. Посмотрим, как отличается работа этой функции на том же самом примере:
Как видите - здесь мы получили все найденные совпадения и их количество в качестве результата.
Необходимо обратить ваше внимание на дополнительный параметр, появившийся в этой функции по сравнению с preg_match(): order. Значение этого параметра определяет структуру выходного массива с найденными совпадениями. Его значение может быть одним из перечисленных ниже:
PREG_PATTERN_ORDER - результаты поиска будут сгруппированы по номеру регулярного выражения, которое возвратило этот результат (это значение используется по умолчанию).
PREG_SET_ORDER - результаты поиска будут сгруппированы по месту их нахождения в тексте
Для того, чтобы лучше понять разницу между этими значениями, посмотрим на результат работы одного и того же скрипта при использовании каждого из них:
Сначала посмотрим на то, как выглядит результат при использовании PREG_PATTERN_ORDER:
Как видите - массив результатов содержит внешние индексы, соответствующие номерам регулярных выражений, от которых получен результат (индекс 0 имеет основное регулярное выражение). По этим индексам в массиве расположены массивы, содержащие непосредственно найденную информацию, причем индекс в этом внутреннем массиве соответствует "порядковому номеру" данного фрагмента в исходном тексте.
Теперь попробуем то же самое, но с PREG_SET_ORDER:
Как видите - здесь основной массив содержит результаты поиска, сгруппированные по порядку их нахождения в тексте, причем каждый результат представляет собой массив с результатами поиска по этому найденному фрагменту для всех имеющихся регулярных выражений.
Эта функция позволит вам произвести замену текста по регулярному выражению. Как и в предыдущих функциях, здесь производится поиск по регулярному выражению 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. Следующий пример осуществляет конвертацию дат между этими форматами в заданном тексте:
Обратите внимание на текст, используемый для замены. В нем использованы т.н. backreferences, т.е. ссылки на найденный ранее текст. Всего таких ссылок может быть не более 100 с номерами от 0 до 99 (соответственно в тексте они выглядят как \0, \1, \2 ... \99). Backreference с номером 0 будет заменена на весь найденный текст, \1 - на текст, найденный первым внутренним регулярным выражением, \2 - вторым и т.д. Номерв внутренним регулярным выражениям присваиваются по мере их находжения в тексте, т.е. слева-направо. В нашем случае \1 - это месяц, \2 - день, \3 - год.
Помимо стандартного синтаксиса регулярных выражений, в PHP, совместно с функцией preg_replace() используется еще один дополнительный модификатор - 'e'. Его использование заставляет PHP рассматривать текст замены не как текст, а как PHP код, что дает возможность еще больше расширить сферу применения этой функции в вашем коде. Следующий пример демонстрирует использование этого модификатора - он производит замену всех целых десятичных чисел в тексте на их шестнадцатиричные эквиваленты:
И еще одно. Функция 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);
Сами по себе регулярные выражения очень просты, интересно лишь их совместное использование для решения общей задачи.
Эта функция является расширенной версией функции 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 - возвращать также результаты поиска по внутренним регулярным выражениям.
Рассмотрим пару примеров. Для начала - выражение, которое разбивает произвольный текст на отдельные слова:
Как видите - мы получаем содержимое файла 'my_text.txt' в виде строки, разбиваем его на отдельные слова и выводим содержимое массива слов, чтобы убедиться, что все работает правильно.
Еще один пример производит разбиение заданного слова на буквы (он описан в PHP Manual):
Эта функция - единственная, не относящаяся непосредственно к механизму регулярных выражений. Ее назначение - "квотинг" символов, имеющих специальное значение в синтаксисе регулярных выражений. Обычно это символы:
. \ + * ? [ ^ ] $ ( ) { } = ! < > :
Все эти символы, встречающиеся в строке будут "отквочены" путем добавления символа '\' непосредственно перед каждым из них. Модифицированная таким образом строка будет возвращены в качестве результата.
Эта фцнкция также имеет необязательный параметр delimiter. Если этот параметр задан, то символ, переданный в качестве этого параметра тоже будет "отквочен" данной функцией.
Действие этой функции похоже на действие команды 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. Это очень выжный материал который мы часто будем использовать в дальнейшем. Если вы усвоили его - тогда мы можем двигаться дальше.