Nickolay.info. PHP. Регулярные выражения - это просто

Регулярные выражения - это просто

Введение

Попросту говоря, регулярные выражения (regular expressions, сокращенно regexp, regex) - это шаблоны, которые можно сопоставлять со строками. В отличие от обычного определения вхождения подстроки в строку, которое в PHP можно делать, например, с помощью функции strpos, шаблоны позволяют включать в искомое или заменяемое строковое выражение групповые символы (wildcards) - именно в этом их сила.

Регулярные выражения доступны не только в PHP, но и в большинстве современных языков и пакетов, работающих с текстом - от Perl, Javascript и .NET Framework до Microsoft Office, так что знать их полезно любому грамотному пользователю, не говоря уж о программистах. Однако, начинающих regexp'ы нередко пугают, а толстенные руководства наводят тоску (например, самая классическая и рекомендуемая книга - Mastering Regular Expressions автора Jeffrey Friedl).

Поэтому постараемся сказать обо всём предельно кратко и доступно.

В PHP доступно 2 типа регулярных выражений:

Символьные классы и специальные символы

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

^
Начало строки. Ставится перед символом или шаблоном, на который действует.
- строка начинается с "а"

$
Конец строки. Ставится после символа или шаблона, на который действует.
а$ - строка заканчивается на "а"

[]
В квадратных скобках указываются альтернативные символы, в строке может присутствовать любой из них.
PHP[345] может означать "PHP3", "PHP4" или "PHP5"

- внутри квадратных скобок
Символом "минус" (дефис) разделяются первый и последний символы последовательности, в строке может присутствовать любой символ от первого до последнего включительно. Разумеется, "последовательности символов" имеют смысл лишь относительно текущей кодировки. Например, в UTF-8 (Юникоде) и Windows-1251 (стандартная русифицированная Windows) русские буквы (кроме "ё") гарантированно имеют идущие подряд коды, в кодировках DOS и КОИ-8R это не так.
PHP[3-5] может означать "PHP3", "PHP4" или "PHP5"

^ внутри квадратных скобок
В строке допустим любые символы, кроме указанных этим знаком.
[^A-C] означает любую цифру, или букву от "D" и далее по алфавиту и т.п.

|
Используется как разделитель для нескольких альтернативных шаблонов.
PHP4|PHP5 означает "PHP4" или "PHP5"

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

.
Точка обозначает любой символ.
PHP. может означать "PHP3", "PHP4", "PHP5" или "PHPA" и т.д.

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

Перечислим ещё несколько полезных специальных символов, которые есть в PCRE-совместимых регулярных выражениях:
\s - пробел или табуляция или иной пробельный (space) символ;
\S - видимые символы, можно считать, что это всё, что не совпадает с \s;
\w - в этот спецсимвол включены все символы, которые могут входить в слово (word), обычно [a-zA-Z_], хотя это зависит от установленной локали, поддержки Юникода и т.п.;
\W - всё, что не входит в определение \w, то есть, [^a-zA-Z_];
\d - цифра (digit; специальный символ позволяет не писать символьный класс [0-9]);
\D - всё, что не является цифрой.

Квантификаторы

Самое важное - один символьный класс описывает только один символ, а когда символов много, применяются квантификаторы.

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

[a-z][a-z][0-9][0-9]

А если мы не поленимся указать квантификаторы, запись можно изменить:

[a-z]{2}[0-9]{2}

А вот и сами квантификаторы:

?
Ноль или одно повторение предыдущего символа или выражения ("ноль или один").
аб? может означать "а" или "аб"

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

+
Одно или более повторений предыдущего символа или выражения ("хотя бы один", "один или много").
аб+ может означать "аб" или "абб" или "аббб" и т.д.

{n}
Ищется ровно n вхождений предыдущего символа или шаблона.
[0-9]{3} - три любых идущих подряд цифры

{min,max}
Такая конструкция в фигурных скобках обозначает минимальное (min) и максимальное (max) число вхождений, если опущен минимум min (но не запятая), он предполагается равным 0, если опущен максимум max (но не запятая), он предполагается равным infinite (бесконечности).
a{1,3} может означать "а" или "аа" или "ааа"
a{1,} может означать цепочку из букв "а" любой длины, но не менее одного символа
a{,3} может означать цепочку из букв "а" в количестве не более трёх (обратите внимание, ноль букв "а" тоже попадает под это выражение!)

Манипулируя указанными выше символьными классами, специальными символами и квантификаторами, можно составить много полезных примеров, скажем, таких:
[^\s] - любой символ, который не является пробельным;
[^\s]+ - минимум один символ, который не является пробельным;
\s+ - минимум один пробельный символ;
^\d+$ - строка является числом из одной и более цифр (^ вне квадратных скобок - не исключение символов, а начало строки; обратите также внимание, что здесь вся строка должна соответствовать шаблону - так как в него включены метки начала ^ и конца $ строки);
^[a-zA-Z0-9]+$ - латинские буквы и цифры, минимум один символ;
^[a-zа-я0-9_]{1,8}$ - строка только из латинских или русских букв, цифр и подчёркивания от 1 до 8 символов длиной (игнорирование регистра обычно можно указать с помощью описанных ниже модификаторов шаблонов);
[^(\x7F-\xFF)|(\w)|(\s)] - исключаем символы с кодами 127 и больше (как видите, в регулярных выражениях можно писать 16-ричные коды символов в стиле языка Си), разрешаем печатаемые и пробельные символы.

Стандартные функции для работы с регулярными выражениями

Как всё это использовать в PHP? Да очень просто, нам понадобится всего несколько стандартных функций:

Посмотрим подробнее на каждую из этих функций.

int preg_match (string $pattern, string $subject [, array $matches]);

Функция ищет в заданном тексте $subject совпадения с шаблоном $pattern, если задан необязательный третий параметр - массив $matches, то пишет в него результаты поиска.

Элемент $matches[0] будет содержать часть строки, соответствующую вхождению всего шаблона, $matches[1] - часть строки, соответствующую первому подшаблону (если он есть), и так далее.

Возвращает количество найденных соответствий. Это может быть 0 (совпадения не найдены) и 1, поскольку preg_match прекращает свою работу после первого найденного совпадения.

Приведём пример, выбирающий из произвольной строки номер российского сотового телефона вида +7XXXXXXXXXX или 8XXXXXXXXXX, где X - цифры:

$number='Мой номер сотового: +79169013311';
 if (!preg_match("/(\+7|8)(\d{10})/",$number, $matches))
  echo 'В строке '.$number.' не найден российский номер сотового!';
 else echo 'Найден номер '.$matches[0];

В элементе $matches[0] будет содержаться весь найденный номер, в $matches[1] - первый подшаблон (+7 или 8), а в $matches[2] - второй подшаблон (10 цифр сотового номера).

Попробуйте улучшить этот пример, разрешив между группами цифр шаблона пробелы или тире, так чтоб находилось +7 XXX XXX XX XX или 8-XXX-XX-XX-XXX.

Обратим внимание на один нюанс: шаблон поиска в первом аргументе функции не только взят в "двойные кавычки", как любая интерпретируемая строка PHP, но и заключён в символы-ограничители / ... /, во-первых, эти ограничители нужны функциям, чтобы они могли "понять" шаблон (кроме / ... / используют также # ... #), во-вторых, это делается потому, что после закрывающего символа / могут ставиться так называемые модификаторы шаблонов, уточняющие их действие:

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

Рассмотрим остальные функции, применяемые для поиска в строках по шаблонам regexp'ов.

int preg_match_all (string $pattern, string $subject, array $&matches);

В отличие от предыдущей функции, третий аргумент здесь обязателен. Функция preg_match_all выполняет глобальный поиск шаблона $pattern в строке $subject и помещает результаты поиска в массив $matches.

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

Чтобы понять, как это работает, поищем сначала что-нибудь простое, например, целые числа с допустимым, но необязательным знаком "+" или "-" перед первой цифрой.

$numbers='11, -1, +1.5, -2e4';
 $res=preg_match_all("/[\+|-]?[\d]+/",$numbers, $matches);
 if ($res===false) echo 'Ошибка!';
 else if ($res===0) echo 'Ничего не найдено';
 else {
  for ($i=0; $i<$res; $i++) echo '<br>'.$matches[0][$i];
 }

Первый вопрос по нашему примеру - почему мы выводили элементы $matches[0][$i]? Дело в том, что после поиска элемент $matches[0] содержит массив полных вхождений шаблона, элемент $matches[1] содержит массив вхождений первого подшаблона, и так далее.

Таким образом, $matches[0][0] - это первое найденное число, $matches[0][1] - второе и т.д.

Вместо $res можно было воспользоваться для определения количества найденных чисел выражением count($matches[0]).

Второй вопрос - почему наш поиск "неожиданно" нашёл в строке $numbers целых 6 чисел?

11
-1
+1
5
-2
4

В самом деле, мы ведь не указали, что цифра должна начинаться после пробела или с начала строки?

Но писать шаблон вроде "/ [\+|-]?[\d]+/" (добавлен пробел перед знаком числа) не стоит - потеряем число 11, которое является первым в строке. Пытаться использовать символ "^" (начало строки) тоже не будем - тогда есть риск потерять все остальные числа.

Вместо этого попытаемся исключить символы, которых непосредственно перед целым числом заведомо не может быть - потому что они есть только в вещественных числах, например, десятичную точку и букву e (используется в так называемой экспоненциальной записи вещественных чисел, где 2e4 означает 2*104): "/[^.eE][\+|-]?[\d]+/"

Такой шаблон найдёт в строке $numbers ровно 4 числа:

11
-1
+1
-2

Легко заметить, что и этот шаблон несовершенен. Например, в строке

2E-4 

(в экспоненциальной записи числа после E или e можно использовать знак "-" или "+", показывающий, на отрицательную или положительную степень числа 10 нужно умножить основание) наш шаблон найдёт 2 цифры: 2 и -4.

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

Давайте перевернём задачу с головы на ноги. У нас есть строки формата [знак][цифры][остальное], нам нужно оставить только [знак][цифры].

Вынесем [остальное] в отдельный подшаблон, и будем печатать не $matches[0][$i], а $matches[1][$i]. Часть [знак][цифры] легко опишется подшаблоном ([\+|-]?[\d]+), а часть [остальное] может иметь вид ([^\s,]*) (любые символы, за исключением пробельных и запятой - последнее на случай, если 2 целых числа разделены запятой без пробела: 1,2).

Получаем следующий код:

$numbers='33.01e-2 21.5E-004 11e+01, -1,33, +1.5, -2e4';
 $res=preg_match_all("/([\+|-]?[\d]+)([^\s,]*)/",$numbers, $matches);
 if ($res===false) echo 'Ошибка!';
 else if ($res===0) echo 'Ничего не найдено';
 else {
  for ($i=0; $i<$res; $i++) echo '<br>'.$matches[1][$i];
 }

Он найдёт то, что мы вправе и ожидать для строки $numbers:

33
21
11
-1
33
+1
-2

Поведение функции preg_match_all можно изменить, если передать ей четвёртым параметром константу PREG_SET_ORDER. Тогда $matches[0] будет содержать первый набор вхождений, $matches[1] - второй набор вхождений, и т.д.

То есть, элемент $matches[0][0] содержит первое вхождение всего шаблона, элемент $matches[0][1] содержит первое вхождение первой подмаски, и т.д. В нашем цикле изменится лишь порядок обращения к элементам массива $matches:

$numbers='11.006E+014, -1.473, +1.5, -2e4';
 $res=preg_match_all("/([\+|-]?[\d]+)([^\s,]*)/",$numbers, $matches, PREG_SET_ORDER);
 if ($res===false)echo 'Ошибка!';
 else if ($res===0) echo 'Ничего не найдено';
 else {
  for ($i=0; $i<$res; $i++) echo '<br>'.$matches[$i][1];
 }

Наконец, указав вместо PREG_SET_ORDER флаг PREG_OFFSET_CAPTURE, мы могли для каждой найденной подстроки возвращать её позицию в исходной строке.

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

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

string preg_replace (string $pattern, string $replacement, string $subject);

Функция ищет в строке $subject совпадения с шаблоном $pattern и заменяет их на $replacement.

При разборе этой функции полезными окажутся подмаски, соответствующие заключённым в круглые скобки частям шаблона (подшаблонам). Подмаски последовательно нумеруются, начиная с 1, подмаска с номером 1 может обозначаться в строке шаблона как \\1 либо как $1, причём последний вариант предпочтительней. Подмаска \\0 или $0 соответствует нахождению всего шаблона.

Если непосредственно за подмаской в строке $replacement должна следовать цифра, во избежание путаницы подмаска изолируется фигурными скобками: \${1}0 соответствует первой подмаске и следующей за ней цифре 0, в противном случае \$10 будет воспринято как десятая подмаска.

Если во время выполнения функции были обнаружены совпадения с шаблоном, будет возвращено изменённое значение $subject, в противном случае будет возвращен исходный текст $subject.

Приведём пример, в котором из записи чисел в экспоненциальной форме убирается "всё лишнее", то есть, предшествующие знаки "+" или "-", дробная часть числа, если она есть, а также вся часть, начиная с буквы "e" или "E". Наш предыдущий шаблон для удобства мы перепишем так, чтобы он непосредственно отражал структуру числа в экспоненциальной форме:

Теперь попробуем подставить этот шаблон в небольшую программку (естественно, убрав из шаблона разрывы строк):

$numbers='11.07E-034, -1, +1.5006, -2e-11, 5';
 $res=preg_replace("/([\+|\-]?)(\d+)(\.?)(\d*)([e|E]?)([\+|\-]?)(\d*)/", "$2", $numbers);
 if ($res==$numbers) echo 'Замены не сделаны';
 else echo $res;

На экране увидим следующее:

11, 1, 1, 2, 5

Существенно, что параметры $subject, $pattern и $replacement могут быть массивами, в этом случае обработка производится для каждого из их элементов.

Модификатор /e меняет поведение функции preg_replace так, что параметр $replacement после выполнения подстановок интерпретируется как PHP-код и только после этого используется для замены. Если предложенный код некорректен, возникнет ошибка синтаксиса.

У функции есть версия preg_replace_callback, при вызове которой вместо параметра $replacement программист может указать собственную функцию, которой передается массив найденных вхождений.

Итак, круглые скобки позволяют ещё и запоминать подшаблоны для дальнейшего использования в регулярном выражении. Если нам нужно только группировать подшаблоны, но не запоминать их, сэкономить ресурсы компьютера можно с помощью последовательности символов "?:" (вопросительный знак и двоеточие) после открывающей круглой скобки, которая позволяет скобкам не запоминать:

(?:[0-9]*)([a-z]*)

Здесь первая пара круглых скобок - не запоминающая, так что код

$str='0123abs dddeee 01aa';
 $res=preg_replace("/(?:[0-9]*)([a-z]*)/", "$1", $str);
 echo $res;

покажет в качестве $1 значения

abs dddeee aa

Четвёртая полезная стандартная функция - preg_split, она разбивает строку по регулярному выражению и возвращает массив, состоящий из подстрок:

array preg_split (string $pattern, string $subject [, int $limit [, int $flags ]])

Как видно из общей записи, у функции 2 обязательных параметра и 2 необязательных.

Параметр $pattern по-прежнему обозначает регулярное выражение, а $subject - строку, с которой производится работа.

Параметр $limit, если он задан, показывает максимальное число возвращаемых строк. Если количество строк не ограничено, но нужно указать четвёртый параметр ($flags), передаётся $limit=-1.

Параметр $flags может быть произвольной комбинацией следующих флагов (соединение происходит при помощи оператора '|'):

Функция возвращает массив, состоящий из подстрок строки $subject, которая разбита по границам, соответствующим шаблону $pattern.

Например, для разбиения строки на слова, разделённые пробельными символами ("\r", "\n", "\t", "\f", " "), достаточно следующего кода:

$text = "Это строка текста,
состоящая из слов, разделённых
пробелами или запятыми.";
 $words = preg_split("/[\s,]+/", $text);
 for ($i=0; $i<count($words); $i++) echo '<br>'.$words[$i];

Для посимвольного разбиения строки можно было поступить ещё проще:

$str = 'Строка текста';
 $chars = preg_split('//', $str, -1, PREG_SPLIT_NO_EMPTY);
 print_r($chars);

Здесь стандартная функция print_r распечатает массив в удобном для отладки виде:

Array ( [0] => С [1] => т [2] => р [3] => о [4] => к [5] => а [6] => [7] => т 
 [8] => е [9] => к [10] => с [11] => т [12] => а )

Жадность и лень

Теперь немного о жадности и лени, да-да, "жадные" и "ленивые" квантификаторы - вполне официальные термины.

Представим, что нам нужно найти в произвольном тексте все теги HTML, заключённые в треугольные скобки. Шаблон тега будет очень простым: "/<.*>/". Проверим, как это работает, на произвольной строке с тегами:

$html = '<p>Абзац <u>текста</u> с <b>различными</b> тегами.</p>';
 $res=preg_match_all("/<.*>/",$html, $matches);
 if ($res===false) echo 'Ошибка!';
 else if ($res===0) echo 'Ничего не найдено';
 else {
  echo '<br>Всего тегов: '.$res;
  for ($i=0; $i<$res; $i++) echo '<br>'.htmlspecialchars($matches[0][$i]);
 }

Увы, результатом выдачи оказалась вся исходная строка:

Всего тегов: 1
<p>Абзац <u>текста</u> с <b>различными</b> тегами.</p>

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

Такое поведение называется жадным, в частности, жадность квантификатора "*" и привела к увиденному нами результату.

Помимо жадных, существуют и нежадные или ленивые (lazy) квантификаторы, которые наоборот ищут минимальное, наиболее короткое соответствие.

Чтобы пометить квантификатор как ленивый, обычно достаточно добавить после него знак вопроса:

Жадный	Ленивый
*	*?
+	+?
{n,}	{n,}?

Как Вы понимаете, добавлять дополнительный вопрос к обычному квантификатору "?", обозначающему ноль или один символов, бессмысленно.

С ленивым квантификатором "*" наш шаблон примет вид "/<.*?>/" и программа благополучно напечатает следующее:

Всего тегов: 6
<p>
<u>
</u>
<b>
</b>
</p>

Следует понимать, что использование ленивых квантификаторов вместо жадных может повлечь за собой обратную проблему, в частности, когда выражению соответствует слишком короткая или вообще пустая строка. Иногда имеет смысл вместо "лени" поработать над шаблоном, например, в нашем случае теги можно было найти и обычным жадным квантификатором, применив шаблон "/<[^>]*>/" (исключили символ ">" из "внутренностей" тега).

Наконец, существует ревнивая или сверхжадная квантификация.

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

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

Ставить их можно так:

Жадный	Ревнивый
*	*+
?	?+
+	++
{n,}	{n,}+

Позиционные проверки

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

Чтобы была возможность описать и то, чего не должно быть перед искомой подстрокой или после неё, ретроспективную и опережающую проверки делят ещё на 2 вида каждую:

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

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

Проверки описываются в круглых скобках следующим образом:

Наверное, нетрудно отследить логику этих обозначений: ретроспективность обозначена знаком "<", словно отсылающим налево от строки, позитивность - знаком "=", негативность - знаком "!", напоминающим об операции "не равно" или отрицания во многих языках программирования.

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

Например, если в тексте часть цифр выделена тегом жирного начертания <b>...</b>, а часть нет, нам же нужно "вытащить" только цифры, выделенные жирным, помочь может вот такой шаблон:

(?<=\<b\>)(?<!\<\/b\>)([\s]*[\d]+[\s]*)

Здесь перед цифрами числа [\d]+, окруженными произвольным количеством пробельных символов [\s]*, мы требуем наличия открывающей части тега жирного шрифта (?<=\<b\>) и отсутствия его закрывающей части (?<!\<\/b\>). Попробуем применить это к какому-нибудь фрагменту разметки HTML:

$text="
В этом <b>тексте</b> некоторые цифры, например, <b>12</b>, выделены <b>жирным</b>, 
а другие, например, 13, нет. Нам нужно \"вытащить\" только те, которые 
выделены: <b> 14 </b>, <i>15</i>, <b></b>16</b>.
 ";
 $res=preg_match_all("#(?<=\<b\>)(?<!\<\/b\>)([\s]*[\d]+[\s]*)#",$text, $matches);
 if ($res===false) echo 'Ошибка!';
 else if ($res===0) echo 'Ничего не найдено';
 else {
  for ($i=0; $i<$res; $i++) echo '<br>'.$matches[0][$i];
 }

Вывод программы:

12
14

Лишние пробелы до и после числа 14 помехой не станут, а вот учесть запись вида <b><i>14</i></b> наш шаблон уже не сможет, и "расширения" ретроспективной позитивной проверки вроде (?<=\<b\>[.]*) здесь не помогут, более того, они вызовут синтаксическую ошибку. Дело в том, что позиционные проверки подчиняются ряду ограничений:

Заключение

Особо хотелось бы предостеречь читателя от искушения использовать "всемогущие" регулярные выражения везде и всюду, ведь, помимо массы достоинств, они имеют и недостатки. Главный из них - заведомо более медленная работа по сравнению со стандартными строковыми функциями или обращениями к элементам массивов. Например, писать поиск по большой таблице базы данных с помощью регулярных выражений я бы не стал - гораздо перспективнее выглядит в этом плане инструкция LIKE из MySQL, позволяющая искать вхождение строки в строковое поле таблицы.

Вообще, если не нужны сложные правила поиска и замены строк, использованию preg_replace следует предпочитать str_replace, preg_split может быть заменена обычной split, а с простыми задачами по поиску и обработке фикированных подстрок вместо preg_match и preg_match_all успешно справляются strpos, strstr, substr, substr_replace и целый ряд других стандартных функций, имеющихся в PHP.

Рейтинг@Mail.ru

вверх гостевая; E-mail