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

Добавление комментариев

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

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

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

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

preg_match("/^

   (1[-\s.])? # optional '1-', '1.' or '1'
   ( \( )?    # optional opening parenthesis
   \d{3}      # the area code
   (?(2) \) ) # if there was opening parenthesis, close it
   [-\s.]?    # followed by '-' or '.' or space
   \d{3}      # first 3 digits
   [-\s.]?    # followed by '-' or '.' or space
   \d{4}      # last 4 digits

   $/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.])? # optional '1-', '1.' or '1'
   ( \( )?    # optional opening parenthesis
   \d{3}      # the area code
   (?(2) \) ) # if there was opening parenthesis, close it
   [-\s.]?    # followed by '-' or '.' or space
   \d{3}      # first 3 digits
   [-\s.]?    # followed by '-' or '.' or space
   \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() для каждого шаблона, строка будет разбираться снова и снова.

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

$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]";

// предположим, что массив $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 the replacement string
   return $data[$matches[1]];
 } else {
   return $matches[0];
 }
}

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

Жадный против нежадного

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

$html = 'Hello <a href="http://azovsky.net/world">World!</a>';

if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {

print_r($matches);

}

Результат будет такой:

/* output:
 Array
 (
     [0] => Array
         (
             [0] => <a href="http://azovsky.net/world">World!</a>
         )

)
 */

Давайте изменим текст и добавим второй тег ссылки:

$html = '<a href="http://azovsky.net/hello">Hello</a>
 <a href="http://azovsky.net/world">World!</a>';

if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {

print_r($matches);

}

/* output:
 Array
 (
     [0] => Array
         (
             [0] => <a href="http://azovsky.net/hello">Hello</a>
             [1] => <a href="http://azovsky.net/world">World!</a>

        )

)
 */

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

$html = '<a href="http://azovsky.net/hello">Hello</a> <a href="http://azovsky.net/world">World!</a>';

if (preg_match_all('/<a.*>.*<\/a>/',$html,$matches)) {

  print_r($matches);

}

/* output:
 Array
 (
     [0] => Array
         (
             [0] => <a href="http://azovsky.net/hello">Hello</a> <a href="http://azovsky.net/world">World!</a>

        )

)
*/

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

Когда используется жадные квантификаторы (например, * или +), то выбирается как можно больше символов в строке.

Если вы добавите знак вопроса после квантификатора (.*?) он становится "нежадный" (ungreedy):

$html = '<a href="http://azovsky.net/hello">Hello</a> <a href="http://azovsky.net/world">World!</a>';

// добавим ? После всех *
if (preg_match_all('/<a.*?>.*?<\/a>/',$html,$matches)) {

  print_r($matches);

}

/* output:
 Array
 (
     [0] => Array
         (
             [0] => <a href="http://azovsky.net/hello">Hello</a>
             [1] => <a href="http://azovsky.net/world">World!</a>

        )

)
*/

Теперь правильный результат. Другой способ вызвать ленивое поведение является использование модификатора 'U'.

Просмотр вперёд и назад

Выражение для просмотра вперёд (lookahead) ищет по шаблону, который следует за текущим шаблоном. Это проще объяснить на примере.

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

$pattern = '/foo(?=bar)/';

preg_match($pattern,'Hello foo'); // false
preg_match($pattern,'Hello foobar'); // true

Данный пример может показаться не очень полезным, так как мы могли бы просто проверять на "foobar". Однако также можно использовать выражение просмотра вперед с отрицанием (негативный просмотр вперёд). В следующем примере "foo" совпадет только если он не следует перед "bar".

$pattern = '/foo(?!bar)/';

preg_match($pattern,'Hello foo'); // true
preg_match($pattern,'Hello foobar'); // false
preg_match($pattern,'Hello foobaz'); // true

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

Следующий шаблон находит "bar" если он не следует за "foo".

$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, '<test>'); // true
preg_match($pattern, '<foo'); false="" preg_match($pattern,="" 'bar="">'); // false
preg_match($pattern, 'hello'); // true

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

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

// если начинается с 'q', то должно начинаться с 'qu'
// иначе должно начинаться с '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 = '*world*';

$text = 'Hello *world*!';

preg_match('/'.$word.'/', $text); // causes a warning
preg_match('/'.preg_quote($word).'/', $text); // true

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

$word = '*world*';

$text = 'Hello *world*!';

preg_match('/\Q'.$word.'\E/', $text); // true

Однако, этот второй метод не является 100% безопасным, так как сама строка может содержать \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'

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

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

preg_match('/(?:H.*) (f.*)(b.*)/', 'Hello foobar', $matches);

echo "f* => " . $matches[1]; // prints 'f* => foo'
echo "b* => " . $matches[2]; // prints 'b* => bar'

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

Имена для групп

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

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

preg_match('/(?P<fstar>f.*)(?P<bstar>b.*)/', 'Hello foobar', $matches);

echo "f* => " . $matches['fstar']; // prints 'f* => foo'
echo "b* => " . $matches['bstar']; // prints 'b* => bar'

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

preg_match('/(?P<hi>H.*) (?P<fstar>f.*)(?P<bstar>b.*)/', 'Hello foobar', $matches);

echo "f* => " . $matches['fstar']; // prints 'f* => foo'
echo "b* => " . $matches['bstar']; // prints 'b* => bar'

echo "h* => " . $matches['hi']; // prints 'h* => Hello'

Не изобретайте велосипед

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

Парсинг [X]HTML

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

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

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

$doc = DOMDocument::loadHTML('
 <html>
 <body>Test
   <a href="http://www.azovsky.net">First link</a>
   <a href="http://azovsky.net">Second link</a>
 </body>
 </html>
');

echo $doc->getElementsByTagName('a')
   ->item(1)
   ->getAttribute('href');

// выводит: http://azovsky.net

Проверка формы ввода

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

if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
  $errors []= "Please enter a valid e-mail.";
}

// список всех поддерживаемых фильтров
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.

Другое

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

strtotime() для разбора дат.
Использование регулярных функций строк для замены функций регулярных выражений, если ваш шаблон не содержит регулярные выражения:
Например: str_replace() vs. preg_replace(), explode() vs. preg_split(), strpos() vs. preg_match().

Источник.