Регулярные выражения в php: контекстный поиск

 | 17.01

Искомая строка описывается в паттерне при помощи разных конструкций, базовыми из которых являются перечисление (какие символы могут присутствовать) и повторение (сколько раз повторяется символ из определенного перечня). Например, телефон состоит из трех групп цифр (в первой группе три цифры, и первая из них не может быть нулем, в остальных по две цифры), разделенных дефисами. На языке регулярных выражений это записывается как [1-9]d{2}-d{2}-d{2}. Также мы рассматривали и другие, не менее важные конструкции – выбор из альтернатив (если может присутствовать только одна из нескольких последовательностей символов), исключение из диапазона и другие. В данной статье будет рассмотрен очень полезный инструмент, а именно — подмаски.

Эпизод 1. Вечный вопрос делимости целого

Наверное, особо любопытные читатели уже задались вопросом: «Раз шаблон для поиска описывает некоторую структуру, нельзя ли и ее разделить на части?» Как разделить найденную дату на год, месяц и день, чтобы сформировать из них timestamp с помощью функции mktime()? Как разделить имя файла на путь, имя и расширение? Как разделить URL на составляющие?

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

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

Подмаски нумеруются, счет ведется слева направо. Локализированный при помощи скобок набор альтернатив тоже считается подмаской. При наличии подмасок функція preg_match() формирует следующий массив результатов: $matches[0] – совпадение с паттерном, $matches[1] – совпадение с первой подмаской, $matches[2] – совпадение со второй подмаской и т.д. При наличии флага PREG_OFFSET_CAPTURE каждое совпадение «расщепляется» на массив из двух элементов (элемент с нулевым ключом – текст совпадения, с ключом 1 — позиция).

Паттерн для расщепления даты на составляющие выглядит следующим образом: (d{2})(/|-|.)(d{2})(/|-|.)(d{4}|d{2}). В нем пять подмасок. Первая соответствует дню, третья – месяцу, пятая – году.

Ниже приведен результат работы функции preg_match():

Код:

preg_match(«/(d{2})(/|-|.)(d{2})(/|-|.)(d{4}|d{2})/», «09.07.2008», $matches);

print_r($matches)

Вывод:

Array ( [0] => 09.07.2008 [1] => 09 [2] => . [3] => 07 [4] => . [5] => 2008 )

Привожу также код готовой функции, которая преобразовывает строку с датой в timestamp:

function str_to_timestamp($str)

   {

   // проверяем строку на соответствие формату даты, с указанием подмасок

   if (preg_match(«/(d{2})(/|-|.)(d{2})(/|-|.)(d{4}|d{2})/», $str, $m))

      {

      // если строка соответствует

      switch (strlen($m[5])) // проверяем, как указан год

         {

         case 2: // если двумя цифрами

            if (IntVal($m[5]) < 30) // год меньше 30 относим к 21 веку

               $year = 2000 + IntVal($m[5]);

            else // год больше 30 – к 20-му

               $year = 1900 + IntVal($m[5]);

            break;

         case 4: // если год указан четырьмя цифрами

            $year = $m[5]; // оставляем его без изменений

            break;

         }

      // составляем timestamp из дня, месяца и года и возвращаем его

      return mktime(0,0,0,$m[3], $m[1], $year);

      }

   else // если строка не соответствует формату даты – возвращаем FALSE

     return FALSE;

   }

Эпизод 2. Сортируем подмаски

С preg_match() все относительно просто. Теперь разберемся с функцией preg_match_all(). При наличии подмасок функция возвращает через массив $matches не только все полные совпадения паттерна, а и все совпадения для каждой из подмасок. Соответственно, здесь возможно два способа выдачи результатов:

  • сначала по номерам подмасок, а потом по совпадениям;
  • по совпадениям, а потом по номерам подмасок.

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

  • PREG_PATTERN_ORDER — $matches[0] будет содержать массив совпадений всего паттерна, $matches[1] — массив совпадений первой подмаски и т. д.
  • PREG_SET_ORDER — $matches[0] будет содержать массив первых совпадений, $matches[1] — массив вторых совпадений и т. д. В каждом массиве совпадений элементом с ключом 0 будет массив совпадений с полным паттерном, с ключом 1 – массив совпадений первой подмаски и т.д.

По умолчанию установлен флаг PREG_PATTERN_ORDER. Флаги PREG_PATTERN_ORDER и PREG_SET_ORDER могут комбинироваться с флагом PREG_OFFSET_CAPTURE при помощи операции Ѕ. Соответственно, при этом каждое совпадение будет «расщепляться» на массив из двух элементов (текст совпадения, позиция).

Для наглядности приведу следующие примеры:

Код:

preg_match_all(«/(d{2})-(d{2})/», «11-12 21-22 31-32», $matches,

PREG_PATTER_ORDER);

print_r($matches)

Вывод:

Array ( [0] => Array ( [0] => 11-12 [1] => 21-22 [2] => 31-32 ) [1] => Array ( [0] => 11 [1] => 21 [2] => 31 ) [2] => Array ( [0] => 12 [1] => 22 [2] => 32 ) )

Код:

preg_match_all(«/(d{2})-(d{2})/», «11-12 21-22 31-32», $matches,

PREG_SET_ORDER);

print_r($matches)

Вывод:

Array ( [0] => Array ( [0] => 11-12 [1] => 11 [2] => 12 ) [1] => Array ( [0] => 21-22 [1] => 21 [2] => 22 ) [2] => Array ( [0] => 31-32 [1] => 31 [2] => 32 ) )

Эпизод 3. По принципу «матрешки»

Иногда возникает необходимость во вложении подмасок. Наличие вложенных подмасок существенно влияет на порядок их нумерации. Она осуществляется следующим образом:

  • сначала нумеруется сама подмаска, которая содержит вложенные
  • потом нумеруются вложенные подмаски, слева направо
  • потом нумеруются подмаски, следующие за той, которая содержала вложенные

Наглядно разобраться во вложенных подмасках можно «методом научного тыка».

К примеру, выполнив следующий код:

&lt;?php preg_match_all(«/(1)(2)((3)4)5/», «12345», $m); ?&gt;

&lt;pre&gt;

&lt;?php print_r($m); ?&gt;

&lt;/pre&gt;

мы получим

Array([0] =&gt; Array ([0] =&gt; 12345) [1] =&gt; Array ([0] =&gt; 1) [2] =&gt;

Array ([0] =&gt; 2) [3] =&gt; Array ([0] =&gt; 34) [4] =&gt; Array ([0] =&gt; 3))

А теперь попробуйте провести тот же эксперимент с паттерном (1)(2)((3)4)(5) и той же строкой, а также с паттерном (1)(2)((3(4))(5))(6) и строкой «123456».

Эпизод 4. Повторение — мать учения

Давайте, вспомнив эту древнюю пословицу, вернемся к материалам самой первой статьи. Там мы рассматривали конструкции повторения, именуемые по-научному квантификаторами. Так вот, квантификаторы можно применять не только, чтобы указать, сколько раз повторяется один предшествующий символ (или перечень символов), а и для того, чтобы указать количество повторений предшествующей последовательности символов. В данном случае нужная последовательность должна быть выделена как подмаска, а квантификатор должен стоять сразу после закрывающей скобки. Аналогичным образом можно искать конструкцию, в которой некая последовательность может либо присутствовать, либо не присутствовать. Для примера рассмотрим URL. Он состоит из

  • протокола (http://), который может указываться или нет;
  • трех «w», которые аналогично могут быть указаны или нет;
  • домена, состоящего из последовательности чередующихся буквенно-цифровых символов, разделенных точками;

Ресурс на сервере (может быть указан или нет). В свою очередь, его составляющими являются путь к ресурсу (последовательность чередующихся буквенно-цифровых символов, разделенных слэшами) и имя ресурса — html-странички, php-скрипта, рисунка.

Составляем паттерн (см. таблицу).

В шаблоне выделены следующие подмаски (в порядке, котором они будут выдаваться функцией preg_match_all()):

  • протокол (http://)?
  • три «w» с точкой (www.)?
  • домен ([dw-]+(.[dw-]+)*)
  • повторяющиеся точка-разделитель и имя поддомена (.[dw-]+)*
  • запрошенный ресурс (/([dw-]+/)+)([dw.-]+))?
  • путь к ресурсу (/([dw-]+/)+)
  • повторяющаяся часть пути (имя подкаталога и косая) ([dw-]+/)+
  • имя ресурса ([dw.-]+)

Применив к наведенному в таблице URL функцию preg_match_all() с указанием составленного паттерна, получим следующий массив совпадений:

Array

(

 [0] =&gt; Array ( [0] =&gt; http://www.rock-kingdom.com.ua/forum/kino/

 2651-gorod-boga.html )

 [1] =&gt; Array ( [0] =&gt; http:// )

 [2] =&gt; Array ( [0] =&gt; www. )

 [3] =&gt; Array ( [0] =&gt; rock-kingdom.com.ua )

 [4] =&gt; Array ( [0] =&gt; .ua )

 [5] =&gt; Array ( [0] =&gt; /forum/kino/2651-gorod-boga.html )

 [6] =&gt; Array ( [0] =&gt; /forum/kino/ )

 [7] =&gt; Array ( [0] =&gt; kino/)

 [8] =&gt; Array ( [0] =&gt; 2651-gorod-boga.html )

)

Как видим, для повторяющихся подмасок функция preg_match_all() возвращает совпадение с их последним повторением. В случае домена — это доменное имя самого верхнего уровня (.ua), для пути — имя самого нижнего каталога в иерархии (kino/).

Эпизод 5. Практическая работа

Взгляните на код скрипта (example7.php), который обрабатывает введенный в поле URL и разделяет его на составные части:

&lt;?php

if (isset($_REQUEST[«url»]))

 {

 // обрабатываем текст, введенный в поле ввода

 $result =preg_match_all(«/(http://)?(www.)?

 ([dw-]+(.[dw-]

 +)*)((/([dw-]+/)+)([dw.-]+))?/»,

 $_REQUEST[«url»], $m);

 }

?&gt;

&lt;form id=»checkform» name=»checkform»

action=»example7.php» method=»post»&gt;

 &lt;table width=»60%»&gt;

 &lt;tr&gt;

 &lt;td width=»10%»&gt;

 URL:

 &lt;/td&gt;

 &lt;td&gt;

&lt;input type=»text» id=»url» name=»url» size=»15″

style=»width: 100%;»

value=»&lt;?php echo $_REQUEST[«url»]; ?&gt;»&gt;

 &lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td colspan=»2″ align=»center»&gt;

 &lt;input type=»submit» value=»Разделить»&gt;

 &lt;/td&gt;

 &lt;/tr&gt;

 &lt;/table&gt;

&lt;/form&gt;

&lt;?php

// если был введен текст

if (isset($_REQUEST[«url»]))

 {

 ?&gt;

 &lt;br&gt;

 &lt;?php

 // если было совпадение с паттерном

 if ($result)

 {

 // выдаем результаты

 ?&gt;

 &lt;table&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Полный URL:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[0][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Протокол:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[1][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Доменное имя:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[3][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Запрошенный ресурс:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[5][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Путь к ресурсу:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[6][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;tr&gt;

 &lt;td&gt;&lt;b&gt;Имя ресурса:&lt;/b&gt;&lt;/td&gt;

 &lt;td&gt;&lt;?php echo $m[8][0] ?&gt;&lt;/td&gt;

 &lt;/tr&gt;

 &lt;/table&gt;

 &lt;?php

 }

 else

 {

 // иначе сообщаем, что был введен

 некорректный URL

 ?&gt;

 Введенный текст не является URL

 &lt;?php

 }

 }

?&gt;

И, как всегда, в конце статьи — несколько задачек :-).

Наш паттерн не учитывает возможность присутствия параметров (http://некий_сайт/forum/showthread.php?goto=newpost&t=127) и фрагментов (http://некий_сайт/forum/showthread.php?p=60343#post60343). Попробуйте добавить в паттерн дополнительные подмаски для извлечения этих элементов.

Для повторяющихся подмасок функция preg_match_all() возвращает совпадение с их последним повторением. А что делать, если надо получить все такие совпадения? К примеру, разделить домен rock-kingdom.com.ua на составляющие? Или разделить список параметров на отдельные параметры и значения? Подсказка: перечитайте статью в МК, №9 (513).

Чтобы стать специалистами в этой области, нам осталось рассмотреть конструкции-утверждения, модификаторы и средства для замены текстовых фрагментов.

Как искать в начале или в конце строки?

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

Мы рассмотрим только две простейшие из них — символы начала (^) и конца ($) строки.

Конструкция ^некий_паттерн означает, что некий текст, который мы ищем, располагается строго в начале строки, в которой осуществляется поиск.

Конструкция некий_паттерн$ означает, что некий текст, который мы ищем, располагается строго в конце строки, в которой осуществляется поиск.

Для примера рассмотрим паттерн, который извлекает из строки первое слово:

preg_match(«/^[^s.,:?!()]+/», «Это некоторый текст», $m);

print_r($m);

Вывод:

Array ( [0] => Это )

Если текст, в котором осуществляется поиск, состоит из нескольких строк, можно включить специальный многострочный режим, в котором символы ^ и $ совпадают с началом и концом каждой строки:

preg_match_all(«/^[^s.,:?!()]+/m», «Это некоторый текстrnдля

проверкиrnпоиска в многострочномrnтексте.», $m);

print_r($m);

Вывод:

Array ( [0] => Array ( [0] => Это [1] => для [2] => поиска

[3] => тексте ) )

Обратите внимание на символ «m» после закрывающего паттерн ограничителя. Это — модификатор, про них детальнее читайте во втором разделе этой статьи.

Примером использования символа конца строки может послужить извлечение имени файла из полного пути:

preg_match(«//([^/:»<>*?|]+)$/», «/usr/share/pixmaps/gedit.xpm», $m);

print_r($m);

Вывод:

Array ( [0] => /gedit.xpm [1] => gedit.xpm )

Символы начала и конца строки можно использовать вместе. Так делают, если строка имеет строго определенную структуру. Например, у нас есть список фильмов. Каждая строка имеет следующий формат: «Название (год, страна, жанр)». Надо распарсить список, разбив строку на отдельные поля, чтобы потом залить в БД. Для этого применяем следующий паттерн:

preg_match(«/^([^()]+)s+((d{4}),s+([а-яА-Я]+),s+([а-яА-Я]+))$/»,

«Нереальный блокбастер (2008, США, Комедия)», $m);

print_r($m);

Вывод:

Array ( [0] => Нереальный блокбастер (2008, США, Комедия) [1] =>

Нереальный блокбастер [2] => 2008 [3] => США [4] => Комедия )

Что такое модификаторы ?

Модификаторы по сути являются глобальными опциями поиска. Указанный модификатор распространяется на всю строку. Существуют также специальные конструкции для введения локальных опций поиска, но их рассмотрение лежит за пределами данной серии статей.

Модификатор указывается при помощи буквы после закрывающего паттерн символа-ограничителя: /^[^s.,:?!()]+/m.

В таблице приведены наиболее часто употребляемые модификаторы:

Как осуществлять замену?

В PHP существуют функции, позволяющие производить замену соответствующих паттерну участков текста. Для этого используется функция preg_replace(), которая принимает следующие параметры:

mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit ] )

  • $pattern — паттерн, описывающий участки текста, которые необходимо заменить;
  • $replacement — строка, на которую будет производиться замена;
  • $subject — текст, в котором будет производиться замена;
  • $limit — сколько совпадений заменять. Если не указан, будут заменены все совпадения.

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

$result = preg_replace(«/(http://)?(www.)?[dw-]+(.[dw-]+)+(/)?/»,

«», «Вот мой сайт: http://mysite.com/ . Но его URL вы не увидите :(«);

echo $result;

Вывод:

Вот мой сайт: . Но его URL вы не увидите 🙁

Замена с использованием регулярных выражений предоставляет полезную возможность включения в текст замены значений подмасок из совпадения. Для этого служит конструкция $n, где n — номер подмаски (нумерация идет от единицы, $0 соответствует полному совпадению).

Рассмотрим следующий пример. В тексте документа есть контактные телефоны, но они введены как попало — xxxxxxx, xxx xxxx, xxx-xx-xx. Надо их все свести к формату xxx-xx-xx. Данная задача легко решается при помощи функции preg_replace():

$result = preg_replace(«/([1-9]{1}d{2})(s+|-)?(d{2})(s+|-)?(d{2})/»,

«$1-$3-$5», «Телефоны: 1234567 346 7896 435-56-83»);

echo $result;

Вывод:

Телефоны: 123-45-67 346-78-96 435-56-83

И напоследок рассмотрим еще одну интересную особенность функции preg_replace() — возможность обработки одновременно нескольких паттернов. Если передаваемые в функцию параметры $pattern и $replacement будут одномерными массивами одинаковой длины, то участки текста, соответствующие паттернам-элементам массива $pattern, будут заменяться на соответствующие значения элементов массива $replacement:

$string = «Быстрый бурый лис перепрыгнул через ленивую собаку»;

$patterns[] = «/Быстрый/»;

$patterns[] = «/бурый/»;

$patterns[] = «/лис/»;

 

$replacements[] = «Медленный»;

$replacements[] = «белый»;

$replacements[] = «медведь»;

echo preg_replace($patterns, $replacements, $string);

Вывод:

Медленный белый медведь перепрыгнул через ленивую собаку

Когда стоит, а когда не стоит использовать регулярные выражения?

Регулярные выражения, безусловно, очень мощный инструмент. Но за его мощь приходится платить ресурсами сервера — реализация этих функций представляет по сути программу-интерпретатор языка паттернов. Поэтому использовать их рекомендуется только тогда, когда Вы на 100% уверены, что никаким другим способом задачу решать нельзя.

Например, если бы список фильмов лежал в текстовом файле данных в формате названиеЅгодЅстранаЅжанр, то для разделения каждой записи на поля можно было бы применить функцию explode(). И работать она будет быстрее, чем preg_match_all().

Также не забывайте о том, что не следует изобретать велосипед. Если Вы решили писать парсер для какой-либо структуры или формата данных, поищите в Сети — а не существуют ли уже готовые наработки? Для XML и HTML существует класс DOMDocument, для BBCode (язык разметки постов на форумах) — библиотека xBB.

Все ли здесь рассказано про регулярные выражения?

Конечно, нет! Мы не рассмотрели и половины возможностей, предоставляемых регулярными выражениями, и всего лишь 3 из 8 функций для работы с ними в PHP. Рассмотреть их все в пределах небольшой серии статей просто невозможно. И незачем — мы рассмотрели самые ходовые, наиболее часто используемые возможности, при помощи которых решаются 90% проблем, требующих применения регулярных выражений.

И под занавес

Что ж, уважаемые читатели, пришла наша пора прощаться, но только в этой серии статей. По адресу http://rock-kingdom.com.ua/rege.zip лежит архив со всеми программами-примерами.

Где применяются регулярные выражения?

Много где! Их применение не ограничено программированием в области обработки текстовой информации.

На *NIX-серверах основные службы конфигурируются при помощи текстовых файлов. И при описании различных ограничений по доступу используется язык Perl-совместимых регулярных выражений. Загляните в конфиги Web-сервера (httpd.conf), proxy-сервера (squid.conf), и вы увидите там регулярные выражения. С помощью их и расширения Apache mod_rewrite возможна реализация Human Friendly URLs (например, www.site.com/prices вместо www.site.com/?show=prices). Ссылки подобного вида не только красиво смотрятся и лучше запоминаются, благодаря им сайт лучше индексируется поисковиками.

Robo User
Robo User
Web-droid editor