Статьи

Советы и приемы передового регулярного выражения

Дважды в месяц мы возвращаемся к любимым постам наших читателей на протяжении всей истории Nettuts +.

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


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

Например, вот что мы могли бы использовать для проверки телефонных номеров в США.

1
preg_match(«/^(1[-\s.])?(\()?\d{3}(?(2)\))[-\s.]?\d{3}[-\s.]?\d{4}$/»,$number)

Это может стать намного более читабельным с комментариями и некоторым дополнительным интервалом.

01
02
03
04
05
06
07
08
09
10
11
12
preg_match(«/^
 
            (1[-\s.])?
            ( \( )? # optional opening parenthesis
            \d{3} # the area code
            (?(2) \) ) # if there was opening parenthesis, close it
            [-\s.]?
            \d{3} # first 3 digits
            [-\s.]?
            \d{4} # last 4 digits
 
            $/x»,$number);

Давайте поместим это в сегмент кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$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.])?
            ( \( )? # optional opening parenthesis
            \d{3} # the area code
            (?(2) \) ) # if there was opening parenthesis, close it
            [-\s.]?
            \d{3} # first 3 digits
            [-\s.]?
            \d{4} # last 4 digits
 
            $/x»,$number)) {
 
        echo «valid\n»;
    } else {
        echo «invalid\n»;
    }
}
 
/* prints
 
123 555 6789 is valid
1-(123)-555-6789 is valid
(123-555-6789 is invalid
(123).555.6789 is valid
123 55 6789 is invalid
 
*/

Хитрость заключается в том, чтобы использовать модификатор ‘x’ в конце регулярного выражения . Это приводит к игнорированию пробелов в шаблоне, если они не экранированы (\ s). Это позволяет легко добавлять комментарии. Комментарии начинаются с «#» и заканчиваются новой строкой.


В PHP preg_replace_callback () может использоваться для добавления функции обратного вызова в замены регулярных выражений.

Иногда вам нужно сделать несколько замен. Если вы вызываете preg_replace () или str_replace () для каждого шаблона, строка будет анализироваться снова и снова.

Давайте посмотрим на этот пример, где у нас есть шаблон электронной почты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$template = «Hello [first_name] [last_name],
 
Thank you for purchasing [product_name] from [store_name].
 
The total cost of your purchase was [product_price] plus [ship_price] for shipping.
 
You can expect your product to arrive in [ship_days_min] to [ship_days_max] business days.
 
Sincerely,
[store_manager_name]»;
 
// assume $data array has all the replacement data
// such as $data[‘first_name’] $data[‘product_price’] etc…
 
$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);
 
// this could be done in a loop too,
// but I wanted to emphasize how many replacements were made

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

Итак, вот лучший способ сделать это с помощью обратных вызовов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// …
 
// this will call my_callback() every time it sees brackets
$template = preg_replace_callback(‘/\[(.*)\]/’,’my_callback’,$template);
 
function my_callback($matches) {
    // $matches[1] now contains the string between the brackets
 
    if (isset($data[$matches[1]])) {
        // return the replacement string
        return $data[$matches[1]];
    } else {
        return $matches[0];
    }
}

Теперь строка в $ template анализируется регулярным выражением только один раз.


Прежде чем я начну объяснять эту концепцию, я хотел бы сначала показать пример. Допустим, мы ищем теги привязки в HTML-тексте:

1
2
3
4
5
6
7
$html = ‘Hello <a href=»/world»>World!</a>’;
 
if (preg_match_all(‘/<a.*>.*<\/a>/’,$html,$matches)) {
 
    print_r($matches);
 
}

Результат будет таким, как ожидалось:

01
02
03
04
05
06
07
08
09
10
/* output:
Array
(
    [0] => Array
        (
            [0] => <a href=»/world»>World!</a>
        )
 
)
*/

Давайте изменим вход и добавим второй тег привязки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
$html = ‘<a href=»/hello»>Hello</a>
<a href=»/world»>World!</a>’;
 
if (preg_match_all(‘/<a.*>.*<\/a>/’,$html,$matches)) {
 
    print_r($matches);
 
}
 
 
/* output:
Array
(
    [0] => Array
        (
            [0] => <a href=»/hello»>Hello</a>
            [1] => <a href=»/world»>World!</a>
 
        )
 
)
*/

Опять же, похоже, все в порядке. Но не позволяй этому обмануть тебя. Единственная причина, по которой он работает, заключается в том, что теги привязки находятся на отдельных строках, и по умолчанию PCRE сопоставляет шаблоны только по одной строке за раз (дополнительная информация о модификаторе ‘m’ ). Если мы встретим два тега привязки в одной строке, они больше не будут работать, как ожидалось:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
$html = ‘<a href=»/hello»>Hello</a> <a href=»/world»>World!</a>’;
 
if (preg_match_all(‘/<a.*>.*<\/a>/’,$html,$matches)) {
 
    print_r($matches);
 
}
 
/* output:
Array
(
    [0] => Array
        (
            [0] => <a href=»/hello»>Hello</a> <a href=»/world»>World!</a>
 
        )
 
)
*/

На этот раз шаблон соответствует первому открывающему тегу и последнему открывающему тегу, а также всему, что находится между ними, как одно совпадение, а не как два отдельных совпадения. Это связано с тем, что поведение по умолчанию является «жадным».

«При жадности квантификаторы (такие как * или +) соответствуют максимально возможному количеству символов».

Если вы добавите вопросительный знак после квантификатора (. *?), Он станет «несмешным»:

1
$html = ‘<a href=»/hello»>Hello</a> <a href=»/world»>World!</a>’;

Теперь результат верный. Еще один способ вызвать неуклюжее поведение — использовать модификатор U-шаблона .


Утверждающее утверждение ищет совпадение с шаблоном, которое следует за текущим совпадением. Это можно объяснить проще на примере.

Следующий шаблон сначала соответствует «foo», а затем проверяет, следует ли за ним «bar»:

1
2
3
4
$pattern = ‘/foo(?=bar)/’;
 
preg_match($pattern,’Hello foo’);
preg_match($pattern,’Hello foobar’);

Это может показаться не очень полезным, поскольку мы могли бы просто проверить вместо ‘foobar’. Тем не менее, также возможно использовать прогнозирование для создания отрицательных утверждений. Следующий пример соответствует ‘foo’, только если за ним НЕ следует ‘bar’.

1
2
3
4
5
$pattern = ‘/foo(?!bar)/’;
 
preg_match($pattern,’Hello foo’);
preg_match($pattern,’Hello foobar’);
preg_match($pattern,’Hello foobaz’);

Взгляды с задним числом работают аналогично, но они ищут шаблоны перед текущим соответствием. Вы можете использовать (? <Для положительных утверждений и (? <! Для отрицательных утверждений.

Следующий шаблон соответствует, если есть ‘bar’ и он не следует ‘foo’.

1
2
3
4
5
$pattern = ‘/(?<!foo)bar/’;
 
preg_match($pattern,’Hello bar’);
preg_match($pattern,’Hello foobar’);
preg_match($pattern,’Hello bazbar’);

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

1
2
3
4
5
(?(condition)true-pattern|false-pattern)
 
or
 
(?(condition)true-pattern)

Условие может быть числом. В этом случае это относится к ранее захваченному подшаблону.

Например, мы можем использовать это, чтобы проверить открывающие и закрывающие угловые скобки:

1
2
3
4
5
6
$pattern = ‘/^(<)?[az]+(?(1)>)$/’;
 
preg_match($pattern, ‘<test>’);
preg_match($pattern, ‘<foo’);
preg_match($pattern, ‘bar>’);
preg_match($pattern, ‘hello’);

В приведенном выше примере «1» относится к подшаблону (<), который также является необязательным, поскольку за ним следует знак вопроса. Только если это условие истинно, оно соответствует закрывающей скобке.

Условием также может быть утверждение:

1
2
3
4
5
6
7
8
// if it begins with ‘q’, it must begin with ‘qu’
// else it must begin with ‘f’
$pattern = ‘/^(?(?=q)qu|f)/’;
 
preg_match($pattern, ‘quake’);
preg_match($pattern, ‘qwerty’);
preg_match($pattern, ‘foo’);
preg_match($pattern, ‘bar’);

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

В следующем примере мы используем строку, которая содержит специальный символ (*).

1
2
3
4
5
6
$word = ‘*world*’;
 
$text = ‘Hello *world*!’;
 
preg_match(‘/’.$word.’/’, $text);
preg_match(‘/’.preg_quote($word).’/’, $text);

То же самое можно сделать, заключив строку между \ Q и \ E. Любой специальный символ после \ Q игнорируется до \ E.

1
2
3
4
5
$word = ‘*world*’;
 
$text = ‘Hello *world*!’;
 
preg_match(‘/\Q’.$word.’\E/’, $text);

Однако этот второй метод не безопасен на 100%, так как сама строка может содержать \ E.


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

Давайте начнем с очень простого примера:

1
preg_match(‘/(f.*)(b.*)/’, ‘Hello foobar’, $matches);

Теперь давайте внесем небольшое изменение, добавив еще один подшаблон (H. *) впереди:

1
2
3
4
preg_match(‘/(H.*) (f.*)(b.*)/’, ‘Hello foobar’, $matches);
 
echo «f* => » .
echo «b* => » .

Массив $ match был изменен, что может привести к тому, что скрипт перестанет работать правильно, в зависимости от того, что мы делаем с этими переменными в коде. Теперь мы должны найти каждый вхождение массива $ match в коде и соответствующим образом скорректировать номер индекса.

Если мы на самом деле не заинтересованы в содержимом нового подшаблона, который мы только что добавили, мы можем сделать его «не захватывающим» следующим образом:

1
2
3
4
preg_match(‘/(?:H.*) (f.*)(b.*)/’, ‘Hello foobar’, $matches);
 
echo «f* => » .
echo «b* => » .

Добавляя «?:» В начале подшаблона, мы больше не записываем его в массив $ match, поэтому другие значения массива не сдвигаются.


Существует другой метод предотвращения ловушек, как в предыдущем примере. Мы можем фактически дать имена каждому подшаблону, чтобы мы могли ссылаться на них позже, используя эти имена вместо номеров индексов массива. Это формат: (? P шаблон)

Мы могли бы переписать первый пример в предыдущем разделе, например так:

1
2
3
4
preg_match(‘/(?P<fstar>f.*)(?P<bstar>b.*)/’, ‘Hello foobar’, $matches);
 
echo «f* => » .
echo «b* => » .

Теперь мы можем добавить еще один подшаблон, не нарушая существующие совпадения в массиве $ match:

1
2
3
4
5
6
preg_match(‘/(?P<hi>H.*) (?P<fstar>f.*)(?P<bstar>b.*)/’, ‘Hello foobar’, $matches);
 
echo «f* => » .
echo «b* => » .
 
echo «h* => » .

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

Плакат на Stackoverflow содержит блестящее объяснение того, почему мы не должны использовать регулярные выражения для анализа [X] HTML.

… дорогой лорд, помогите нам, как кто-нибудь может выжить в этом бедствии, используя регулярное выражение для анализа HTML, обреченное человечество на вечность ужасных пыток и дыр в безопасности, использующих регулярное выражение в качестве инструмента для обработки HTML, устанавливает разрыв между этим миром и ужасным царством коррумпированных лица …

Не говоря уже о шутках, неплохо было бы потратить некоторое время и выяснить, какие существуют парсеры XML или HTML и как они работают. Например, PHP предлагает несколько расширений, связанных с XML (и HTML).

Пример: получение второй ссылки на странице HTML

01
02
03
04
05
06
07
08
09
10
11
12
13
14
$doc = DOMDocument::loadHTML(‘
    <html>
    <body>Test
        <a href=»http://www.nettuts.com»>First link</a>
        <a href=»http://net.tutsplus.com»>Second link</a>
    </body>
    </html>
‘);
 
echo $doc->getElementsByTagName(‘a’)
        ->item(1)
        ->getAttribute(‘href’);
 
// prints: http://net.tutsplus.com

Опять же, вы можете использовать существующие функции для проверки пользовательских данных, таких как отправка форм.

1
2
3
4
if (!filter_var($_POST[’email’], FILTER_VALIDATE_EMAIL)) {
 
    $errors []= «Please enter a valid e-mail.»;
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// get supported filters
print_r(filter_list());
 
/* output
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
)
*/

Дополнительная информация: PHP Data Filtering

Вот некоторые другие утилиты, которые следует иметь в виду перед использованием регулярных выражений:


Спасибо за прочтение!