Регулярные выражения это словно “Швейцарский армейский нож” который разрезает блоки информации по определённым шаблонам. Они сопровождаются огромным арсеналом инструментов, о существовании некоторых не знают даже те, кто не раз писал регулярки. Сегодня мы рассмотрим некоторые продвинутые техники, которые вы сможете применить в ваших проектах.
Добавляем комментарии
Иногда (100% преувеличение :) ) регулярные выражения сложны или вовсе непонятны. Написав какое-то выражение сегодня, завтра вам возможно понадобится несколько попыток, чтобы вспомнить как функционирует тот или иной шаблон. Как и во всех случаях жизни (что касается программирования) не забывайте писать комментарии к своим регулярным выражениям.
К примеру вот пример выражения для проверки телефонного номера в США:
preg_match("/^(1[-\s.])?(\()?\d{3}(?(2)\))[-\s.]?\d{3}[-\s.]?\d{4}$/",$number)
Благодаря комментариям этот код становится намного понятнее:
preg_match("/^
(1[-\s.])? # выборочно '1-', '1.' or '1'
( \( )? # выборочно: открывающаяся скобка
\d{3} # код
(?(2) \) ) # если была открыта круглая скобка, закрыть
[-\s.]? # далее '-' или '.' или пробел
\d{3} # первые 3 цифры
[-\s.]? # далее '-' или '.' или пробел
\d{4} # последние 4 цифры
$/x",$number);
Давайте пустим этот код в привычную для него среду:
$numbers = array(
"123 555 6789",
"1-(123)-555-6789",
"(123-555-6789",
"(123).555.6789",
"123 55 6789");
foreach ($numbers as $number) {
echo "$number is ";
if (preg_match("/^
(1[-\s.])? # выборочно '1-', '1.' or '1'
( \( )? # выборочно: открывающаяся скобка
\d{3} # код
(?(2) \) ) # если была открыта круглая скобка, закрыть
[-\s.]? # далее '-' или '.' или пробел
\d{3} # первые 3 цифры
[-\s.]? # далее '-' или '.' или пробел
\d{4} # последние 4 цифры
$/x",$number)) {
echo "верно\n";
} else {
echo "неверно\n";
}
}
/* примеры
123 555 6789 верно
1-(123)-555-6789 верно
(123-555-6789 неверно
(123).555.6789 верно
123 55 6789 неверно
*/
Хитрость заключается в символе “\x” в конце выражения. Это позволяет игнорировать пробелы (исключение: экранированные пробелы при помощи \s) в самом выражении для того, чтобы мы могли использовать комментарии (начинающиеся с # и заканчивающиеся перед переходом на новую строку);
Использование функций обратного вызова
В PHP существует функция preg_replace_callback(), которая используется для добавления функций обратного вызова к регулярным выражениям.
Иногда возникает задача, когда нужно произвести нескольких замен в тексте. Если использовать функции preg_replace() или str_replace() для каждого шаблона, строка будет обрабатываться несколько раз.
Давайте рассмотрим пример с шаблоном email:
$template = "Здравствуйте, [first_name] [last_name],
Спасибо за покупку [product_name] в магазине [store_name].
Конечная цена [product_price] плюс [ship_price] за доставку.
Вы можете ожидать посылку в течении [ship_days_min], [ship_days_max] рабочих дней.
С уважением,
[store_manager_name]";
// предположим что в массиве $data данные, которые нужно вставить вместо маркеров
// такие как $data['first_name'] $data['product_price'] и т.д.
$template = str_replace("[first_name]",$data['first_name'],$template);
$template = str_replace("[last_name]",$data['last_name'],$template);
$template = str_replace("[store_name]",$data['store_name'],$template);
$template = str_replace("[product_name]",$data['product_name'],$template);
$template = str_replace("[product_price]",$data['product_price'],$template);
$template = str_replace("[ship_price]",$data['ship_price'],$template);
$template = str_replace("[ship_days_min]",$data['ship_days_min'],$template);
$template = str_replace("[ship_days_max]",$data['ship_days_max'],$template);
$template = str_replace("[store_manager_name]",$data['store_manager_name'],$template);
// это можно сделать и в цикле,
// но лучше избежать столького количества итераций
Заметим, что любой из “маркеров”, представленных в вырежении выше, заключён в квадратные скобки. Мы можем выследить их с помощью регулярки и заменить при помощи функции обратного вызова.
Вот так мы и поступим:
// ...
// my_callback() будет вызываться каждый раз, когда будут найдены скобки
$template = preg_replace_callback('/\[(.*)\]/','my_callback',$template);
function my_callback($matches) {
// $matches[1] теперь содержит строку, которая находилась между скобок
if (isset($data[$matches[1]])) {
// вернуть заменённую строку
return $data[$matches[1]];
} else {
return $matches[0];
}
}
Теперь строка используется только единожды.
“Сетка” против “Не сетки”
Перед тем, как объясниться, я продемонстрирую пример. Предположим, что нам необходимо найти угловые скобки html элементов:
$html = 'Привет <a href="http://ruseller.com">Мир!</a>';
if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {
print_r($matches);
}
Результат будет следующим:
/* на выходе:
Array
(
[0] => Array
(
[0] => <a href="http://ruseller.com">Мир!</a>
)
)
*/
Давайте добавим второй тег:
$html = '<a href="http://ruseller.com">Привет</a>
<a href="http://ruseller.com">Мир!</a>';
if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {
print_r($matches);
}
/* на выходе:
Array
(
[0] => Array
(
[0] => <a href="http://ruseller.com">Hello</a>
[1] => <a href="http://ruseller.com">World!</a>
)
)
*/
Всё это работает благодаря тому, что ссылки находятся на разных строках. Если поместить их на одной, данный скрипт не сработает т.к. регулярные выражения обрабатывают по строкам.
$html = '<a href="http://ruseller.com">Hello</a> <a href="http://ruseller.com">World!</a>';
if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {
print_r($matches);
}
/* на выходе:
Array
(
[0] => Array
(
[0] => <a href="http://ruseller.com">Привет</a> <a href="http://ruseller.com">Мир!</a>
)
)
*/
На этот раз регулярное выражение нашло только первый открывающийся и закрывающийся тег. В народе техника приведённая выше называется “Сетка”
Если в регулярное выражение мы добавим (.*?) то эта техника снова заработает и в случае строки без переноса:
$html = '<a href="http://ruseller.com">Привет</a> <a href="http://ruseller.com">Мир!</a>';
// заметьте ?
if (preg_match_all('/<a.*?>.*?<\/a>/',$html,$matches)) {
print_r($matches);
}
/* на выходе:
Array
(
[0] => Array
(
[0] => <a href="http://ruseller.com">Hello</a>
[1] => <a href="http://ruseller.com">World!</a>
)
)
*/
Теперь всё работает нормально.
Предыдущие и последующие выражения
И снова начнём с примера. Данное регулярное выражение ищет строку foo за которой следует строка bar:
$pattern = '/foo(?=bar)/';
preg_match($pattern,'Hello foo'); // false
preg_match($pattern,'Hello foobar'); // true
А теперь наоборот:
$pattern = '/foo(?!bar)/';
preg_match($pattern,'Hello foo'); // true
preg_match($pattern,'Hello foobar'); // false
preg_match($pattern,'Hello foobaz'); // true
Поиск последующего выражения происходит до того, как произошло нахождение предыдущего выражения. Используйте ?< для позитивных выражений, _
Ещё пример:
$pattern = '/(?<!foo)bar/';
preg_match($pattern,'Hello bar'); // true
preg_match($pattern,'Hello foobar'); // false
preg_match($pattern,'Hello bazbar'); // true
Шаблоны для условий (If-Then-Else)
Формат таких выражений выглядит следующим образом:(?(condition)true-pattern|false-pattern) или (?(condition)true-pattern)
В качестве условия может быть вставлено число. Приведу более распространённый пример для нахождения всё тех же открывающихся и закрывающихся скобок:
$pattern = '/^(<)?[a-z]+(?(1)>)$/';
preg_match($pattern, ''); // true
preg_match($pattern, ''); // false
preg_match($pattern, 'hello'); // true
Это выражение работает следующим образом: закрывающийся тег ищется только при том условии, если был найден открывающийся тег. Можно писать и такие выражения:
// начинаем с поиска 'q',
// если таковых символов нет ищем 'f'
$pattern = '/^(?(?=q)qu|f)/';
preg_match($pattern, 'quake'); // true
preg_match($pattern, 'qwerty'); // false
preg_match($pattern, 'foo'); // true
preg_match($pattern, 'bar'); // false
Фильтрование
Существуют различные причины для фильтрации входных данных при разработке веб-приложений. Мы фильтруем данные перед вставкой в базу или при выводе их в браузер. Аналогичным образом, перед примирением регулярного выражения строку нужно отфильтровать. В PHP существует функция preg_quote специально для этих целей.
В следующем примере мы используем строку, которая содержит символ (*):
$word = '*мир*';
$text = 'Привет *мир*!';
preg_match('/'.$word.'/', $text); // вызовет warning
preg_match('/'.preg_quote($word).'/', $text); // true
Того же самого эффекта может добиться, заключив строку между \Q и \E.
$word = '*мир*';
$text = 'Привет *мир*!';
preg_match('/\Q'.$word.'\E/', $text); // true
К слову сказать, второй пример не всегда не очень хорош с точки зрения безопасности. В самой строке может находится символ \E.
Подшаблоны
Подшаблоны, записанные в круглые скобки заключают свой результат в массив для дальнейшего использования.
Начнём с небольшого примера:
preg_match('/(f.*)(b.*)/', 'Hello foobar', $matches);
echo "f* => " . $matches[1]; // prints 'f* => foo'
echo "b* => " . $matches[2]; // prints 'b* => bar'
Теперь произведём небольшое изменение, добавив (H.*) в начало (часто распространённая ловушка). Нарушилась последовательность индексов:
preg_match('/(H.*) (f.*)(b.*)/', 'Hello foobar', $matches);
echo "f* => " . $matches[1]; // prints 'f* => Hello'
echo "b* => " . $matches[2]; // prints 'b* => foo'
Теперь порядок элементов в массиве немного изменился. Исправим ситуацию для того, чтобы скрипт работал корректно:
preg_match('/(?:H.*) (f.*)(b.*)/', 'Hello foobar', $matches);
echo "f* => " . $matches[1]; // prints 'f* => foo'
echo "b* => " . $matches[2]; // prints 'b* => bar'
Добавив ‘?:’, мы сообщаем, что не добавляем этот элемент в массив. Так что последовательность индексов не нарушится.
Именованные подшаблоны
Существует еще один метод для предотвращения ловушек, как в предыдущем примере. Мы можем каждому подшаблону дать своё имя, а потом ссылаться на них. Формат: (? Ppattern)
Перепишем прошлые пример:
preg_match('/(?Pf.*)(?Pb.*)/', 'Hello foobar', $matches);
echo "f* => " . $matches['fstar']; // prints 'f* => foo'
echo "b* => " . $matches['bstar']; // prints 'b* => bar'
И второй:
preg_match('/(?PH.*) (?Pf.*)(?Pb.*)/', 'Hello foobar', $matches);
echo "f* => " . $matches['fstar']; // prints 'f* => foo'
echo "b* => " . $matches['bstar']; // prints 'b* => bar'
echo "h* => " . $matches['hi']; // prints 'h* => Hello'
Не изобретайте колесо
Есть много мнений о том, что не следует использовать регулярные выражения для обработки
HTML. Однако иногда возникает такая задача:
К слову сказать полезно знать как происходит парсинг XML и HTML. Пример: вытащить вторую ссылку из страницы:
[PHP] $doc = DOMDocument::loadHTML('
<html>
<body>Тест
<a href="http://ruseller.com">Первая ссылка</a>
<a href="http://www.ruseller.com">Вторая ссылка</a>
</body>
</html>
');
echo $doc->getElementsByTagName('a')
->item(1)
->getAttribute('href');
// на выходе: http://www.ruseller.com
Валидация полей формы
В связи с большими временными запросами вместо регулярных выражений можно использовать специальные фильтры:
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$errors []= "Введите валидный e-mail.";
}
// Получить все возможные фильтры
print_r(filter_list());
/* на выходе
Array
(
[0] => int
[1] => boolean
[2] => float
[3] => validate_regexp
[4] => validate_url
[5] => validate_email
[6] => validate_ip
[7] => string
[8] => stripped
[9] => encoded
[10] => special_chars
[11] => unsafe_raw
[12] => email
[13] => url
[14] => number_int
[15] => number_float
[16] => magic_quotes
[17] => callback
)
*/
Надеюсь, что данная статья была вам полезна и открыла что-то новое.