PHP

Регулярные выражения для продвинутых пользователей

Регулярные выражения это словно “Швейцарский армейский нож” который разрезает блоки информации по определённым шаблонам. Они сопровождаются огромным арсеналом инструментов, о существовании некоторых не знают даже те, кто не раз писал регулярки. Сегодня мы рассмотрим некоторые продвинутые техники, которые вы сможете применить в ваших проектах.
Добавляем комментарии

Иногда (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
 	)
 	*/

Надеюсь, что данная статья была вам полезна и открыла что-то новое.
Добавлено: 19 Августа 2013 03:03:01 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...