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
)
*/
Теги:
Регулярные выражения
Добавлено: 02 Мая 2018 18:58:43 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...