Сегодня мы продолжим тему, начатую в предыдущем выпуске и поговорим о функциях 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) |