Глава 22 · С нуля → практика
Регулярные выражения: точная грамматика и пределы
Проверяем артикула и извлекаем значения, отличая несовпадение от ошибки самого выражения.
Проверить формат артикула по маленькому правилу
Склад принимает артикул вида AB-1234: две заглавные латинские буквы, дефис, четыре цифры. Регулярное выражение описывает эту форму. Если нужно просто сравнить один готовый код, хватит ===; шаблон нужен, когда допустим целый набор строк по одному правилу.
| Часть шаблона | Смысл |
|---|---|
/ ... / | Границы записи шаблона для preg_match; не часть артикула. |
\A и \z | Начало и конец всей строки: посторонний текст по краям не принимается. |
[A-Z]{2} | Две буквы из диапазона A–Z. |
- | Буквальный дефис. |
[0-9]{4} | Ровно четыре ASCII-цифры. |
<?php
$sku = 'AB-1234';
$result = preg_match('/\A[A-Z]{2}-[0-9]{4}\z/', $sku);
if ($result === 1) {
echo "Формат подходит\n";
} elseif ($result === 0) {
echo "Формат не подходит\n";
} else {
echo "Ошибка самого шаблона\n";
}
Ожидаемый результат
Формат подходит
preg_match возвращает 1 при совпадении, 0 при отсутствии, false при ошибке. Значения проверены строго и раздельно. Совпадение с шаблоном не доказывает, что такой артикул существует в магазине.
Попробуйте сами. Проверьте ab-1234, AB-123 и XAB-1234.
Разбор решения
Все три не подходят: неверный регистр, нехватка цифры и лишний символ перед началом. Не убирайте якоря, чтобы «починить» ввод.
Чтобы повторять проверку, вынесите шаблон и выбор результата в функцию с аргументом sku. Ниже — извлечение групп, Unicode, диагностика ошибок и пределы длины/сложности. Начните с фиксированного собственного шаблона; произвольный пользовательский regex — отдельный договор и расходы.
Когда шаблон действительно нужен
Шаблон как трафарет для артикула
Магазин принимает артикулы вида AB-1234. Нужно проверить всю строку, а не случайно найти подходящий кусок внутри мусора.
- Строка от пользователя
Например AB-1234 или prefixAB-1234suffix.
- Правило формы
Две заглавные латинские буквы, дефис и четыре цифры; от начала до конца строки.
- Разрешить или отклонить
AB-1234 подходит. Строка с посторонними краями — нет.
Регулярное выражение проверяет форму записи, а не существование товара в базе. Ошибка самого шаблона — отдельный исход, её нельзя смешивать с обычным несовпадением.
Регулярное выражение описывает форму строки: две латинские буквы, дефис, четыре числа. Это полезно для артикула, фиксированного фрагмента лога или разделения по нескольким разделителям. Оно не заменяет JSON-парсер, HTML-парсер или бизнес-правило «товар существует и принадлежит продавцу». Для одной подстроки возьмите str_contains, для фиксированного разделителя explode: цель будет видна сразу.
<?php
declare(strict_types=1);
foreach (['AB-2048', 'ab-2048', 'AB-2048 extra'] as $sku) {
$matched = preg_match('/\A[A-Z]{2}-[0-9]{4}\z/', $sku);
if ($matched === false) {
throw new RuntimeException(preg_last_error_msg());
}
echo $matched === 1 ? "Формат принят\n" : "Неверный формат\n";
}
Ожидаемый результат
Формат принят
Неверный формат
Неверный формат
\A и \z задают начало и конец всей строки. Это исключает частичное совпадение. preg_match возвращает 1,0 или false: ошибка движка отличается от обычного отсутствия совпадения. Проверка формата не выдает доступ к товару.
Именованные группы делают извлечение понятным
<?php
declare(strict_types=1);
$line = 'order=O-17 status=paid';
$result = preg_match('/\Aorder=(?<id>O-[0-9]+) status=(?<status>[a-z]+)\z/', $line, $matches);
if ($result !== 1) {
throw new UnexpectedValueException('Неизвестный формат журнала');
}
echo $matches['id'], ':', $matches['status'], "\n";
Ожидаемый результат
O-17:paid
Имена id и status переживут добавление некаптурирующей группы лучше, чем магические индексы 1 и 2. Здесь журнал имеет фиксированный формат; для структурных JSON-логов используйте json_decode, чтобы обрабатывать экранирование правильно.
Unicode, пользовательский текст и замена
Модификатор u включает работу с UTF-8; невалидная последовательность может дать ошибку. Для букв разных алфавитов применяют Unicode-классы, но набор допустимых знаков выбирается под задачу. Имя человека не ограничивайте искусственным [A-Za-z]+: в реальности есть апострофы, дефисы и другие алфавиты. Не вклеивайте сырой пользовательский ввод в шаблон: для буквального поиска используйте preg_quote с тем же разделителем либо обычную строковую функцию.
<?php
declare(strict_types=1);
$title = " Красный\t чай\n";
$normalized = preg_replace('/\s+/u', ' ', trim($title));
if ($normalized === null) {
throw new RuntimeException(preg_last_error_msg());
}
echo $normalized, "\n";
Ожидаемый результат
Красный чай
preg_replace возвращает null при ошибке. Нормализация уместна для однострочного заголовка, но уничтожит значимые переносы в письме, коде или адресе. Сначала назовите тип поля, затем выбирайте преобразование.
Опасные шаблоны и стоимость
Вложенные неоднозначные повторения могут породить огромный перебор на почти совпадающем вводе. Ограничивайте размер строки, предпочитайте ясные диапазоны и длину, тестируйте длинный неподходящий хвост. Повышение pcre.backtrack_limit само по себе не метод лечения. Для пароля, например, часто яснее несколько проверок длины и политики, чем один гигантский шаблон. Отдельно собирайте ошибки движка, чтобы сломанный шаблон не выглядел как обычная ошибка пользователя.
Попробуйте сами. Артикул должен состоять из 1..20 латинских букв, цифр, дефиса. Какой шаблон и какие тесты?
Разбор решения
/\A[A-Za-z0-9-]{1,20}\z/. Проверьте пустую строку, длину 20 и 21, перевод строки в конце, кириллицу, пробел и массив вместо строки. До preg_match нужен is_string. Шаблон не устанавливает уникальность артикула.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
preg_matchПроверить формат кода заказа и извлечь его номер за один поиск.
preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|falseВ ~\AORD-([0-9]+)\z~ тильды обрамляют шаблон, \A/\z требуют всю строку, ORD- — буквальный префикс, [0-9]+ — хотя бы одна цифра, (...) сохраняет номер. $matches — обычная переменная; функция заполнит её сама.
| Аргумент / результат | Договор |
|---|---|
| pattern | Строка с разделителями: ~\AORD-[0-9]+\z~. |
| subject | Строка, в которой ищем. Поиск без якорей допускает совпадение внутри текста. |
| &matches = null | Записываемый массив: [0] всё совпадение, [1] первая захватывающая группа; именованные группы имеют и имя, и номер. |
| flags = 0 | PREG_OFFSET_CAPTURE меняет каждый элемент в [текст, byteOffset]; PREG_UNMATCHED_AS_NULL сохраняет незаполненные группы как null, вместе с offset как [null,-1]. Объединение через |. |
| offset = 0 | Начальная позиция в байтах; отрицательная — от конца. Это не substr: начало исходной строки и предыдущий контекст остаются видны. /u требует границы UTF-8 символа. |
| Результат | 1 — найдено, 0 — не найдено, false — отказ. Сравнивайте === false, затем === 1. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4+; встроенное расширение pcre |
- ORD-104 → проверка префикса и цифр.
- Группа ([0-9]+) сохраняет 104.
- $status = 1; $matches[1] = "104" → номер заказа.
<?php
declare(strict_types=1);
$status = preg_match('~\AORD-([0-9]+)\z~', 'ORD-104', $matches);
if ($status === 1) { echo 'Номер: ', $matches[1], "\n"; }
Ожидаемый результат
Номер: 104
if выполняет блок только при найденном совпадении. Индекс 1 содержит текст цифр, не целое число; ведущие нули не теряются.
<?php
declare(strict_types=1);
function orderNumber(string $code): ?string
{
$result = preg_match('~\AORD-(?<number>[0-9]+)\z~', $code, $m);
if ($result === false) { throw new RuntimeException(preg_last_error_msg()); }
return $result === 1 ? $m['number'] : null;
}
echo orderNumber('ORD-007'), "\n";
echo orderNumber('ORD-7x') ?? 'Исправьте код', "\n";
Ожидаемый результат
007
Исправьте код
function объявляет повторно используемое действие; ?string означает строку или null. Оператор ?? подставляет сообщение только для null. Ошибка движка не становится сообщением «плохой код».
<?php
declare(strict_types=1);
preg_match('~(?<code>BR[0-9]+)(?:-(?<size>[0-9]+))?~u', 'Я BR41', $m,
PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL);
echo $m['code'][0], '@', $m['code'][1], "\n";
echo $m['size'][0] === null ? 'size:null' : 'size:set';
echo '@', $m['size'][1], "\n";
Ожидаемый результат
BR41@3
size:null@-1
Я занимает 2 байта, затем пробел: BR41 начинается с байта 3. (?:...) группирует без отдельного результата; ? делает суффикс необязательным. Эти формы подробно разобраны в синтаксисе главы.
Не читайте $matches до проверки успеха. PREG_UNMATCHED_AS_NULL появился в 7.2; с 7.4 включает и незаполненные конечные группы. Для поиска буквального фрагмента удобнее str_contains/strpos. Отрицательный offset слишком далеко до начала в PHP прижимается к 0; offset после конца даёт false и internal error. Не делайте из ошибочного offset отсутствие совпадения.
Попробуйте. Замените + на {3}: ORD-007 проходит, ORD-7 нет. {3} требует ровно три цифры.
Дальше: preg_match_all, preg_quote, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.
Следующая практика. Прежние упражнения продолжают введение.
Возвращает 1 при совпадении,0 при отсутствии и false при ошибке. Matches получает найденные группы; шаблон принадлежит программе.
<?php
declare(strict_types=1);
$raw = '25';
$ok = preg_match('/\A[1-9][0-9]{0,2}\z/', $raw);
echo $ok === 1 ? "1..999\n" : "Неверно\n";
Ожидаемый результат
1..999
Тип raw должен быть проверен до этого фрагмента. Ограниченная длина не позволит переполнить int при последующем приведении.
<?php
declare(strict_types=1);
$result = preg_match('/\Aorder:(?<id>[1-9][0-9]*)\z/', 'order:2048', $matches);
if ($result !== 1) {
throw new UnexpectedValueException('Не ссылка заказа');
}
echo $matches['id'], "\n";
Ожидаемый результат
2048
Группа возвращается строкой. Перед int добавьте допустимый предел; проверка формата не заменяет права доступа к заказу.
Сравните. Для подстроки есть str_contains; preg_match_all находит все совпадения; парсеры JSON/CSV лучше регулярки для своих форматов.
preg_replaceПривести повторяющиеся пробелы к одному или перестроить текст по захваченным полям.
preg_replace(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, int &$count = null): string|array|nullТри обязательных аргумента читаются как «что искать, чем заменить, где искать». В первом примере [ \t]+ собирает подряд пробелы и табуляции, замена " " оставляет один пробел. $1 в одинарных кавычках остаётся текстом до обработки preg_replace.
| Аргумент / результат | Договор |
|---|---|
| pattern | Одна строка-шаблон с разделителями либо массив шаблонов. |
| replacement | Строка либо массив замен, по порядку элементов pattern, не по совпадающим ключам. Если replacement — массив, pattern тоже должен быть массивом; иначе TypeError. Одна строка обслуживает все шаблоны; недостающие элементы массива означают пустую замену. $0 — всё совпадение, $1…$99 / \1…\99 — числовые группы; ${1}2 отделяет группу 1 от цифры 2. |
| subject | Строка либо массив строк. Ключи массива сохраняются; правила применяются к каждому значению. |
| limit = -1 | Максимум замен на каждый шаблон в каждой строке. -1 — все, 0 — ни одной. |
| &count = null | Необязательная переменная: общее число сделанных замен. & в сигнатуре означает запись результата в переданную переменную. |
| Результат | Новая строка/массив, прежние значения без совпадений сохраняются. null при технической ошибке. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4+; встроенное расширение pcre |
- "Bolt M10" → найдено три пробела.
- Одна замена на " ".
- Результат "Bolt M10", count = 1.
<?php
declare(strict_types=1);
$clean = preg_replace('~[ \t]+~', ' ', 'Bolt M10', -1, $count);
echo $clean, "\n", 'Замен: ', $count, "\n";
Ожидаемый результат
Bolt M10
Замен: 1
Количество замен — число совпадений, не число удалённых байтов. Эта версия сохраняет переводы строк.
<?php
declare(strict_types=1);
$label = preg_replace('~\A([A-Z]{2})([0-9]+)\z~', '$1 / ${2}0', 'BR41');
echo $label, "\n";
Ожидаемый результат
BR / 410
Группа 2 содержит 41; ${2}0 добавляет буквальный 0. Это форматирование, не умножение числа. $name по имени в replacement не поддерживается: для имён применяйте callback.
<?php
declare(strict_types=1);
$result = preg_replace(['~old~', '~new~'], ['new', 'ready'],
['a' => 'old old', 'b' => 'old'], 1, $count);
foreach ($result as $key => $value) { echo $key, ':', $value, "\n"; }
echo 'Замен: ', $count, "\n";
Ожидаемый результат
a:ready old
b:ready
Замен: 4
Сначала old становится new, затем new становится ready. limit=1 действует отдельно на каждую пару шаблон/строка, count суммирует четыре операции.
Правила массива последовательны: следующее видит результат предыдущего. Ключи pattern/replacement не сопоставляются: важен порядок обхода; согласуйте порядок явно. Для одновременного словаря буквальных замен используйте strtr. preg_quote предназначен для pattern, не для replacement. Старый /e удалён в PHP7: вместо исполнения текста используйте явную callback-функцию. Перед чтением результата отличите null от допустимой пустой строки.
Попробуйте. Поставьте limit=0 в первом примере: исходные три пробела сохранятся, count станет 0.
Дальше: preg_replace_callback, preg_filter, preg_quote. Как устроен шаблон и вызов; ошибка или нет совпадения.
Следующая практика. Прежние упражнения продолжают введение.
Возвращает результат замен, либо null при ошибке. Limit ограничивает число замен для каждого шаблона; count получает выполненное число.
<?php
declare(strict_types=1);
$result = preg_replace('/\s+/u', ' ', "Чай\t зеленый");
if ($result === null) {
throw new RuntimeException(preg_last_error_msg());
}
echo $result, "\n";
Ожидаемый результат
Чай зеленый
Нормализация подходит для однострочного поля. Не применяйте к отзыву с абзацами без решения, что переносы незначимы.
<?php
declare(strict_types=1);
$line = 'pin=123456 status=sent';
$safe = preg_replace('/\bpin=[0-9]{6}\b/', 'pin=[hidden]', $line, 1, $count);
if ($safe === null) {
throw new RuntimeException('Ошибка шаблона');
}
echo $safe, ':', $count, "\n";
Ожидаемый результат
pin=[hidden] status=sent:1
Лучше изначально не писать секрет в журнал. Это ограниченный пример известного формата, а не универсальный редактор персональных данных.
Сравните. str_replace проще для точной строки; callback позволяет вычислять замену по именованным группам.
preg_splitРазделить список тегов, где между словами встречаются запятые, точки с запятой и пробелы.
preg_split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array|false[,;\s]+ — один или более символов из набора: запятая, ; или пробельный. NO_EMPTY убирает пустую часть перед первым и после последнего разделителя. implode здесь только собирает результат для печати.
| Аргумент / результат | Договор |
|---|---|
| pattern | Шаблон разделителя, не шаблон нужного слова. |
| subject | Одна строка. |
| limit = -1 | Максимум обычных частей; остаток попадает в последнюю. -1 и 0 — без ограничения, 1 — вся строка. Захваченные разделители не считаются обычными частями. |
| flags = 0 | PREG_SPLIT_NO_EMPTY убирает пустые части; PREG_SPLIT_DELIM_CAPTURE возвращает захватывающие группы разделителя; PREG_SPLIT_OFFSET_CAPTURE делает элементы парами [текст, byteOffset]. Флаги объединяются |. |
| Результат | Массив частей; false при ошибке. Без найденного разделителя обычно [subject], а не отказ. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4+; встроенное расширение pcre |
- " гайки, болты; шайбы " → группы разделителей.
- Пробелы и пунктуация удаляются как разделители.
- Остаются три тега в исходном порядке.
<?php
declare(strict_types=1);
$tags = preg_split('~[,;\s]+~u', ' гайки, болты; шайбы ', -1, PREG_SPLIT_NO_EMPTY);
echo implode(' / ', $tags), "\n";
Ожидаемый результат
гайки / болты / шайбы
/u — UTF-8 и Unicode-пробелы; запятая внутри самого тега этим форматом не допускается. Для CSV с кавычками нужен CSV-парсер.
<?php
declare(strict_types=1);
$parts = preg_split('~\s*=\s*~', 'title = Bolt = M10', 2);
echo $parts[0], "\n", $parts[1], "\n";
Ожидаемый результат
title
Bolt = M10
limit 2 запрещает дальнейшее деление после первого разделителя; второе = остаётся частью значения.
<?php
declare(strict_types=1);
$parts = preg_split('~([+\-])~', '12+5-2', -1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE);
foreach ($parts as [$text, $offset]) { echo $text, '@', $offset, "\n"; }
Ожидаемый результат
12@0
+@2
5@3
-@4
2@5
Скобки захватывают знак, поэтому он не теряется. Это только токены калькулятора, ещё не вычисление и не разрешение выполнять ввод как PHP.
Пустой шаблон может разбить текст на байты, с /u — на кодовые точки; это не всегда видимые графемы. Для графем есть \X, для одного буквального разделителя — explode. NO_EMPTY удаляет пустые строки, но не строку "0". Вложенные захватывающие группы разделителя добавляют дополнительные элементы — чаще нужна одна группа или (?:...).
Попробуйте. Замените строку на "0;;1": NO_EMPTY оставит оба значения 0 и 1.
Дальше: preg_match_all, preg_quote. Как устроен шаблон и вызов; ошибка или нет совпадения.
Следующая практика. Прежние упражнения продолжают введение.
Возвращает части строки либо false при ошибке. PREG_SPLIT_NO_EMPTY пропускает пустые части; другие флаги позволяют сохранить разделители и байтовые смещения.
<?php
declare(strict_types=1);
$parts = preg_split('/\s*[,;]\s*/u', 'php, api; shop', -1, PREG_SPLIT_NO_EMPTY);
if ($parts === false) {
throw new RuntimeException('Не разделить строку');
}
echo implode('|', $parts), "\n";
Ожидаемый результат
php|api|shop
Формат явно разрешает два разделителя. Если сам тег может содержать запятую, нужен структурный формат или экранирование с настоящим парсером.
<?php
declare(strict_types=1);
$words = preg_split('/\s+/u', trim(' чай зеленый '), -1, PREG_SPLIT_NO_EMPTY);
if ($words === false) {
throw new RuntimeException('Неверный текст');
}
echo count($words), ':', implode(',', $words), "\n";
Ожидаемый результат
2:чай,зеленый
Это разбиение по пробелам, а не языковая морфология. Для кавычек, операторов поиска и синонимов потребуется отдельный разбор.
Сравните. explode для одного фиксированного разделителя понятнее и не требует движка шаблонов.