Регулярные выражения в PHP

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

PHP

PHP использует PCRE (PCRE2 с версии PHP 7.3) для регулярных выражений, и он поставляется с несколькими расширенными возможностями, которые могут помочь в написании читабельных, самоочевидных и простых в обслуживании регулярных выражений. Функции PHP filters и ctype обеспечивают проверку URL, email и буквенно-цифровых значений, что помогает не использовать регулярные выражения в первую очередь.

IDE могут предоставить более красивую подсветку синтаксиса, чтобы сделать данное регулярное выражение более читабельным и легким для восприятия, и даже предложить быстрые исправления для его улучшения. Однако написание понятных и более читабельных регулярных выражений в первую очередь может помочь в долгосрочной перспективе.

Вот несколько советов по улучшению и написанию лучших регулярных выражений в PHP. Обратите внимание, что они могут не работать в старых версиях PHP (старше PHP 7.3). Кроме того, использование этих улучшений также означает, что регулярные выражения могут быть менее переносимы на другие языки. Например, именованные захваты поддерживаются даже в старых версиях PHP, но в JavaScript функция именованных захватов была добавлена только в ECMAScript 2018.

Выбор разделителя

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

Рассмотрим регулярное выражение ниже:

В любом регулярном выражении символ-разделитель содержит выражение, за которым следуют необязательные флаги. В приведенном примере (foo|bar) - это само выражение, а i - флаг/модификатор. Символ / является разделителем.

В качестве разделителя часто используется прямая косая черта (/), но это может быть любой символ, например ~, !, @, #, $ и т.д. Буквенно-цифровые символы (A-Z, a-z и 0-9), многобайтовые символы (например, Emojis) и обратные слеши (\) не могут быть разделителем.

В качестве разделителей можно использовать фигурные скобки. Регулярные выражения с {}, (), [] и <> также принимаются, и могут быть более читабельными в зависимости от контекста.

Выбор разделителя важен, поскольку все вхождения символа-разделителя в выражение должны быть экранированы. Чем меньше экранированных символов в регулярном выражении, тем более читабельным оно будет. Отказ от выбора метасимволов (таких как ^, $, скобки и другие символы, которые имеют специальное значение в регулярных выражениях) может уменьшить количество экранируемых символов.

Хотя прямая косая черта часто используется в качестве разделителя регулярных выражений, она часто не подходит для регулярных выражений, содержащих URI.

Прямые косые черты (/) являются неудачным выбором разделителя в приведенном примере, потому что само выражение также содержит прямые косые черты, которые теперь должны быть экранированы, что приводит к довольно нечитабельному фрагменту.

Простая замена разделителя с / на # сделала выражение более читабельным, поскольку оно больше не содержит символов экранирования:

Сокращение количества экранирующих символов

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

В регулярных выражениях некоторые метасимволы не считаются метасимволами, если они используются внутри квадратных скобок (класс символов). Например, символы ., *, + и $ (среди прочих) обладают особой функциональностью в регулярных выражениях, но не внутри квадратных скобок.

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

Кроме того, некоторые символы не нуждаются в экранировании, если они не являются частью диапазона.

Например, символ тире (-) обозначает диапазон символов, если он используется между двумя символами, но он не несет никакой специальной функциональности, если используется в другом месте. В регулярном выражении /[A-Z]/ символ тире - используется для создания диапазона соответствия от A до Z. Если символ тире экранирован (/[A\-Z]/), регулярное выражение соответствует только символам A, Z и -. Вместо того, чтобы экранировать символ тире (\-), просто переместите символ тире в конец квадратных скобок, чтобы уменьшить количество символов, которые нужно экранировать; регулярное выражение /[A\-Z]/ эквивалентно [AZ-], но последнее более читабельно.

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

Существует флаг X, который выдает ошибку регулярного выражения, если символ без специального значения экранирован, но он не является контекстно-зависимым (например, выдает ошибку в зависимости от скобок и т.д.).

Группы без захвата

В регулярных выражениях скобки () начинают группу захвата. Совпавшие результаты будут переданы в список совпадений:

Рассмотрим пример регулярного выражения, которое извлекает цену из заданного текста, из текста Цена: €24.

В приведенном выше фрагменте есть две группы захвата: первая - для валюты ((£|€)), за ней следует числовое значение.

В переменной $matches будут храниться совпавшие результаты из обеих групп захвата:

 

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

Синтаксис неперехватывающей группы - это скобка, которая начинается с (?: и заканчивается ). Движок Regex подставляет выражение внутри скобок, но оно не возвращается как совпадение, т.е. не захватывается.

Если выражение выше интересует только числовое значение, группу захвата (£|€) можно превратить в группу без захвата: (?:£|€).

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

PHP 8.2 поддерживает модификатор /n, который делает все захватывающие группы не захватывающими, если они не поименованы.

Именованные захваты

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

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

Именованная группа захвата имеет синтаксис (?<, за которым следует имя группы, и заканчивается ).

В приведенном выше примере (?<currency>£|€) - это именованная группа захвата с именем currency, а (?<price>\d+) - с именем price. Имена обеспечивают небольшой контекст при чтении регулярного выражения, а также дают возможность назвать значения в массиве совпадающих значений.

Массив $matches теперь содержит имена и позиционные значения совпадающих значений.

Использование именованных групп захвата позволяет легко использовать значения $matches и легко изменить регулярное выражение позже, сохранив имя группы захвата.

По умолчанию группы захвата с дублирующимися именами не допускаются, что приводит к ошибке PHP Warning: preg_match(): Compilation failed: two named subpatterns have the same name (PCRE2_DUPNAMES not set) at offset ... in ... on line ..... Можно явно разрешить это дублирование именованных групп захвата с помощью модификатора J:

В этом регулярном выражении есть две группы захвата с именем currency, и оно явно разрешено с флагом J. Когда оно сопоставляется со строкой, оно возвращает только последнее совпадение для именованного значения захвата, но позиционные значения (0, 1, 2, ...) содержат все совпадения.

Использование комментариев

Некоторые регулярные выражения довольно длинные и занимают несколько строк.

Конкатенация регулярного выражения при комментировании отдельных подшаблонов или утверждений может улучшить читабельность и обеспечить меньший размер diff-вывода при проверке коммитов:

Кроме того, комментарии можно добавлять внутри самого регулярного выражения.

Существует флаг регулярного выражения, x, который заставляет механизм игнорировать все символы пробелов, что позволяет распределять, выравнивать или даже разбивать выражение на несколько строк:

В /Price: (?<currency>£|€)(?<price>\d+)/i, движок сопоставляет символ белого пробела сразу после строки Price:, но с флагом x все белые пробелы игнорируются. Чтобы найти белый пробел, используйте специальный символ \s.

Кроме того, с флагом x символ # начинает строчный комментарий, подобно синтаксису комментариев // и # в PHP.

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

При хранении в переменной PHP использование Heredoc/Nowdoc позволяет сохранить форматирование. Начиная с PHP 7.3, синтаксис heredoc/nowdoc стал более расслабленным.

Именованные классы символов

Регулярные выражения поддерживают классы символов, и они могут помочь снять напряжение с регулярных выражений и одновременно сделать их более читабельными.

\d - это, пожалуй, самый часто используемый класс символов. \d представляет собой одну цифру и эквивалентен [0-9] (в режиме не-Unicode). Далее, \D является инверсией \d и эквивалентен [^0-9].

Регулярное выражение, которое тщательно ищет цифры, за которыми не следует цифра, может быть упрощено без изменения функциональности:

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

\w эквивалентно [A-Za-z0-9_]:

[:xdigit:] - это именованный класс символов, который соответствует всем шестнадцатеричным символам и эквивалентен [a-fA-F0-9]:

\s - a соответствует всем пробельным символам, и эквивалентен [ \t\r\n\v\f]:

При использовании регулярных выражений с поддержкой Unicode (флаг /u), это позволяет использовать еще несколько классов символов. Именованные классы символов Unicode имеют вид \p{EXAMPLE}, где EXAMPLE - имя класса символов. Использование заглавной буквы P (например, \P{FOO}) является инверсией данного класса символов.

Например, \p{Sc} - это именованный класс символов для всех существующих и будущих символов валют. Существует их более длинная форма (например, \p{Currency_Symbol}), но на данный момент PHP их не поддерживает.

Классы символов позволяют захватывать/сопоставлять классы даже без предварительных знаний о символах. Новые символы валют, введенные в будущем, начнут соответствовать, как только эта информация будет включена в следующее обновление базы данных Unicode.

Классы символов Юникода также включают очень полезный список классов сценариев для всех сценариев Юникода. Например, \p{Sinhala} представляет все символы сингальского языка и эквивалентен \x{0D80}-\x{0DFF}.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий