Глава 80 · От первого запуска к редким конструкциям
PCRE: от первого правила до полного справочника preg_*
Все функции и вложенные темы официального book.pcre: форма текста, Unicode, группы, диагностика, конфигурация, ограничения и современная PCRE2.
Сначала полезный результат: проверить код товара
Шаблон ищет нужную часть текста
Нужно извлечь номер из строки «Заказ #42», а не заставить читателя сначала выучить всю грамматику регулярных выражений.
- Текст: Заказ #42
Исходная строка остаётся неизменной.
- Шаблон: /#([0-9]+)/
# буквально; [0-9]+ — одна или больше цифр; скобки сохраняют найденное.
- preg_match → 1
Совпадение найдено. 0 означает отсутствие, false — ошибку.
- $matches[1] = «42»
Это ещё строка цифр; правила допустимого номера задаёт приложение.
Сначала решаем одну задачу и разбираем каждый знак. Затем изучаем группы, Unicode, замены, ограничения и стоимость поиска.
В заявке пришёл код BR41. Нужно понять, начинается ли он с BR и заканчивается ли цифрами, затем получить эти цифры. Регулярное выражение — маленькое правило формы текста; preg_match применяет его к строке. Правило не знает, существует ли товар, и не заменяет проверку склада. Начнём с одного вызова, затем раскроем его запись и расширим задачу. Полный официальный раздел PCRE здесь включает все функции, настройки и вложенные темы языка; редкие конструкции идут после основ.
Сохраните пример в UTF-8-файл regex-demo.php без BOM и выполните установленным PHP CLI. <?php начинает PHP-код; $code — имя переменной, = присваивает ей значение, кавычки задают текст, ; завершает команду. Скобки после preg_match передают аргументы через запятую: сначала правило, затем исходную строку. $found сохраняет ответ; echo выводит его. "\n" в двойных кавычках — перевод строки. Первый запуск PHP показывает запуск по шагам.
- $code хранит BR41.
- Правило ~\ABR[0-9]+\z~: начало, BR, одна или больше цифр, конец.
- preg_match возвращает 1: весь BR41 подходит.
- echo выводит 1; никаких внешних действий не выполняется.
<?php
$code = 'BR41';
$found = preg_match('~\ABR[0-9]+\z~', $code);
echo $found, "\n";
Ожидаемый результат
1
Тильды ограничивают тело шаблона; \A/\z привязывают его к целой строке. BR — обычные буквы, [0-9] выбирает цифру, + повторяет выбор хотя бы раз. Кавычки принадлежат PHP, не самому regex.
У preg_match три разных ответа: 1 найдено, 0 не найдено, false техническая ошибка. Оператор === сравнивает и значение, и тип. if (...) выполняет блок в { ... } только при истинном условии, elseif проверяет следующую ветвь, else — оставшийся случай. echo печатает своё сообщение: один голый false на экране был бы незаметен.
<?php
$code = 'BR?';
$found = preg_match('~\ABR[0-9]+\z~', $code);
if ($found === false) {
echo "Проверка не выполнилась\n";
} elseif ($found === 1) {
echo "Форма кода подходит\n";
} else {
echo "После BR нужны цифры\n";
}
Ожидаемый результат
После BR нужны цифры
Неподходящий код — штатный ответ правила, не поломка regex. Сообщение не обещает, что товар найден в каталоге.
Дальше понадобятся массивы для нескольких совпадений, foreach для их обхода, function/return для повторного использования и callback-функции для вычисляемой замены. Текущие примеры вводят местный синтаксис до использования; ссылки помогают вернуться к языковой основе. Вложенные (...), условия рядом и рекурсия не нужны, чтобы выполнить первые два примера.
Как выбрать функцию и когда обойтись без regex
Выберите действие по результату, который действительно нужен. Буквальный поиск и разбиение проще без regex; регулярное правило окупается, когда форма меняется, есть поля или несколько видов разделителя.
| Нужно | Инструмент |
|---|---|
| Первое совпадение / проверить форму | preg_match |
| Все совпадения с полями | preg_match_all |
| Отобрать элементы массива без изменения | preg_grep |
| Разделить по правилу | preg_split |
| Вставить буквальные данные в pattern | preg_quote |
| Заменить фиксированным текстом/группами | preg_replace |
| Заменить и вернуть только затронутое | preg_filter |
| Вычислить замену | preg_replace_callback |
| Разным правилам разные вычисления | preg_replace_callback_array |
| Разобрать технический отказ | preg_last_error и preg_last_error_msg |
| Точная подстрока / разделитель / словарь | str_contains/strpos, explode, str_replace/strtr: полный раздел строковых функций |
| Структурированный формат | JSON → json_decode, CSV → str_getcsv/fgetcsv, HTML → DOM/HTML parser, URL → parse_url + проверка договора. Regex не заменяет весь парсер. |
Поиск идёт слева направо по subject, но внутри одной попытки возможен возврат — backtracking. Левейшее успешное совпадение не обязательно самое длинное. PHP сохраняет compiled patterns в cache до 4096 на thread, очищая старые неиспользуемые записи партиями; размер не настраивается. Это не cache результатов и не гарантия общей памяти между HTTP-запросами. Не собирайте бесконечно разные шаблоны, если можно вынести меняющиеся данные в обычный словарь.
Установка и версия: PHP, PCRE2 и JIT
pcre встроено в PHP и всегда доступно в стандартной поддерживаемой сборке. На Windows не нужно добавлять extension=pcre в php.ini. Сначала проверьте тот executable, который запускает задачу: PHP_VERSION и PCRE_VERSION — разные версии. Здесь новые run проверены на PHP8.5.11 / PCRE2 10.44; другой PHP может быть собран с другой библиотекой.
- PHP CLI запускает ваш файл.
- PCRE_VERSION сообщает именно библиотеку правил.
- PCRE_JIT_SUPPORT сообщает возможность JIT; ini_get — текущую настройку.
- Версии и настройки сравнивают до переноса нового синтаксиса.
<?php
echo 'PHP ', PHP_VERSION, "\n";
echo 'PCRE ', PCRE_VERSION, "\n";
echo 'JIT support: ', PCRE_JIT_SUPPORT ? 'yes' : 'no', "\n";
echo 'JIT setting: ', ini_get('pcre.jit'), "\n";
Это environment-dependent диагностика, mode=lint: числа зависят от вашей сборки, универсальный exact stdout не заявлен. Локальная проверенная среда указана отдельно. Тернарный оператор условие ? да : нет выбирает одну из двух надписей.
Обычная сборка использует bundled PCRE2 и по умолчанию поддерживает JIT. На не-Windows сборщик может выбрать --with-external-pcre (минимум10.30), а --without-pcre-jit отключает JIT при сборке. На Windows используется bundled library. Это сведения для администратора сборки, не инструкция менять ваш компьютер. Установка CLI и отличие конфигурации веб-SAPI — в отдельной главе.
| Версия PHP | Встроенная PCRE по истории PHP.net |
|---|---|
| 8.4.0 | 10.44 |
| 8.3.0 | 10.42 |
| 8.2.0 | 10.40 |
| 8.1.1 / 8.1.0 | 10.39 / 10.37 |
| 7.4.12 и 8.0.0 | 10.35 |
| 7.4.6 / 7.4.0 | 10.34 / 10.33 |
| 7.3.0 | 10.32, переход на PCRE2 |
| 7.2.0 / 7.0.3 / 7.0.0 | PCRE1 8.41 / 8.38 / 8.37; исторические неподдерживаемые PHP-ветви, не варианты для новой установки |
Таблица — история обновлений bundled library, не обещание полного соответствия любого дистрибутива. Старые выпуски PCRE1 в PHP.net связаны с CVE-исправлениями; перенос приложения должен включать поддерживаемую PHP-ветвь целиком, а не ручную замену одного DLL. PHP8.4 добавляет r и новые возможности bundled10.44; PHP8.2 — внешний n. Для external PCRE смотрите реальный PCRE_VERSION. Устанавливать ничего для примеров этой главы не нужно.
Три настройки и отдельные пределы компиляции
Есть три настройки PCRE, все INI_ALL: их можно менять разрешёнными механизмами соответствующей SAPI, включая ini_set. Но это три разных ограничения/переключателя, а не одна «максимальная длина строки». Текущее ini_get может отличаться от factory default. Для CLI и FPM проверяйте каждую конфигурацию отдельно.
| Директива / стандартное значение | Смысл и ограничения |
|---|---|
| pcre.backtrack_limit = 1000000 | Предел match-work; до PHP5.3.7 default100000. Не байты, не число найденных групп и не таймаут. Счётчик у interpreter/JIT устроен различно. |
| pcre.recursion_limit = 100000 | PHP передаёт depth limit PCRE2. В современном interpreter это глубина matching frames; JIT его не использует. Историческое предупреждение PHP.net о C-stack описывает прежнюю архитектуру, но расход памяти и другие стеки всё равно ограничены. |
| pcre.jit = 1 | Разрешает JIT при наличии поддержки и пригодного шаблона. Это не обещание, что каждый match действительно выполнится JIT. У него отдельный stack limit. |
- ini_set возвращает прежнее значение либо false.
- Временно записываем известный небольшой лимит.
- Выполняем локальную проверку.
- finally возвращает прежнее значение даже при исключении.
<?php
declare(strict_types=1);
$old = ini_set('pcre.backtrack_limit', '5000');
if ($old === false) { throw new RuntimeException('Setting is unavailable'); }
try { echo ini_get('pcre.backtrack_limit'), "\n"; }
finally { ini_set('pcre.backtrack_limit', $old); }
echo ini_get('pcre.backtrack_limit') === $old ? 'Восстановлено' : 'Не восстановлено';
echo "\n";
Ожидаемый результат
5000
Восстановлено
Exact stdout не зависит от исходного числа: мы временно задаём своё и проверяем восстановление, не меняя php.ini. Никакие настройки других процессов не затрагиваются.
Стартовые (*LIMIT_MATCH=n), (*LIMIT_DEPTH=n), (*LIMIT_HEAP=n) позволяют шаблону снизить соответствующие ограничения, не поднять лимиты вызывающей стороны. HEAP в KiB относится к interpreter, DEPTH не действует для JIT; исторический LIMIT_RECURSION остаётся alias DEPTH. PHP не предоставляет общую настройку таймаута regex. Не подставляйте отрицательные/огромные числа «для отключения защиты». Компиляция имеет собственные пределы: примерно64K code units compiled pattern при default2-byte linkage, до65535captures, default250 вложенных групп, variable lookbehind по умолчанию255. Эти величины могут зависеть от сборки, а исходная длина pattern не равна compiled size. Некоторые upstream ман-страницы от10.43 всё ещё пишут32 для имени capture, но tagged10.44 и PHP.net subpatterns фиксируют128; не переносите старую таблицу без версии.
Наблюдаемый контролируемый отказ при малом лимите — в разделе производительности. После отказа задача должна честно сообщить, что проверка не завершилась. Пытаться повторять всё с удвоенным лимитом по запросу клиента — плохая политика нагрузки.
Все константы: флаги, ошибки и версия
Константа — именованное значение PHP без знака $. Она объясняет намерение и избавляет от запоминания чисел. Есть флаги, коды ошибок и сведения о сборке: их нельзя смешивать, даже если внутреннее число случайно совпало. Оператор | объединяет совместимые биты флагов; это не логическое ||.
| Константа / тип | Назначение |
|---|---|
| PREG_PATTERN_ORDER | int: preg_match_all по группам; режим по умолчанию. |
| PREG_SET_ORDER | int: preg_match_all по совпадениям; не объединять с предыдущим. |
| PREG_OFFSET_CAPTURE | int: элементы captures становятся [текст, байтовая позиция]. |
| PREG_UNMATCHED_AS_NULL | int, PHP7.2+: отсутствовавшие captures → null; конечные у preg_match с PHP7.4; в паре с OFFSET_CAPTURE → [null,-1]. |
| PREG_SPLIT_NO_EMPTY | int: убрать пустые части, включая крайние. |
| PREG_SPLIT_DELIM_CAPTURE | int: вернуть захватывающие группы разделителя. |
| PREG_SPLIT_OFFSET_CAPTURE | int: пары [часть, byteOffset] именно у split; не подменять PREG_OFFSET_CAPTURE. |
| PREG_GREP_INVERT | int: отобрать не совпавшие значения массива. |
| PREG_NO_ERROR | int, PHP5.2+: технической ошибки нет, даже если совпадений 0. |
| PREG_INTERNAL_ERROR | int, PHP5.2+: общий внутренний/compile отказ; точный compile-текст дополнительно приходит warning. |
| PREG_BACKTRACK_LIMIT_ERROR | int, PHP5.2+: исчерпан match-work limit. |
| PREG_RECURSION_LIMIT_ERROR | int, PHP5.2+: исчерпан depth limit; имя историческое. |
| PREG_BAD_UTF8_ERROR | int, PHP5.2+: недопустимые UTF-8 байты subject. |
| PREG_BAD_UTF8_OFFSET_ERROR | int, PHP5.3+: старт внутри UTF-8 code point. |
| PREG_JIT_STACKLIMIT_ERROR | int, PHP7.0+: недостаточен отдельный стек JIT. |
| PCRE_VERSION | string, PHP5.2.4+: версия и дата библиотеки, не PHP. |
| PCRE_VERSION_MAJOR / PCRE_VERSION_MINOR | int: две части версии PCRE; удобны для диагностической карты. |
| PCRE_JIT_SUPPORT | bool: сборка поддерживает JIT; не доказательство его применения к каждому правилу. |
- BR41 найден в строке x BR41.
- Без флага элемент [0] — "BR41".
- С OFFSET_CAPTURE элемент [0] — ["BR41",2].
- Код ошибок PREG_NO_ERROR читают отдельно после операции.
<?php
declare(strict_types=1);
preg_match('~BR[0-9]+~', 'x BR41', $plain);
preg_match('~BR[0-9]+~', 'x BR41', $position, PREG_OFFSET_CAPTURE);
echo $plain[0], "\n";
echo $position[0][0], '@', $position[0][1], "\n";
echo preg_last_error() === PREG_NO_ERROR ? 'Без ошибки' : 'Ошибка';
echo "\n";
Ожидаемый результат
BR41
BR41@2
Без ошибки
Номер byteOffset2 не является индексом группы: у этих двух чисел разные роли. Отдельная константа PREG_SPLIT_OFFSET_CAPTURE нужна именно preg_split.
Развёрнутые применения flags есть в карточках preg_match_all, preg_split и preg_grep. PREG_PATTERN_ORDER и PREG_SET_ORDER выбирают альтернативные формы, их не нужно объединять. Набор поддерживаемых flags зависит от функции: недопустимая комбинация в современном PHP может вызвать ValueError, поэтому не передавайте «все флаги сразу».
Четыре разных ситуации: найдено, не найдено, правило сломано, ввод повреждён
Перед сохранением кода заказа важно понимать, что проверка действительно состоялась. 0 — нормальное отсутствие совпадения; false/null зависят от выбранной функции и требуют диагностики. Неправильный regex вызывает warning при компиляции; неверный тип аргумента может бросить исключение PHP ещё до поиска. Ошибка runtime (UTF-8, лимит) обычно отражается return и последним PCRE-кодом.
- Заведомо повреждённый ~[~ не компилируется.
- Локальный обработчик записывает сам факт E_WARNING.
- preg_match возвращает false; обычной проверки текста не произошло.
<?php
declare(strict_types=1);
$warnings = 0;
set_error_handler(function (int $level) use (&$warnings): bool {
if ($level === E_WARNING) { ++$warnings; return true; }
return false;
});
try { $result = preg_match('~[~', 'BR41'); }
finally { restore_error_handler(); }
echo $result === false ? 'Правило не скомпилировано' : 'Готово';
echo '/', $warnings, "\n";
Ожидаемый результат
Правило не скомпилировано/1
Обработчик здесь нужен только для намеренно плохого учебного шаблона. Он возвращает true для обработанного warning, затем обязательно снимается. В рабочем коде исправьте собственный pattern; не прячьте ошибки через @.
<?php
declare(strict_types=1);
$result = preg_replace('~x~u', 'y', "\xFF");
$error = preg_last_error();
echo $result === null ? 'Нет результата' : 'Результат есть';
echo '/', $error === PREG_BAD_UTF8_ERROR ? 'bad-utf8' : 'другое', "\n";
Ожидаемый результат
Нет результата/bad-utf8
Технический отказ не означает, что исходный текст уже чистый. Не заменяйте null на пустую строку молча: иначе данные потеряются.
Сохраняйте preg_last_error и preg_last_error_msg немедленно: последующий regex, включая логгер или валидацию сообщения, может сменить состояние. Параметры с & — выходные переменные; не передавайте туда выражение. Неверные flags/типы относятся к PHP API и не обязаны менять last_error. Исключение callback также остаётся исключением. У preg_filter null может быть штатным отсутствием замен, поэтому смотрите его отдельный договор. Для журналов достаточно имени правила, кода, размера входа и обезличенного контекста; полный пользовательский текст часто избыточен.
Все модификаторы и их версии
Буквы после закрывающего разделителя выбирают режим всего шаблона. Они не передаются вместо PHP-flags аргумента. Например i меняет сравнение, а PREG_OFFSET_CAPTURE меняет структуру возвращённых данных. В модификаторах пробел/LF могут игнорироваться, но неизвестная буква даёт warning; писать лишнее оформление не стоит.
| Модификатор | Действие и версия |
|---|---|
| i | Caseless; Unicode /u может сопоставлять разные code points с общим регистром. |
| m | ^/$ начинают/заканчивают строки внутри subject; не делает точку многострочной. |
| s | Точка включает newline; не влияет на отрицательный класс, уже допускающий его. |
| x | Пробелы оформления и #комментарии вне классов игнорируются; экранируйте буквальные пробел/#. |
| A | Попытка только на стартовой позиции offset; это не то же, что абсолютный \A. |
| D | Заставляет $ означать абсолютный конец; игнорируется с m, не меняет \Z. |
| S | Исторический study hint; с PHP7.3 не имеет эффекта. |
| U | Переворачивает жадность: обычный repeat становится lazy, с ? — greedy. |
| X | Исторический EXTRA; в PHP8.5.11 внешний X no-op, неизвестные escapes ошибочны и без него. Внутренний (?X) запрещён с PHP7.3. |
| J | Разрешить одинаковые имена captures; внешний модификатор с PHP7.2, внутренний (?J) существовал раньше. |
| u | UTF-8 плюс UCP в PHP: pattern и subject должны быть корректными; offsets остаются байтовыми. |
| n | PHP8.2+: обычные (...) не захватывают, именованные захватывают и получают номера. |
| r | PHP8.4+: при iu исключает cross-ASCII/non-ASCII caseless, например K и Kelvin K. |
- Unicode Kelvin K родственен латинской K при case folding.
- /iu допускает совпадение K с K.
- /iur запрещает переход между ASCII/non-ASCII, результат0.
<?php
declare(strict_types=1);
echo preg_match('~\AK\z~iu', 'K'), '/', preg_match('~\AK\z~iur', 'K'), "\n";
echo preg_match('~\AГ\z~iur', 'г'), "\n";
Ожидаемый результат
1/0
1
r PHP8.4+ не отключает весь Unicode-регистр: Г/г остаются парой. Для ASCII идентификатора явный ASCII-алфавит без /i часто проще.
<?php
declare(strict_types=1);
preg_match('~(?<code>BR)|(?<code>NT)~J', 'NT', $m);
echo $m['code'], "\n";
echo preg_match('~\ABR41\z~SX', 'BR41'), "\n";
Ожидаемый результат
NT
1
Официальная историческая страница examples называет /J ошибкой; с PHP7.2 это неверно. S/X в этой проверенной версии ничего не меняют — не используйте их как «ускорение» или дополнительную защиту.
Сочетания n/J и группа (?i:...) показаны в разделе групп и локальных настроек. При внешнем PCRE версия PHP сама по себе не гарантирует всякую новую возможность библиотеки. /u — не HTML-sanitizer, /i — не нормализация Unicode, /s — не разрешение опасного регулярного выражения.
Как проверять собственный шаблон на небольших примерах
До применения к файлу импорта составьте маленькую таблицу «ввод → ожидаемое решение». Включите обычный код, пустой ввод, лишний хвост, перевод строки и похожую букву другого алфавита. Это проверяет договор, а не только то, что один удачный пример когда-то прошёл.
- BR41 ожидается1, BR? ожидается0.
- Цикл применяет одно правило к каждому случаю.
- При несовпадении с ожиданием пример сообщает конкретный статус.
<?php
declare(strict_types=1);
$cases = [['BR41', 1], ['BR?', 0], ['', 0], ["BR41\n", 0], ['ВR41', 0]];
foreach ($cases as [$input, $expected]) {
$actual = preg_match('~\ABR[0-9]+\z~', $input);
echo $actual === $expected ? 'PASS' : 'FAIL';
echo "\n";
}
Ожидаемый результат
PASS
PASS
PASS
PASS
PASS
В последнем вводе первая буква кириллическая В. Деструктуризация [$input,$expected] читает два элемента каждой пары; === обнаружит и false вместо ожидаемого0.
<?php
declare(strict_types=1);
function codeForLabel(string $input): ?string
{
$status = preg_match('~\A(BR|NT)([0-9]{1,4})\z~', $input, $m);
if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
return $status === 1 ? $m[1] . ' / ' . $m[2] : null;
}
echo codeForLabel('BR41') ?? 'Проверьте код', "\n";
echo codeForLabel('XX41') ?? 'Проверьте код', "\n";
Ожидаемый результат
BR / 41
Проверьте код
Функция связывает regex с полезным результатом — меткой для печати. На уровне UI остаётся экранирование HTML; здесь формируется обычный текст, не веб-страница.
Старая страница PCRE examples полезна как набор форм разделителей, но её вывод о J нужно читать с версией. Отсутствующий разделитель/закрытие и неизвестный модификатор остаются ошибками; пример с /J сегодня допустим. Исходный шаблон, ожидание и результат храните вместе, чтобы изменение требования не свелось к случайному добавлению ?.
Две оболочки: строка PHP и разделители шаблона
Нужно найти путь /catalog/bolts. Кавычки сначала создают строку PHP, затем preg-функция читает внутри неё разделитель ~ и тело правила. Разделитель — один неалфавитно-цифровой байт, не пробел, не обратный слеш и не NUL. Обычно это ~, # или /. Начальные пробелы перед корректным разделителем игнорируются, но яснее их не писать. UTF-8 знак § не подходит: он занимает несколько байтов.
- PHP читает строку "~ /catalog/... ~" без показанных пояснительных пробелов.
- Тильда ограничивает шаблон; слеши пути теперь обычные символы.
- После конечной тильды i разрешает различие регистра.
<?php
declare(strict_types=1);
echo preg_match('~/catalog/bolts~i', 'Go to /CATALOG/BOLTS'), "\n";
echo preg_match('/\/catalog\/bolts/i', 'Go to /CATALOG/BOLTS'), "\n";
Ожидаемый результат
1
1
Оба правила ищут одинаковый фрагмент. В варианте с / каждый внутренний слеш экранирован, чтобы PHP не принял его за конец шаблона.
<?php
declare(strict_types=1);
$product = 'bolt~large';
$pattern = '~\A' . preg_quote($product, '~') . '\z~';
echo preg_match($pattern, 'bolt~large'), "\n";
echo preg_match('{\ABR[0-9]+\z}', 'BR41'), "\n";
Ожидаемый результат
1
1
В первом случае preg_quote знает выбранную тильду. Второй показывает парные разделители { и }; сами фигурные скобки-оболочки не означают повторение.
Допускаются парные (), {}, [], <>; PHP должен корректно найти закрывающий разделитель, а буквальные скобки внутри требуют осторожного экранирования. Для начала выбирайте ~. Даже внутри [...] и \Q...\E разделитель всё ещё обрабатывается PHP. Разделитель отсутствует, не закрыт или после него неизвестная буква — E_WARNING, а не нормальный поиск. Для буквального пользовательского фрагмента используйте preg_quote.
Метасимволы: выбрать один знак или последовательность
Формат артикула BR41 можно описать без перечисления каждого возможного номера. Метасимвол — знак, который задаёт действие в языке шаблона. Остальные символы сравниваются буквально. Положение важно: . вне класса означает любой допустимый знак, а [.] — именно точку. Класс [...] выбирает один знак, группа (...) объединяет целую последовательность.
| Запись | Как читать |
|---|---|
| \ | Escape: буквальный метасимвол или специальная команда. |
| ^ / $ | Позиция начала/конца; особенности перевода строки — в разделе якорей. |
| . | Один знак, обычно кроме перевода строки. |
| [...] | Один знак из множества; [^...] — вне множества. |
| | | Альтернативная ветвь. |
| (...) | Группа, обычно с сохранением текста. |
| * / + / ? | 0+, 1+, 0–1 повторений предыдущего элемента. |
| {n} / {n,m} | Точное число либо диапазон повторений. |
- [A-Z] выбирает одну заглавную ASCII-букву.
- {2} повторяет этот выбор дважды → BR.
- [0-9]+ выбирает непустую последовательность цифр → 41.
<?php
declare(strict_types=1);
echo preg_match('~\A[A-Z]{2}[0-9]+\z~', 'BR41'), "\n";
echo preg_match('~\A[A-Z]{2}[0-9]+\z~', 'B41'), "\n";
Ожидаемый результат
1
0
Две буквы — условие продукта; одна буква недостаточна. Результат 0 означает только несоответствие этому договору.
<?php
declare(strict_types=1);
echo preg_match('~\Av1.2\z~', 'v1x2'), "\n";
echo preg_match('~\Av1[.]2\z~', 'v1x2'), "\n";
echo preg_match('~\Av1\.2\z~', 'v1.2'), "\n";
Ожидаемый результат
1
0
1
Первый шаблон намеренно слишком широк. Два способа буквальной точки — [.] и \. — устраняют эту ошибку.
Внутри класса метасмысл имеют \, начальный ^, дефис диапазона -, закрывающий ], а [ участвует в специальных POSIX-записях. Все остальные знаки обычно буквальны; PHP-разделитель всё равно нужно экранировать. Вне класса ] и } без парного начала могут быть литералами, но намеренно экранированная пунктуация понятнее. Внешний ? также начинает специальные конструкции после (: (?:...), (?=...) и другие разобраны дальше.
Обратный слеш: PHP сначала, regex потом
Имя папки может содержать \, а строка отчёта — табуляцию. Слеш бывает командой PHP-строки и командой PCRE: эти два уровня нельзя смешивать. В одинарной PHP-строке специально обрабатываются \\ и \'; прочие сочетания вроде \d проходят к regex. Чтобы PCRE получил \\ для буквального слеша, в PHP-коде нужно написать четыре слеша. Nowdoc сохраняет текст без PHP-интерполяции и удобен для длинного правила.
| Запись | Как читать |
|---|---|
| \a / \e / \f / \n / \r / \t | BEL 07, ESC 1B, formfeed 0C, LF 0A, CR 0D, TAB 09. |
| \cx | ASCII control: например \cJ — LF; не произвольная Unicode-буква. |
| \xhh / \x{hhhh} / \N{U+hhhh} | Hex-код: первые два hex digits или явная code point; последний вариант требует UTF. |
| \0dd / \o{ddd} | Octal-код. Начальный 0 снимает смысл back reference; фигурная форма удобнее для длинного числа. |
| \d/\D, \w/\W, \s/\S | Цифра/не цифра, word/не word, whitespace/не whitespace; /u меняет множества. |
| \h/\H, \v/\V | Горизонтальный/вертикальный пробел и дополнение; не зависят от выбора newline для точки. |
| \R / \N | Последовательность перевода строки / один знак кроме newline; \R воспринимает CRLF целиком. |
| \b/\B, \A/\Z/\z/\G | Условия на позицию без потребления текста; не использовать внутри класса. |
| \p{...}/\P{...}/\X | Unicode-свойство, его отрицание, графемный кластер. |
- PHP-литерал содержит четыре слеша между save и report.
- PCRE получает два: команду «один буквальный слеш».
- В тексте save\report найден один реальный слеш.
<?php
declare(strict_types=1);
$path = 'save\report';
echo preg_match('~\Asave\\\\report\z~', $path), "\n";
echo bin2hex($path), "\n";
Ожидаемый результат
1
736176655c7265706f7274
Hex 5c — единственный байт обратного слеша. Он не является разделителем файла на диске: пример проверяет только текст.
<?php
declare(strict_types=1);
$line = "BR41\t12\n";
echo preg_replace('~\t~', ' | ', $line);
echo preg_match('~\x{20AC}~u', 'Цена €'), "\n";
Ожидаемый результат
BR41 | 12
1
В двойной PHP-строке \t создаёт табуляцию; в одинарном шаблоне \t прочитает PCRE. \x{20AC} — euro code point, а /u выбирает UTF-8.
<?php
declare(strict_types=1);
preg_match('~\Q$1.00\E~', 'Price $1.00', $price);
echo $price[0], "\n";
preg_match('~SKU:\K[A-Z]{2}[0-9]+~', 'SKU:BR41', $code, PREG_OFFSET_CAPTURE);
echo $code[0][0], '@', $code[0][1], "\n";
Ожидаемый результат
$1.00
BR41@4
\Q...\E делает метасимволы буквальными, но не защищает PHP-разделитель и встроенную \E из внешнего ввода. \K проверяет SKU:, затем сбрасывает начало сообщаемого совпадения; уже захваченные группы сохраняются.
Неизвестный escape вроде \q — ошибка в PCRE2. В числовых escapes старые короткие записи неоднозначны: \11 может быть группой 11 либо octal 11. Для ясности выбирайте \g{1} и \o{11}/\x09. В классе \b — backspace 08, снаружи — граница слова. Старые тексты про максимум 99 back references не описывают PCRE2: лимит 99 относится к числовым ссылкам replacement в preg_replace. \K внутри lookaround по умолчанию запрещён с PCRE2 10.38; не переносите туда этот приём. PHP поддерживает NUL в строках; историческое утверждение о NUL как обязательном конце шаблона к современному PHP не относится.
Байты, Unicode properties и видимые знаки
В слове «гайка» буквы должны проверяться как UTF-8, а цену € нельзя измерять одним байтом. В PHP модификатор /u включает UTF и Unicode properties (UCP). Поэтому \d означает Unicode-десятичную цифру, \s Unicode-пробелы, \w включает буквы/числа/соединители и с PCRE2 10.43 также Mn. Для протокола с ASCII-цифрами пишите [0-9]. Без /u строка разбирается побайтно, таблицы некоторых классов могут зависеть от locale. Отдельный (*UTF) в PCRE2 сам по себе не включает UCP; не приравнивайте его к PHP /u.
- "гайка 12" содержит кириллицу и fullwidth digits.
- /u + \p{L}+ находит слово как буквы.
- \d принимает 12; [0-9] их отклоняет.
<?php
declare(strict_types=1);
preg_match('~\p{L}+~u', 'гайка 12', $m);
echo $m[0], "\n";
echo preg_match('~\A\d+\z~u', '12'), '/', preg_match('~\A[0-9]+\z~', '12'), "\n";
Ожидаемый результат
гайка
1/0
Код товара может требовать ASCII, хотя название допускает много языков. /u не означает «разрешить любой символ».
<?php
declare(strict_types=1);
$text = "e\u{0301}Я";
preg_match_all('~. ~ux', $text, $points);
preg_match_all('~\X~u', $text, $clusters);
echo strlen($text), '/', count($points[0]), '/', count($clusters[0]), "\n";
echo bin2hex($clusters[0][0]), "\n";
Ожидаемый результат
5/3/2
65cc81
x игнорирует пробел в учебном правиле, поэтому . считает code points. \X сохраняет e с акцентом одним кластером. Графема — модель текстового сегмента, не гарантия одного пиксельного глифа на любом шрифте.
<?php
declare(strict_types=1);
echo preg_match('~\A\p{Cyrillic}+\z~u', 'Гайка'), "\n";
echo preg_match('~\A\p{Lu}\z~iu', 'г'), "\n";
echo preg_match('~\A\p{Lu}\z~iu', 'Г'), "\n";
Ожидаемый результат
1
0
1
Свойство Lu означает uppercase даже при /i; caseless не превращает его в произвольную букву. Cyrillic — письменность, а не доказательство русского языка или отсутствия визуальных подмен.
Каждая code point имеет general category. Однобуквенная L объединяет Ll/Lm/Lo/Lt/Lu; \pL сокращает \p{L}. \P{Lu} и \p{^Lu} — отрицание. Unicode blocks вроде InMusicalSymbols и произвольные Perl properties не поддерживаются. PCRE2 также понимает script/sc/scx, некоторые binary properties и Bidi_Class: сверяйте установленную библиотеку. Cs описывает суррогаты, но допустимый UTF-8 их не содержит. Ни /u, ни \X не нормализуют NFC/NFD: визуально похожие строки могут иметь разные байты. Для нормализации нужен отдельный осознанный шаг intl. Повреждённый UTF-8 приводит к false/PREG_BAD_UTF8_ERROR, а не штатному 0.
| General category | Смысл |
|---|---|
| C | прочее |
| Cc | управляющий |
| Cf | форматирующий |
| Cn | не назначен |
| Co | частное использование |
| Cs | суррогат |
| L | буква |
| Ll | строчная |
| Lm | модификатор-буква |
| Lo | другая буква |
| Lt | титульная |
| Lu | прописная |
| M | комбинируемая метка |
| Mc | занимающая место |
| Me | окружающая |
| Mn | не занимающая место |
| N | числовой знак |
| Nd | десятичная цифра |
| Nl | буквенное число |
| No | другой числовой знак |
| P | пунктуация |
| Pc | соединитель |
| Pd | тире |
| Pe | закрывающая |
| Pf | конечная кавычка |
| Pi | начальная кавычка |
| Po | прочая |
| Ps | открывающая |
| S | символ |
| Sc | валюта |
| Sk | модификатор |
| Sm | математический |
| So | другой, включая многие emoji |
| Z | разделитель |
| Zl | строк |
| Zp | абзацев |
| Zs | пробел |
Имена письменностей, перечисленные на официальной PHP-странице (не обещание, что это весь более новый Unicode-набор библиотеки): Arabic, Armenian, Avestan, Balinese, Bamum, Batak, Bengali, Bopomofo, Brahmi, Braille, Buginese, Buhid, Canadian_Aboriginal, Carian, Chakma, Cham, Cherokee, Common, Coptic, Cuneiform, Cypriot, Cyrillic, Deseret, Devanagari, Egyptian_Hieroglyphs, Ethiopic, Georgian, Glagolitic, Gothic, Greek, Gujarati, Gurmukhi, Han, Hangul, Hanunoo, Hebrew, Hiragana, Imperial_Aramaic, Inherited, Inscriptional_Pahlavi, Inscriptional_Parthian, Javanese, Kaithi, Kannada, Katakana, Kayah_Li, Kharoshthi, Khmer, Lao, Latin, Lepcha, Limbu, Linear_B, Lisu, Lycian, Lydian, Malayalam, Mandaic, Meetei_Mayek, Meroitic_Cursive, Meroitic_Hieroglyphs, Miao, Mongolian, Myanmar, New_Tai_Lue, Nko, Ogham, Old_Italic, Old_Persian, Old_South_Arabian, Old_Turkic, Ol_Chiki, Oriya, Osmanya, Phags_Pa, Phoenician, Rejang, Runic, Samaritan, Saurashtra, Sharada, Shavian, Sinhala, Sora_Sompeng, Sundanese, Syloti_Nagri, Syriac, Tagalog, Tagbanwa, Tai_Le, Tai_Tham, Tai_Viet, Takri, Tamil, Telugu, Thaana, Thai, Tibetan, Tifinagh, Ugaritic, Vai, Yi. Common объединяет общие знаки, Inherited — наследующие письменность контекста. Пробел/дефис в имени человека обычно нужно разрешить отдельно; список письменностей не является готовой политикой имени. До PCRE8.32 (bundled до PHP5.4.14) \X имел более узкую модель base+marks; современная версия использует extended grapheme cluster. Unicode-таблицы и их стоимость зависят от версии, поэтому не делайте универсальных заявлений «Unicode медленный».
Проверить всю строку, строку отчёта или точный offset
Код заказа не должен принимать скрытый перевод строки на конце. ^ и $ удобны для строк отчёта, но $ по умолчанию разрешает позицию перед последним newline. Для строгого поля используйте \A...\z. Все якоря проверяют позицию: сами символы не поглощают.
- Ввод "BR41\n" содержит завершающий LF.
- ^...$ допускает позицию перед LF.
- \A...\z требует дойти до абсолютного конца → 0.
<?php
declare(strict_types=1);
$code = "BR41\n";
echo preg_match('~^BR[0-9]+$~', $code), '/', preg_match('~\ABR[0-9]+\z~', $code), "\n";
echo preg_match('~^BR[0-9]+$~D', $code), "\n";
Ожидаемый результат
1/0
0
Модификатор D делает $ end-only без /m. \z выразительнее, когда нужен именно полный договор поля.
<?php
declare(strict_types=1);
$report = "NT8\nBR41\nNT9";
preg_match_all('~^BR[0-9]+$~m', $report, $m);
echo implode(',', $m[0]), "\n";
echo preg_match('~\ABR[0-9]+\z~m', $report), "\n";
Ожидаемый результат
BR41
0
/m меняет ^/$, но не \A/\z. /s имеет другую роль: он меняет точку, не якоря.
<?php
declare(strict_types=1);
$text = 'x BR41';
echo preg_match('~^BR~', $text, $m, 0, 2), '/', preg_match('~\GBR~', $text, $m, 0, 2), "\n";
echo preg_match('~BR~A', $text, $m, 0, 2), "\n";
echo preg_match('~(?<=x )BR~', $text, $m, 0, 2), "\n";
Ожидаемый результат
0/1
1
1
\G и A привязаны к старту текущего поиска; ^ без /m — к исходному началу. Lookbehind видит "x " до offset, чего не было бы после substr.
\Z разрешает конец или позицию перед финальным newline независимо от /m; \z — только конец. \b — граница между word/non-word либо соответствующий край строки, \B — её отсутствие. Это не универсальная граница языкового слова: дефис/апостроф и Unicode-модель требуют выбора. У всех альтернатив должно быть нужное ограничение: \A(?:BR|NT)\z, а не \ABR|NT\z. Выбор CR/LF/CRLF разбирается вместе с точкой.
Точка, переводы строк и запрет разрывать UTF-8
Поле заметки может занимать две строки. . без /s пропускает один символ, кроме выбранного newline; /s позволяет ему пройти перевод строки. Это не меняет ^/$ и не влияет на отрицательные классы: [^;] включает newline. В /u точка потребляет одну code point, а не графему.
- A, затем LF, затем B.
- A.B не проходит LF без /s.
- A.B с /s покрывает три code points.
<?php
declare(strict_types=1);
$note = "A\nB";
echo preg_match('~\AA.B\z~', $note), '/', preg_match('~\AA.B\z~s', $note), "\n";
echo preg_match('~\AA[^;]B\z~', $note), "\n";
Ожидаемый результат
0/1
1
[^;] означает любой знак кроме ;, поэтому newline допустим и без /s. Если newline запрещён, исключите его явно.
<?php
declare(strict_types=1);
$report = "BR41\r\nNT8\n";
$rows = preg_split('~\R~u', $report, -1, PREG_SPLIT_NO_EMPTY);
echo implode('|', $rows), "\n";
echo preg_match('~(*CRLF)^NT8$~m', $report), "\n";
Ожидаемый результат
BR41|NT8
0
\R потребляет CRLF целиком и также LF. Во второй проверке режим CRLF меняет newline-якоря: одинокий последний LF уже не считается разрешённым финальным newline.
В начале тела шаблона (*CR), (*LF), (*CRLF), (*ANYCRLF), (*ANY), (*NUL) задают, что считают newline якоря и точка; если несколько — последнее выигрывает. Это отдельно от \R: (*BSR_ANYCRLF) ограничивает его CR/LF/CRLF, (*BSR_UNICODE) допускает Unicode-переводы. \N всегда исключает newline, даже при /s. Старое описание предлагает \C для одного байта внутри UTF-8; это может разорвать кодовую точку. PHP8.5.11 при /u явно запрещает \C (compile warning). Для бинарных байтов используйте byte-mode осознанно, для текста . или \X.
Класс символов и POSIX-наборы
Поле SKU допускает только большие латинские буквы, цифры и дефис. [...] описывает один разрешённый знак; + после класса — несколько. Начальный ^ инвертирует набор, но всё равно потребляет один знак, поэтому [^x] не совпадает с пустой строкой. Диапазоны — диапазоны кодов, не алфавитная сортировка.
| Запись | Как читать |
|---|---|
| [:alnum:] / [:alpha:] | Буквы и цифры / буквы. В классе пишут [[:alpha:]]. |
| [:ascii:] / [:cntrl:] | ASCII-коды 0…127 / управляющие знаки. |
| [:blank:] / [:space:] | Горизонтальный пробел / пробельные; UCP может расширять набор. |
| [:digit:] / [:xdigit:] | Десятичные / шестнадцатеричные цифры; для строгого ASCII всё же явный [0-9A-Fa-f]. |
| [:graph:] / [:print:] | Печатные без пробелов / печатные с пробелами. |
| [:lower:] / [:upper:] | Строчные / прописные; /i влияет на matching. |
| [:punct:] / [:word:] | Пунктуация / word-набор, историческое Perl-расширение. |
- [A-Z0-9-] разрешает B,R,4,1 и дефис.
- + проверяет каждый знак BR-41.
- \A/\z не позволяют оставить запрещённый хвост за совпадением.
<?php
declare(strict_types=1);
echo preg_match('~\A[A-Z0-9-]+\z~', 'BR-41'), "\n";
echo preg_match('~\A[A-Z0-9-]+\z~', 'BR_41'), "\n";
Ожидаемый результат
1
0
Дефис в конце класса буквальный. Подчёркивание не разрешено бизнес-правилом, хотя входит в \w.
<?php
declare(strict_types=1);
echo preg_match('~\A[[:xdigit:]]{6}\z~', 'A0ff19'), "\n";
echo preg_match('~\s~', "\x0B"), '/', preg_match('~[[:space:]]~', "\x0B"), "\n";
echo preg_match('~\A[[:alpha:]]+\z~u', 'Гайка'), "\n";
Ожидаемый результат
1
1/1
1
Современная PCRE2 включает VT в \s; старое различие PHP-страницы между \s и [:space:] историческое. /u меняет многие POSIX-наборы на Unicode properties.
Буквальный ] ставят первым элементом либо экранируют; буквальный ^ — не первым либо \^; - — первым/последним либо \-. Диапазон Z-a захватывает пунктуацию между кодами: не заменяет [A-Za-z]. \d и \p{L} можно включать в набор, но свойство не может быть концом диапазона. [^\W_] отбирает word кроме underscore; Unicode-границы всё равно определяет /u. [:^digit:] инвертирует вложенный POSIX-набор, например [[:^digit:]]; голое [:digit:] — не правильная оболочка POSIX. /i учитывает регистр и в положительных/отрицательных классах; /m и /s не меняют принадлежность newline классу.
Альтернативы: порядок и область якорей
Доставка бывает pickup или courier. Вертикальная черта разделяет ветви, а (?:...) объединяет выбор, не создавая отдельного сохранённого поля. Движок пробует слева направо; выбирает первую ветвь, которая позволяет завершить весь шаблон. Пустая ветвь допустима и совпадает с пустым текстом.
- (?:pickup|courier) предлагает два конкретных значения.
- \A и \z ограничивают весь выбор.
- courier совпадает, fast-courier не проходит.
<?php
declare(strict_types=1);
$pattern = '~\A(?:pickup|courier)\z~';
echo preg_match($pattern, 'courier'), '/', preg_match($pattern, 'fast-courier'), "\n";
Ожидаемый результат
1/0
Для простого перечня in_array(..., true) или enum может быть понятнее; regex полезен, когда ветви имеют разную структуру.
<?php
declare(strict_types=1);
$pattern = '~\A(?:INV-[0-9]+(*MARK:invoice)|RET-[0-9]+(*:return))\z~';
preg_match($pattern, 'RET-12', $m);
echo $m[0], ':', $m['MARK'], "\n";
preg_match('~BR|BR41~', 'BR41', $short);
echo $short[0], "\n";
Ожидаемый результат
RET-12:return
BR
(*MARK:name) и (*:name) записывают последнюю достигнутую метку успешного пути в MARK. Во втором поиске короткая левая ветвь уже достаточна: longest match здесь не гарантируется.
Если продолжение после группы не проходит, движок может вернуться к другой ветви. Метки не обязаны быть уникальными; выбирайте их для понятного отчёта, а не как замену бизнес-проверкам. Для общей части выносите её за группу: BR(?:41|42). Подробнее о запрете возврата — atomic groups.
Настройки внутри шаблона: локальный регистр
Префикс документа может быть нечувствителен к регистру, а секретная часть — чувствительна. (?i:...) включает i только внутри группы; после закрытия скобок прежняя настройка восстанавливается. (?i) без двоеточия меняет оставшуюся часть текущего уровня, а не только следующий символ.
- (?i:sku) принимает SKU или sku.
- После группы двоеточие и BR41 снова case-sensitive.
- SKU:BR41 проходит; SKU:br41 нет.
<?php
declare(strict_types=1);
$pattern = '~\A(?i:sku):BR41\z~';
echo preg_match($pattern, 'SKU:BR41'), '/', preg_match($pattern, 'SKU:br41'), "\n";
Ожидаемый результат
1/0
Двоеточие внутри (?i:...) — часть синтаксиса группы; двоеточие после неё — данные формата.
<?php
declare(strict_types=1);
echo preg_match('~\A(a(?i)b|c)\z~', 'C'), "\n";
echo preg_match('~\A(?i:tag)(?-i:Q)\z~', 'TAGq'), "\n";
Ожидаемый результат
1
0
i, включённая в первой ветви, влияет на последующие ветви того же уровня уже при компиляции. (?-i:Q) явно выключает i на свою группу.
Доступны i,m,s,x,U,J; n — no-auto-capture (PHP8.2+), r — caseless restrict (PHP8.4+ при подходящей PCRE). (?im-sx) включает im и выключает sx; яснее не повторять одну букву с обеих сторон. (?xx) в PCRE2 дополнительно игнорирует пробел/TAB внутри классов; обычный x этого не делает. (*UTF)/(*UCP) — отдельные глобальные стартовые настройки, не обычные буквы. Старый (?X) больше не поддерживается с PHP7.3; внешний X в PHP8.5.11 принимается как no-op. Все внешние модификаторы и версии.
Группы: сохранить поле, сгруппировать, назвать
Из BR41=12 нужно получить код и количество, а не только ответ да/нет. Обычные (...) сохраняют совпавший текст. Открывающие скобки нумеруются слева направо с 1, [0] всегда всё совпадение. (?:...) только объединяет выражение. (?<sku>...) одновременно даёт понятное имя и обычный номер.
- Вход BR41=12.
- Группа sku захватывает BR41, qty захватывает 12.
- Можно прочитать $m["sku"] и $m["qty"], не запоминая номера.
<?php
declare(strict_types=1);
preg_match('~\A(?<sku>[A-Z]{2}[0-9]+)=(?<qty>[0-9]+)\z~', 'BR41=12', $m);
echo $m['sku'], '/', $m['qty'], '/', $m[1], "\n";
Ожидаемый результат
BR41/12/BR41
Количество ещё строка. Здесь нет проверки существования кода на складе и числового диапазона.
<?php
declare(strict_types=1);
$pattern = '~\A(?|SKU:([A-Z]{2}[0-9]+)|([A-Z]{2}[0-9]+):SKU)\z~';
foreach (['SKU:BR41', 'NT8:SKU'] as $line) {
preg_match($pattern, $line, $m);
echo $m[1], "\n";
}
Ожидаемый результат
BR41
NT8
(?|...) сбрасывает номера групп на начало каждой ветви. Без | сразу после ? поле второй ветви имело бы другой номер.
<?php
declare(strict_types=1);
$pattern = '~\A(?:(?<sku>BR[0-9]+)|(?<sku>NT[0-9]+))\z~J';
preg_match($pattern, 'NT8', $m);
echo $m['sku'], "\n";
preg_match('~(BR)(?<number>[0-9]+)~n', 'BR41', $named);
echo $named[1], '/', $named['number'], "\n";
Ожидаемый результат
NT8
41/41
J разрешает дублирующиеся имена, а n делает обычные группы незахватывающими. Имя number всё равно имеет номер 1; n не выключает вообще все числовые результаты.
Другие формы именования — (?P<name>...) и (?'name'...). Для переносимости выбирайте ASCII-имя из букв/цифр/underscore, не начинайте с цифры. В PCRE2 10.44, bundled PHP8.4+, максимальная длина имени 128 вместо прежних 32; внешний PCRE требует проверки версии. Максимум захватывающих групп 65535 не гарантирует, что такой шаблон скомпилируется: есть отдельные ограничения размера и вложенности. Не смешивайте разные имена с одинаковым номером в branch-reset: PCRE2 это отвергает. После branch-reset следующая группа получает номер после максимума всех ветвей; имена — не отдельные «лишние» захваты. preg_match_all объясняет неоднозначность duplicate names при PREG_PATTERN_ORDER.
Повторения: диапазон, жадность и последние значения
Количество в заказе содержит от одной до трёх ASCII-цифр. Квантификатор относится к элементу непосредственно слева: символу, классу, точке, группе или back reference. {1,3} ограничивает длину, но не числовое значение: 000 и 999 одинаково подходят по форме.
| Запись | Как читать |
|---|---|
| * / + / ? | 0…∞ / 1…∞ / 0…1. |
| {n} / {n,m} / {n,} | Точно n / от n до m / не меньше n. |
| *? / +? / ?? / {n,m}? | Ленивый порядок проб, сохраняя тот же диапазон количества. |
| *+ / ++ / ?+ / {n,m}+ | Possessive: найденное количество не отдаётся назад. |
- [0-9] — одна цифра.
- {1,3} — одна, две либо три таких цифры.
- 12 проходит целиком; 1234 не проходит якоря.
<?php
declare(strict_types=1);
$pattern = '~\A[0-9]{1,3}\z~';
echo preg_match($pattern, '12'), '/', preg_match($pattern, '1234'), "\n";
echo preg_match('~\A[0-9]{,3}\z~', '12'), "\n";
Ожидаемый результат
1/0
1
{,3} означает {0,3} с PCRE2 10.43+, в bundled PHP8.4+. Для PHP8.2+ переносимый вариант — {0,3}. При пустой строке второе правило также пройдёт.
<?php
declare(strict_types=1);
$text = '[BR41] [NT8]';
preg_match('~\[.*\]~', $text, $greedy);
preg_match('~\[.*?\]~', $text, $lazy);
preg_match_all('~\[[^\]]*\]~', $text, $bounded);
echo $greedy[0], "\n", $lazy[0], "\n", implode('|', $bounded[0]), "\n";
Ожидаемый результат
[BR41] [NT8]
[BR41]
[BR41]|[NT8]
Жадный * сначала берёт максимум; *? пробует минимум. Явный класс «до закрывающей скобки» лучше отражает этот простой формат, если вложенных скобок нет.
<?php
declare(strict_types=1);
preg_match('~\A([A-Z]{2}[0-9]+,?)+\z~', 'BR41,NT8', $m);
echo $m[1], "\n";
preg_match('~\A(a|(b))+\z~', 'aba', $nested);
echo $nested[1], '/', $nested[2], "\n";
Ожидаемый результат
NT8
a/b
Одна повторяемая группа не создаёт массив всех итераций. Во вложенной группе b может остаться захват от предыдущей итерации, даже если последняя захватила a. Для списка полей нужен preg_match_all.
Числа в квантификаторах меньше 65536, минимум не больше максимума. {n} — точно n; {n,} — от n; {0} исключает элемент из сопоставления, но не удаляет его определение группы. Неправильные фигурные записи могут стать литералами либо ошибкой по контексту; буквальные скобки экранируйте. PCRE2 10.43+ допускает пробел/TAB вокруг чисел и запятой в { 1 , 3 }; старые версии — нет. Под /U жадность меняется местами, а ? после квантификатора снова её переворачивает. *+ и ++ запрещают возврат: это отдельная семантика, а не универсальная оптимизация. Пустая повторяемая группа не зацикливает движок бесконечно: нулевой прогресс обрабатывается специально. Большой конечный repeat группы увеличивает compiled pattern, поэтому лимит количества не заменяет оценку памяти. Начальное .* с /s часто допускает implicit anchoring, но исключения (backrefs/atomic/control verbs) не позволяют полагаться на это как на правило валидации.
Back reference: повторить именно найденный текст
Метка товара записана дважды для контрольного подтверждения: BR41/BR41. Back reference сравнивает с конкретным ранее захваченным текстом. Повторное написание того же шаблона лишь проверило бы форму и допустило BR41/NT8.
- Группа code сохраняет BR41.
- После / ссылка \k
требует тот же текст. - BR41/NT8 отклоняется, хотя оба кода по отдельности правильны.
<?php
declare(strict_types=1);
$pattern = '~\A(?<code>[A-Z]{2}[0-9]+)/\k<code>\z~';
echo preg_match($pattern, 'BR41/BR41'), '/', preg_match($pattern, 'BR41/NT8'), "\n";
Ожидаемый результат
1/0
Ссылка — на значение группы, не на её имя и не на PHP-переменную.
<?php
declare(strict_types=1);
echo preg_match('~\A(BR)(41):\g{-1}:\g{1}2\z~', 'BR41:41:BR2'), "\n";
echo preg_match('~\A((?i)br)\s+\1\z~', 'BR br'), "\n";
Ожидаемый результат
1
0
\g{-1} — последняя группа, \g{1} — абсолютная первая; фигурные скобки отделяют следующую буквальную цифру 2. Регистр back reference определяется в месте ссылки, поэтому локальная i внутри захвата не делает внешнюю ссылку caseless.
Числовые формы \1, \g1, \g{1} эквивалентны при правильном номере; относительные -1/-2 упрощают перемещение фрагмента. Формы имени: \k<name>, \k'name', \k{name}, \g{name}, (?P=name). Важное уточнение старой PHP-страницы: \g<name> и \g'name' в PCRE2 — subroutine calls, а не ссылки на тот же захваченный текст; это разобрано в рекурсии. Не участвовавшая группа заставляет ссылку провалиться; пустая участвовавшая группа — другой случай. Внутренняя ссылка на текущую группу на первой итерации ещё не имеет значения, но может использовать предыдущее значение на повторе. Forward references бывают полезны только при корректной структуре повторов; не используйте их для обычного поля. Внутри [...] цифровой escape обозначает символ, не back reference. Не переносите syntax pattern \g{1} в replacement: там другой язык $1/${1}.
Lookahead и lookbehind: условие рядом с совпадением
Нужно извлечь число только перед pcs, не включая единицу в результат. (?=...) проверяет текст впереди и не двигает конечную позицию совпадения. (?!...) требует отсутствия впереди; (?<=...) и (?<!...) проверяют позади. Это условия на одну позицию, не дополнительные части вывода.
- В строке "12 pcs, 5 kg" число 12 имеет подходящее продолжение.
- (?= pcs) подтверждает единицу без её потребления.
- Результат — только "12".
<?php
declare(strict_types=1);
preg_match_all('~[0-9]+(?= pcs)~', '12 pcs, 5 kg', $m);
echo implode(',', $m[0]), "\n";
echo preg_match('~\ABR(?!00)[0-9]{2}\z~', 'BR00'), "\n";
Ожидаемый результат
12
0
(?!00) запрещает конкретный суффикс сразу после BR. Если нужен диапазон числа, после извлечения обычно понятнее числовое сравнение.
<?php
declare(strict_types=1);
preg_match('~(?<=qty:)[0-9]+~', 'qty:12', $m);
echo $m[0], "\n";
echo preg_match('~(?<!draft:)BR[0-9]+~', 'draft:BR41'), "\n";
preg_match('~(?<=qty: ?)[0-9]+~', 'qty: 12', $variable);
echo $variable[0], "\n";
Ожидаемый результат
12
0
12
Первый lookbehind фиксирован. Последний допускает 0–1 пробел: ограниченный variable lookbehind поддержан PCRE2 10.43+ (bundled PHP8.4+). Он не переносим на bundled PHP8.2/8.3.
<?php
declare(strict_types=1);
$pattern = '~(?<=ID:)(?!000)[0-9]{3}\b~';
echo preg_match($pattern, 'ID:042'), '/', preg_match($pattern, 'ID:000'), "\n";
Ожидаемый результат
1/0
Lookbehind подтверждает префикс слева, lookahead исключает три нуля справа, затем обычный класс потребляет три цифры. Проверки не «идут» последовательно по входу.
Фиксированные ветви lookbehind могут иметь разную длину: (?<=qty:|count:) работало и раньше. С PCRE2 10.43 variable branches имеют конечный вычислимый максимум, по умолчанию 255 символов; a* по-прежнему не подходит. Фиксированная ветвь имеет отдельный предел 65535; это не одна общая настройка. \X/\R в lookbehind не допускаются из-за переменной длины. Lookarounds можно вкладывать; позитивный захват внутри сохраняется, негативный обычно не даёт успешного захвата (особый случай — false-ветвь conditional). Сами assertion-группы не захватывают. Историческое «assertions нельзя повторять» слишком категорично: PCRE2 разрешает квантификатор lookaround, но (?!x){3} проверяет одну позицию, не следующие три символа. Привязки вроде \b повторять нельзя. Историческое число 200 вложенных групп не универсально: современный compile nesting limit обычно 250 и зависит от сборки, отдельно от match depth.
Atomic и possessive: когда нельзя отдавать уже взятое
При чтении суммы цифры должны закончиться перед пробелом и единицей. Возвращать последнюю цифру из [0-9]+ ради поиска обязательного пробела бессмысленно: цифра пробелом не станет. Atomic-группа (?>...) фиксирует выбранный вариант после выхода из неё; possessive [0-9]++ фиксирует повторение.
- 120 kg → повтор цифр забирает 120.
- Следующий символ — требуемый пробел.
- Текст проходит; повтор не должен отдавать цифры назад.
<?php
declare(strict_types=1);
echo preg_match('~\A[0-9]++ kg\z~', '120 kg'), "\n";
echo preg_match('~\A(?>[0-9]+) kg\z~', '120 kg'), "\n";
Ожидаемый результат
1
1
Для данного языка эти формы равноценны. Это не доказательство измеренного ускорения; реальный профиль зависит от оптимизаций движка.
<?php
declare(strict_types=1);
echo preg_match('~\A[0-9]+[0-9]\z~', '120'), '/', preg_match('~\A[0-9]++[0-9]\z~', '120'), "\n";
echo preg_match('~\A(?>BR|BR41)\z~', 'BR41'), "\n";
Ожидаемый результат
1/0
0
В первой паре последняя цифра нужна следующему элементу, поэтому запрет возврата ломает совпадение. Во втором atomic зафиксировал короткую BR и уже не пробует BR41.
Atomic не захватывает сама, но внутренние (...) могут. Возврат за всю группу к более ранним элементам остаётся возможным; запрещён возврат внутрь уже завершённой группы. Группы можно вкладывать, использовать с lazy и lookbehind. Для проверки буквального суффикса str_ends_with проще, чем ^(?>.*)(?<=...). Старое утверждение «atomic — единственный способ устранить nested-repeat» неверно как рекомендация: чаще лучше убрать неоднозначность грамматики, ограничить вход или использовать парсер. См. ограниченный пример исчерпания лимита.
Условные группы: закрывающая скобка нужна только после открывающей
Один и тот же код разрешён как BR41 или [BR41], но одиночная скобка ошибочна. Conditional проверяет, участвовала ли группа, либо выполняет assertion, и выбирает yes/no ветвь. Это syntax regex, а не PHP if.
- (\[)? сохраняет открывающую скобку в группу 1, если она есть.
- Основная часть читает BR41.
- (?(1)\]) требует закрыть скобку только при существовании первой.
<?php
declare(strict_types=1);
$pattern = '~\A(\[)?[A-Z]{2}[0-9]+(?(1)\])\z~';
foreach (['BR41', '[BR41]', '[BR41', 'BR41]'] as $code) {
echo preg_match($pattern, $code), "\n";
}
Ожидаемый результат
1
1
0
0
Без альтернативной no-ветви conditional не потребляет ничего. Якоря исключают лишнюю закрывающую скобку.
<?php
declare(strict_types=1);
$pattern = '~\A(?(?=INV-)INV-[0-9]{3}|RET-[0-9]{2})\z~';
echo preg_match($pattern, 'INV-104'), '/', preg_match($pattern, 'RET-12'), '/', preg_match($pattern, 'RET-104'), "\n";
Ожидаемый результат
1/1/0
(?=INV-) проверяется без потребления. Выбранная ветвь затем должна сама прочитать и префикс, и нужное число цифр.
Формы: (?(condition)yes) и (?(condition)yes|no); третья ветвь — compile error. Условие может ссылаться на номер или имя группы: (?(<open>)...), использовать положительный/отрицательный lookahead/lookbehind. (?(R)...) означает рекурсивный контекст, на верхнем уровне false; PCRE2 допускает уточнение рекурсивной группы. (?(DEFINE)...) хранит подпрограммы, не исполняя их на этом месте. Для двух простых форм обычная альтернатива нередко понятнее; conditional полезен для связи между ранее прочитанным и продолжением.
Комментарии: сделать проверяемое правило читаемым
Длинное правило нужно объяснять коллегам. Модификатор x позволяет расположить части на отдельных строках и подписать их # комментариями. PHP nowdoc сохраняет многострочный текст без подстановок; после закрывающей метки нужен ;. Комментарий объясняет причину ограничения, а не только повторяет символы.
- Правило разделено на начало, код, количество и конец.
- x исключает пробелы оформления и #комментарии вне классов.
- BR41=12 по-прежнему разбирается на поля.
<?php
declare(strict_types=1);
$pattern = <<<'RX'
~\A
(?<sku>[A-Z]{2}[0-9]+) # код из выгрузки
=
(?<qty>[0-9]{1,3}) # в сообщении максимум три цифры
\z~x
RX;
preg_match($pattern, 'BR41=12', $m);
echo $m['sku'], '/', $m['qty'], "\n";
Ожидаемый результат
BR41/12
Пробел после кода в исходном шаблоне служит оформлению и не обязателен во входе. Это отличается от режима без x.
<?php
declare(strict_types=1);
echo preg_match('~\A\#[ ]BR41\z~x', '# BR41'), "\n";
echo preg_match('~\ABR(?# warehouse code)41\z~', 'BR41'), "\n";
Ожидаемый результат
1
1
\# и [ ] оставляют буквальные символы в x; (?#...) работает и без x. Внутренний комментарий заканчивается ближайшей ), вложенные скобки в нём не используйте.
Комментарий # продолжается до настоящего перевода строки в шаблоне; два текстовых символа \n в nowdoc не завершают его. При обычном x пробел внутри [...] сохраняется; (?xx) в PCRE2 меняет это правило. Динамический текст с # экранируйте preg_quote с актуальным разделителем. Не вставляйте сам разделитель в комментарий: PHP ищет конец шаблона до PCRE-разбора.
Рекурсивное правило и повторное использование подшаблона
Учебная метка группы записывается в круглых скобках и может содержать вложенную группу. Простое [^()]* допускает только один уровень. (?&group) вызывает именованный подшаблон снова; когда этот вызов находится внутри его определения, возникает рекурсия. Вход в примере короткий и заранее ограничен. Для JSON, HTML или языка программирования нужен настоящий парсер, а не расширение этого упражнения.
- Начальная ( открывает группу.
- Обычный текст читается целиком; при новой ( вызывается то же правило group.
- Каждый вызов обязан дойти до своей ); затем управление возвращается наружу.
<?php
declare(strict_types=1);
$pattern = '~\A(?<group>\((?:[^()]++|(?&group))*\))\z~';
foreach (['(bolt(nut)washer)', '(bolt(nut)'] as $text) {
echo preg_match($pattern, $text), "\n";
}
Ожидаемый результат
1
0
[^()]++ читает обычный текст без бессмысленного дробления между повторениями. Именованное определение group целиком занимает строку; рекурсивный вызов не повторяет внешние якоря.
<?php
declare(strict_types=1);
$sameText = '~\A(?<code>[A-Z]{2}[0-9]+)/\k<code>\z~';
$sameRule = '~\A(?<code>[A-Z]{2}[0-9]+)/(?&code)\z~';
echo preg_match($sameText, 'BR41/NT8'), '/', preg_match($sameRule, 'BR41/NT8'), "\n";
Ожидаемый результат
0/1
Back reference требует BR41 снова; subroutine заново выполняет грамматику и принимает NT8. Это различие нельзя скрывать за одним словом «ссылка».
<?php
declare(strict_types=1);
$pattern = '~\A(?&code)->(?&code)\z(?(DEFINE)(?<code>[A-Z]{2}[0-9]+))~';
echo preg_match($pattern, 'BR41->NT8'), "\n";
echo preg_match($pattern, 'BR41->?'), "\n";
Ожидаемый результат
1
0
DEFINE само не потребляет текст; (?&code) вызывает правило по имени. Это легальная ссылка на определение, написанное ниже, в отличие от старого ограничения PHP-страницы.
Другие вызовы: (?R) и (?0) — весь шаблон; (?1), (?2) — группа по номеру; (?P>name), \g<name>, \g'name' — подпрограмма по имени. Возможны относительные номера (?−1) в смысле записи (? -1) без пробела и с ASCII-минусом: фактически (?-1). Не перепутайте с back reference \g{-1}. Рекурсировать можно только с продвижением или достижимой остановкой; цикл на том же вводе способен закончиться PREG_INTERNAL_ERROR. Captures не образуют дерево всех вызовов: после возврата видны соответствующие внешние значения, а не история вложенности. С PCRE2 10.30 вызов подпрограммы больше не автоматически atomic; фиксируйте это явно, если нужно. Исторические PHP.net пояснения про 15 captures, pcre_malloc и C-stack не описывают современную архитектуру PCRE2. Отдельные лимиты match/depth/heap/JIT и compile nesting сохраняются; «неограниченная вложенность» означает форму грамматики, а не бесконечные ресурсы.
Предсказуемый отказ: лимит и однозначная грамматика
Техническая ошибка regex не должна подвесить обработку заявки. Неоднозначные вложенные повторения заставляют движок пробовать множество разбиений, особенно когда последний знак не подходит. Лимит backtrack ограничивает работу matcher, но не обещает время в миллисекундах. Начните с лимита размера входа и ясного алфавита; не принимайте пользовательский текст за готовый regex.
- 20 букв A и завершающий !.
- Неудачное вложенное ([A-Z]+)+ пытается переразбить тот же префикс.
- Малый локальный лимит даёт контролируемый отказ; [A-Z]+ без вложения штатно возвращает 0.
<?php
declare(strict_types=1);
$old = ini_set('pcre.backtrack_limit', '100');
if ($old === false) { throw new RuntimeException('Cannot set test limit'); }
try {
$text = str_repeat('A', 20) . '!';
$result = preg_match('~(*NO_JIT)(*NO_AUTO_POSSESS)(*NO_START_OPT)\A([A-Z]+)+\z~', $text);
echo $result === false && preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR ? 'Лимит' : 'Иной результат';
echo "\n", preg_match('~\A[A-Z]+\z~', $text), "\n";
} finally { ini_set('pcre.backtrack_limit', $old); }
Ожидаемый результат
Лимит
0
NO_JIT/NO_AUTO_POSSESS/NO_START_OPT здесь только делают учебный отказ наблюдаемым на малом вводе; это не настройки для реального сервиса. finally восстанавливает прежний лимит даже при исключении.
<?php
declare(strict_types=1);
function isImportCode(string $input): bool
{
if (strlen($input) > 16) { return false; }
$result = preg_match('~\A[A-Z]{2}-[0-9]{1,10}\z~', $input);
if ($result === false) { throw new RuntimeException(preg_last_error_msg()); }
return $result === 1;
}
echo isImportCode('BR-104') ? 'Принят' : 'Отклонён';
echo "\n", isImportCode(str_repeat('A', 30)) ? 'Принят' : 'Отклонён', "\n";
Ожидаемый результат
Принят
Отклонён
strlen измеряет байты; для ASCII-договора это именно требуемая единица. Ограничение установлено до regex, а не после дорогого извлечения.
Класс [AEIOU] обычно проще, чем пять однобуквенных альтернатив, но измеряйте свой сценарий, а не объявляйте абсолютную скорость. Якорь убирает бессмысленные стартовые позиции; .*/s может позволять implicit anchoring лишь при определённых условиях. Обязательный буквальный суффикс иногда даёт ранний отказ через оптимизацию — это не устраняет проблему всех похожих шаблонов. Тестируйте успех, длинный провал, повреждённую кодировку и границы; не разворачивайте их до гигабайтов ради демонстрации. JIT ускоряет часть правил, но не исправляет неоднозначную грамматику и имеет свой стек. Для справочника тысяч буквальных значений используйте множество/индекс, а не огромную альтернативу. compiled-pattern limit не лечится повышением backtrack_limit.
Границы совместимости с Perl и оболочкой PHP
Имя Perl-compatible означает большое пересечение правил, а не идентичность всех версий. Официальная страница differences направляет к upstream. Здесь ориентир — tagged PCRE2 10.44 и его описание сравнений с Perl5.38; сам Perl в этой среде не запускался. Ни одна строка таблицы не выдаётся за испытание другого языка. Когда переносите правило из Perl/JavaScript/IDE, проверяйте и движок, и оболочку строк/разделителей/замены.
| Различие / группа правил | Следствие для переноса |
|---|---|
| Newline и точка | PCRE2 выбирает CR/LF/CRLF/NUL и др.; Perl иное правило LF. Не переносите /s или newline-политику без тестов. |
| Unicode-подмножество | Не все Perl properties/синтаксисы поддержаны: категория/скрипт/часть binary и Bidi, без произвольного Is/In... и длинного Letter. |
| Повтор assertion | Lookaround{3} проверяет одну позицию; \b* в PCRE2 нельзя. |
| Квантификатор без элемента | Корректный {1,2} в начале ветви — compile error, а не гарантированный литерал как в некоторых Perl-контекстах. |
| Captures негативного lookaround | Обычно не устанавливаются; особое условие negative assertion с false-ветвью может сохранить capture. Не переносите Perl-ожидания. |
| \F \l \L \u \U и именованный \N{...} | Perl-обработка строки не является PHP/PCRE-шаблоном; эти формы не поддерживаются в таком значении. \N и \N{U+...} имеют собственные допустимые значения. |
| \Q...\E | PCRE не имеет Perl-переменных $/@ и не интерполирует их; PHP-строка уже была обработана отдельно. |
| (?{code}) / (??{code}) | Исполнение Perl-кода из шаблона не поддерживается. PCRE callout — C API, PHP preg API не предоставляет регистрацию PHP-callback для него. |
| Рекурсия | С10.30 subroutine не автоматически atomic; старые PCRE1-памятки про обязательную атомарность непригодны. |
| Control verbs внутри subroutine | Эффект ограничен группой; особенно (*THEN) не обязан действовать как у Perl на окружающую альтернативу. |
| Несколько control verbs | Срабатывает первый достигнутый при возврате: поведение сложных комбинаций может отличаться от Perl. |
| Захваты повторов | Вложенный capture может сохранить прежнюю итерацию; Perl мог очистить его. |
| Branch-reset и имена | Один номер с разными именами запрещён; у Perl возможности шире. |
| Комментарии и пробелы | Нельзя произвольно вставлять пробел между ( и ? даже под x; версии Perl тоже меняли правила. |
| Диапазон с классом | [a-[:digit:]] — ошибка PCRE2, а не warning плюс буквальный дефис. |
| Lu/Ll с /i | В PCRE2 свойства сохраняют смысл upper/lower; Perl5.38 отличается. |
| \K в lookaround | С10.38 запрещён по умолчанию; не переносите старое исключение без явного API-контроля. |
| PCRE-расширения | D, U, anchored offset, ограничения \R, стартовые (*CR)/(*NO_JIT), относительные номера, non-atomic positive lookarounds — не универсальный regex-язык. |
| C API, не PHP preg API | NOTBOL/NOTEOL/NOTEMPTY, partial matching, callouts и альтернативный DFA matcher не являются флагами preg_match. (*NOTEMPTY)/(*NOTEMPTY_ATSTART) можно задать в самом шаблоне по PCRE2-договору. |
| Пределы и память | Compile-size/depth/capture/heap/JIT зависят от PCRE2 и оболочки. Interpreter с10.30 не использует прежнюю C-recursion для каждого match-frame. |
| Perl /d /a /u | Не тот же набор модификаторов. PHP /u задаёт UTF+UCP; не копируйте Perl charset flags. |
| Рекурсивные пустые ветви | Некоторые формы, объявляемые Perl бесконечной рекурсией, PCRE2 способен обработать. Для приложения не полагайтесь на эти пограничные различия; обеспечьте продвижение/ограничения. |
- Два кода BR41/NT8 имеют одинаковую форму.
- \g{code} требует ранее захваченное значение →0.
- \g
в PCRE2 вызывает грамматику группы заново →1.
<?php
declare(strict_types=1);
$same = '~\A(?<code>[A-Z]{2}[0-9]+)/\g{code}\z~';
$again = '~\A(?<code>[A-Z]{2}[0-9]+)/\g<code>\z~';
echo preg_match($same, 'BR41/NT8'), '/', preg_match($again, 'BR41/NT8'), "\n";
Ожидаемый результат
0/1
Этот запуск проверяет только PHP/PCRE2, не Perl. Старый список back references PHP.net включает формы вызова подпрограммы; здесь различие показано явно и сверено с upstream10.44.
Не все расширения PCRE2 доступны через PHP. Например библиотека умеет DFA matching и callback-callouts, но preg_* предоставляет свой фиксированный API. Версия pattern-language и PHP-оболочки входят в ваш контракт вместе. Проверка версии и обработка отказа нужны до интеграции стороннего правила.
Практика: небольшая строка складского импорта
Практика объединяет прежние шаги: отберите записи вида BR41=12, сохраните код и количество, а неподходящий ввод верните пользователю без потери исходного текста. Предварительно ограничьте строку 32байтами. Договор ASCII; количество от1 до999. Здесь нет записи в базу, HTTP или сообщения внешнему сервису.
- Проверить byte-length до regex.
- Извлечь sku/qty только из полной строки.
- Преобразовать qty в int и проверить диапазон.
- Вернуть пригодный результат либо причину; техническую ошибку не маскировать.
Сначала попробуйте сами. В решении функция возвращает массив с полем ok; для успеха добавляет sku/qty, для штатного отказа — reason. Это не обязательная архитектура приложения, а способ явно увидеть разные исходы. Тип array не описывает саму форму такого массива — её договор показан здесь текстом.
<?php
declare(strict_types=1);
function parseStockLine(string $line): array
{
if (strlen($line) > 32) { return ['ok' => false, 'reason' => 'слишком длинно']; }
$status = preg_match('~\A(?<sku>[A-Z]{2}[0-9]{1,4})=(?<qty>[0-9]{1,3})\z~', $line, $m);
if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
if ($status === 0) { return ['ok' => false, 'reason' => 'нужна запись SKU=количество']; }
$quantity = (int) $m['qty'];
if ($quantity < 1) { return ['ok' => false, 'reason' => 'количество от 1']; }
return ['ok' => true, 'sku' => $m['sku'], 'qty' => $quantity];
}
foreach (['BR41=12', 'NT8=0', 'BR41:12'] as $line) {
$record = parseStockLine($line);
echo $record['ok'] ? $record['sku'] . ' × ' . $record['qty'] : $record['reason'];
echo "\n";
}
Ожидаемый результат
BR41 × 12
количество от 1
нужна запись SKU=количество
Regex проверяет структуру, затем обычное число проверяет диапазон. Короткая форма return останавливает функцию на конкретной причине. Ведущие нули qty в этом договоре разрешены и исчезают при преобразовании; SKU остаётся строкой.
Следующее изменение требования: количество может быть пустым и тогда требует уточнения. Сначала решите, действительно ли пустое значение допустимо; затем сделайте группу optional с PREG_UNMATCHED_AS_NULL и различайте null/"0". Не заменяйте это проверкой empty, которая смешает разные случаи. Если данные придут в JSON или CSV, начните с их парсера, а regex примените только к отдельному полю. Дальнейшие исходники полезных действий находятся в preg_match, preg_replace_callback и ограничении нагрузки.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
preg_match_allСобрать все коды товаров из заметки, сохранив порядок появления.
preg_match_all(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false[A-Z]{2} означает две латинские заглавные буквы, [0-9]+ — номер. $all[0] — список целых совпадений при обычном порядке. foreach поочерёдно читает этот список; echo печатает каждый код.
| Аргумент / результат | Договор |
|---|---|
| pattern / subject | Один шаблон и одна исходная строка. Поиск продолжается после предыдущего совпадения. |
| &matches | Результат всех совпадений; формат зависит от flags. Именованные группы дублируются под именем и номером. |
| flags | PREG_PATTERN_ORDER по умолчанию: массив по группам; PREG_SET_ORDER: массив по совпадениям. Выбирайте один порядок. Добавляются | PREG_OFFSET_CAPTURE и/или PREG_UNMATCHED_AS_NULL. |
| offset | Байтовый старт, в том числе отрицательный от конца; контекст не обрезается. |
| Результат | Количество полных совпадений, 0 или false при отказе. Не количество групп. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4+; встроенное расширение pcre |
- Записка "BR41, NT8, BR41".
- Три неперекрывающихся совпадения в исходном порядке.
- $all[0] → BR41 / NT8 / BR41; число 3.
<?php
declare(strict_types=1);
$count = preg_match_all('~[A-Z]{2}[0-9]+~', 'BR41, NT8, BR41', $all);
echo 'Найдено: ', $count, "\n";
foreach ($all[0] as $code) { echo $code, "\n"; }
Ожидаемый результат
Найдено: 3
BR41
NT8
BR41
Повторы сохраняются. Это извлечение из заметки, а не проверка формата всей заметки.
<?php
declare(strict_types=1);
$count = preg_match_all('~(?<sku>[A-Z]{2}[0-9]+)(?:=(?<qty>[0-9]+))?~',
'BR41=12 NT8', $rows, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
if ($count === false) { throw new RuntimeException(preg_last_error_msg()); }
foreach ($rows as $row) {
echo $row['sku'], ':', $row['qty'] ?? 'уточнить', "\n";
}
Ожидаемый результат
BR41:12
NT8:уточнить
В PREG_SET_ORDER сначала выбираем совпадение, затем его поля. Число 12 остаётся строкой; допустимое количество и существование SKU проверяет бизнес-логика.
<?php
declare(strict_types=1);
preg_match_all('~(?=([0-9]{2}))~', '1234', $m, PREG_OFFSET_CAPTURE);
foreach ($m[1] as [$pair, $byte]) { echo $pair, '@', $byte, "\n"; }
Ожидаемый результат
12@0
23@1
34@2
Проверка (?=...) не потребляет текст, но внутренняя группа сохраняет пару. Движок сам продвигает поиск после пустого совпадения. Обычный шаблон [0-9]{2} нашёл бы только 12 и 34.
Глобальный поиск хранит весь результат в памяти: для большого файла обрабатывайте логические записи, если шаблон не переходит через их границы. Совпадения обычно не перекрываются. При duplicate named groups и PREG_PATTERN_ORDER именованный массив соответствует последней группе с этим именем, поэтому прежнее значение может оказаться пустым; для ясного контракта предпочитайте уникальные имена.
Попробуйте. Уберите =12 из заметки: обе строки получают «уточнить», но count остаётся 2.
Дальше: preg_match, preg_split, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_grepОтобрать допустимые коды из списка и сохранить связь с исходными ключами.
preg_grep(string $pattern, array $array, int $flags = 0): array|falseЗапись 14 => "BR41" связывает ключ строки импорта 14 с её кодом. preg_grep оставляет эту связь. foreach ($valid as $line => $code) читает ключ и значение, не перенумеровывая строки.
| Аргумент / результат | Договор |
|---|---|
| pattern | Одно правило для значений массива, не для ключей. |
| array | Массив проверяемых значений; в учебном договоре только строки. Вложенные структуры предварительно разбираются отдельно. |
| flags = 0 | PREG_GREP_INVERT возвращает значения, не соответствующие шаблону. |
| Результат | Массив с исходными ключами; [] если ничего не отобрано; false при отказе. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4+; встроенное расширение pcre |
- Строка 14: BR41; строка 20: ???.
- Правило две буквы + цифры принимает только BR41.
- Результат сохраняет ключ 14 → его можно показать в отчёте.
<?php
declare(strict_types=1);
$valid = preg_grep('~\A[A-Z]{2}[0-9]+\z~', [14 => 'BR41', 20 => '???']);
foreach ($valid as $line => $code) { echo $line, ':', $code, "\n"; }
Ожидаемый результат
14:BR41
Ключи — номера записей, а не byte-offset. Функция не преобразует значения.
<?php
declare(strict_types=1);
function invalidCodes(array $codes): array
{
$bad = preg_grep('~\A[A-Z]{2}[0-9]+\z~', $codes, PREG_GREP_INVERT);
if ($bad === false) { throw new RuntimeException(preg_last_error_msg()); }
return $bad;
}
foreach (invalidCodes(['a' => 'BR41', 'b' => 'BR?', 'c' => 'nt8']) as $key => $code) {
echo $key, ':', $code, "\n";
}
Ожидаемый результат
b:BR?
c:nt8
Правило сознательно требует заглавные ASCII-буквы; функция не «исправляет» их автоматически. Реальная проверка массива должна отдельно гарантировать строковые элементы.
preg_grep не возвращает захваченные группы. Для сложной политики по ключу/значению используйте foreach/array_filter с callback, для извлечения полей — preg_match. PHP может приводить скалярные элементы к строке во время проверки, но в результате остаются исходные значения; не используйте это как проверку типов.
Попробуйте. Передайте список только правильных кодов: invalidCodes вернёт [], а не false.
Дальше: preg_match, preg_filter. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_quoteВставить название товара в шаблон как буквальный текст, даже если в нём есть точки, скобки и +.
preg_quote(string $str, ?string $delimiter = null): stringВызов preg_quote($name, "~") создаёт безопасную буквальную часть; оператор . соединяет её с ~ и якорями. Экранируйте данные, а не весь готовый шаблон, иначе якоря потеряют смысл.
| Аргумент / результат | Договор |
|---|---|
| str | Буквальная строка, которую требуется найти. |
| delimiter = null | Один фактически выбранный разделитель шаблона, например ~. Он тоже экранируется. null не добавляет специальный разделитель. |
| Результат | Строка с обратными слешами перед метасимволами. Исходная переменная не меняется; функция binary-safe. |
| Ошибки | Типовые ошибки аргументов — TypeError. Это не проверка корректности UTF-8 и не экранирование HTML/SQL/replacement. |
| Версия | PHP 4+; встроенное расширение pcre |
- Название "M10+ (2)" содержит + и скобки.
- preg_quote превращает их в буквальные символы правила.
- Совпадает точное название, а "M100 2" не подходит.
<?php
declare(strict_types=1);
$name = 'M10+ (2)';
$pattern = '~\A' . preg_quote($name, '~') . '\z~';
echo preg_match($pattern, 'M10+ (2)'), '/', preg_match($pattern, 'M100 2'), "\n";
Ожидаемый результат
1/0
Неэкранированный + означал бы повторение нуля, а скобки — группу. Здесь они данные.
<?php
declare(strict_types=1);
function quantityFor(string $name, string $line): ?string
{
$pattern = '~\A' . preg_quote($name, '~') . ': ([0-9]+) pcs\z~u';
$status = preg_match($pattern, $line, $m);
if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
return $status === 1 ? $m[1] : null;
}
echo quantityFor('BR~41', 'BR~41: 12 pcs'), "\n";
Ожидаемый результат
12
Разделитель ~ из названия экранируется вторым аргументом. Неизменяемая часть шаблона задаёт формат строки, данные задают только имя.
Полный набор обрабатываемых знаков: . \ + * ? [ ^ ] $ ( ) { } = ! < > | : - #. Символ / сам по себе не regex-метасимвол, поэтому для /-разделителя нужен второй аргумент. # экранируется с PHP7.3, delimiter допускает null с 7.2. Для поиска без окружающей грамматики достаточно str_contains; preg_quote не нужен. Для буквального динамического replacement используйте callback.
Попробуйте. Добавьте в название / и #: точное сравнение по-прежнему должно проходить без unknown modifier.
Дальше: preg_match, preg_replace_callback. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_filterОбновить только подходящие строки каталога и получить список затронутых записей.
preg_filter(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, int &$count = null): string|array|nullВ ~\Aold/~ префикс old/ разрешён только в начале строки. Замена new/ переносит подходящие пути в новый раздел. Обычный preg_replace сохранил бы и остальные записи; preg_filter исключает их.
| Аргумент / результат | Договор |
|---|---|
| pattern | Одна строка-шаблон с разделителями либо массив шаблонов. |
| replacement | Строка либо массив замен, по порядку элементов pattern, не по совпадающим ключам. Если replacement — массив, pattern тоже должен быть массивом; иначе TypeError. Одна строка обслуживает все шаблоны; недостающие элементы массива означают пустую замену. $0 — всё совпадение, $1…$99 / \1…\99 — числовые группы; ${1}2 отделяет группу 1 от цифры 2. |
| subject | Строка либо массив строк. Ключи массива сохраняются; правила применяются к каждому значению. |
| limit = -1 | Максимум замен на каждый шаблон в каждой строке. -1 — все, 0 — ни одной. |
| &count = null | Необязательная переменная: общее число сделанных замен. & в сигнатуре означает запись результата в переданную переменную. |
| Результат | Как preg_replace, но возвращает лишь строки, где произведена замена. Массив сохраняет ключи; без замен [] для массива, null для скалярной строки. Поэтому null здесь может означать нормальное отсутствие результата. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 5.3+; встроенное расширение pcre |
- a: old/bolt, b: keep/nut.
- В a заменяется начальный old/ → new/.
- Результат содержит только a: new/bolt.
<?php
declare(strict_types=1);
$changed = preg_filter('~\Aold/~', 'new/', ['a' => 'old/bolt', 'b' => 'keep/nut']);
foreach ($changed as $key => $path) { echo $key, ':', $path, "\n"; }
Ожидаемый результат
a:new/bolt
Ключ позволяет связать результат с исходной записью. Никакие файлы и URL реально не меняются.
<?php
declare(strict_types=1);
$removed = preg_filter('~\Aobsolete\z~', '', 'obsolete', -1, $count);
echo '[', $removed, ']:', $count, "\n";
$absent = preg_filter('~\Aobsolete\z~', '', 'active');
echo $absent === null ? 'Нет замены' : 'Есть замена';
echo ':', preg_last_error() === PREG_NO_ERROR ? 'без ошибки' : 'ошибка', "\n";
Ожидаемый результат
[]:1
Нет замены:без ошибки
Пустая строка результата отличается от null. Сразу читаем ошибку: следующий preg-вызов может изменить её. При пакетной важной миграции проще явно проверять каждую строку.
Проверка if (!$result) теряет различие пустого текста, [] и null. При неверном шаблоне возможны warning и пустой/нулевой результат; не трактуйте «ничего не изменено» как доказательство успешного прогона. Для отбора без изменений используйте preg_grep. limit 0 означает отсутствие замен даже если правило могло бы совпасть.
Попробуйте. Замените obsolete на obsolete: результат всё равно включается, потому что выполнена замена, хотя байты те же.
Дальше: preg_replace, preg_grep, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_replace_callbackВычислить замену для каждого совпадения: увеличить число или подставить буквальную цену из данных.
preg_replace_callback(string|array $pattern, callable $callback, string|array $subject, int $limit = -1, int &$count = null, int $flags = 0): string|array|nullfunction (array $m): string {...} — функция без имени, которую preg_replace_callback вызывает за нас. return отдаёт замену. (int) преобразует учебные цифры в целое, + увеличивает, (string) превращает число обратно в текст.
| Аргумент / результат | Договор |
|---|---|
| pattern | Один шаблон или массив, применяемый последовательно. |
| callback | Вызываемая функция handler(array $matches): string. [0] — целое совпадение, далее группы. Возвращает буквальную замену, а не replacement-шаблон. |
| subject | Строка либо массив строк; сохраняются ключи. |
| limit / &count | -1 — все; максимум на шаблон/строку; count — сумма замен. |
| flags = 0 | С PHP7.4: PREG_OFFSET_CAPTURE и PREG_UNMATCHED_AS_NULL меняют формат matches так же, как у preg_match. |
| Результат | Новая строка/массив; без совпадений исходные значения; null при PCRE-ошибке. Исключение из callback распространяется наружу. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 4.0.5+; flags PHP7.4+; встроенное расширение pcre |
- "batch=4" → группа 1 содержит 4.
- Callback вычисляет 4+1 и возвращает "batch=5".
- Текст получает новую метку; внешней записи нет.
<?php
declare(strict_types=1);
$next = preg_replace_callback('~batch=([0-9]{1,4})~', function (array $m): string {
return 'batch=' . ((int) $m[1] + 1);
}, 'batch=4');
echo $next, "\n";
Ожидаемый результат
batch=5
Это преобразование текста; для уникальных номеров в базе нужна транзакционная политика. Ограничение четырьмя цифрами держит учебное число малым.
<?php
declare(strict_types=1);
$values = ['price' => '$1.00', 'sku' => 'BR41'];
$result = preg_replace_callback('~\{\{([a-z]+)\}\}~',
function (array $m) use ($values): string { return $values[$m[1]] ?? $m[0]; },
'{{sku}} costs {{price}}; {{unknown}}');
echo $result, "\n";
Ожидаемый результат
BR41 costs $1.00; {{unknown}}
use копирует доступ к словарю в замыкание; ?? оставляет неизвестную метку. Возвращённый $1.00 не разбирается как ссылка на группу. Это текстовый шаблон; для HTML результат экранируют в контексте вывода.
<?php
declare(strict_types=1);
$result = preg_replace_callback('~(?<sku>BR[0-9]+)(?:=(?<qty>[0-9]+))?~',
function (array $m): string {
return $m['sku'][0] . '@' . $m['sku'][1] . ':' . ($m['qty'][0] ?? 'нет');
}, 'x BR41', -1, $count, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL);
echo $result, '/', $count, "\n";
Ожидаемый результат
x BR41@2:нет/1
Флаги меняют элементы с текстов на пары: [0] текст или null, [1] byte-offset. Чтение $m["sku"] как строки после этого ошибочно.
Callback не запускается при отсутствии совпадения. Сохраняйте возвращённый результат, а не меняйте захваченный массив. Не выполняйте PHP из пользовательского текста. Если callback вызывает другие preg-функции, сохраняйте их диагностику локально: один общий last_error не журнал всех операций. Контекстные изменения в БД/почте внутри callback потребовали бы отдельной идемпотентности; здесь их нет.
Попробуйте. Добавьте values["unknown"] = "готово": изменится только соответствующая метка.
Дальше: preg_replace, preg_replace_callback_array, preg_quote. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_replace_callback_arrayЗадать отдельное действие каждому типу метки в одном последовательном конвейере.
preg_replace_callback_array(array $pattern, string|array $subject, int $limit = -1, int &$count = null, int $flags = 0): string|array|nullОператор => связывает шаблон с функцией. fn (array $m): string => выражение — короткая функция, возвращающая выражение. Правила выполняются в порядке вставки, а не по позиции первого совпадения в тексте.
| Аргумент / результат | Договор |
|---|---|
| pattern | Ассоциативный массив: строка-шаблон => callable. Ключи уникальны: повтор одинакового ключа массива перезапишет прежний callback. |
| subject | Строка либо массив строк. |
| limit = -1 / &count | Лимит на каждый шаблон/строку; суммарное число замен. |
| flags = 0 | PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL для формата matches; доступны с PHP7.4. |
| Результат | Новая строка/массив; при PCRE-ошибке null. Ошибка аргумента/callback или исключение обработчика не превращаются автоматически в null. |
| Ошибки | Некомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения. |
| Версия | PHP 7.0+; flags PHP7.4+; встроенное расширение pcre |
- "sku:br41 qty:3" содержит две формы поля.
- Первое правило переводит код в верхний регистр.
- Второе оформляет количество → BR41 × 3.
<?php
declare(strict_types=1);
$rules = [
'~sku:([a-z0-9]+)~' => fn (array $m): string => strtoupper($m[1]),
'~qty:([0-9]+)~' => fn (array $m): string => '× ' . $m[1],
];
echo preg_replace_callback_array($rules, 'sku:br41 qty:3'), "\n";
Ожидаемый результат
BR41 × 3
strtoupper здесь предназначен только для ASCII-кода. Русские названия не проходят через него.
<?php
declare(strict_types=1);
$rules = [
'~\{sku\}~' => fn (array $m): string => 'BR41',
'~BR([0-9]+)~' => fn (array $m): string => 'bolt-' . $m[1],
];
$result = preg_replace_callback_array($rules, ['label' => '{sku}'], -1, $count);
echo $result['label'], '/', $count, "\n";
Ожидаемый результат
bolt-41/2
Два совпадения в разных стадиях дают count=2. Если новые значения не должны повторно обрабатываться, используйте один общий шаблон и один dispatch-callback.
Функция не выбирает «первое из всех правил». Порядок влияет на результат; лимит не общий для конвейера. Для пустого массива правил результат остаётся прежним. Имена полей лучше разобрать одной грамматикой, если их значения могут содержать текст, похожий на другие метки.
Попробуйте. Поменяйте правила во втором примере местами: получите BR41/1.
Дальше: preg_replace_callback, preg_replace, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_last_errorОтличить повреждённый UTF-8 и исчерпание ресурса от нормального ответа «не найдено».
preg_last_error(): intПустые скобки означают отсутствие аргументов. Вызов читает состояние последней PCRE-операции. Сохраните код сразу после результата, до следующей проверки или regex-логгера.
| Аргумент / результат | Договор |
|---|---|
| Результат | Один код: PREG_NO_ERROR, PREG_INTERNAL_ERROR, PREG_BACKTRACK_LIMIT_ERROR, PREG_RECURSION_LIMIT_ERROR, PREG_BAD_UTF8_ERROR, PREG_BAD_UTF8_OFFSET_ERROR или PREG_JIT_STACKLIMIT_ERROR. |
| Ошибки | Не сообщает ошибки PHP-типов/исключения callback. Сообщение компиляции обычно приходит отдельным E_WARNING. |
| Версия | PHP 5.2+; встроенное расширение pcre |
- Передан байт FF под режимом /u.
- preg_match возвращает false.
- Код BAD_UTF8 → вход не соответствует договору UTF-8.
<?php
declare(strict_types=1);
$result = preg_match('~. sentinel~u', "\xFF");
$error = preg_last_error();
echo $result === false ? 'Отказ' : 'Готово';
echo ':', $error === PREG_BAD_UTF8_ERROR ? 'не UTF-8' : 'другая причина', "\n";
Ожидаемый результат
Отказ:не UTF-8
\xFF в двойных кавычках создаёт конкретный невалидный байт. Это не попытка автоматически угадать его настоящую кодировку.
<?php
declare(strict_types=1);
preg_match('~.~u', 'Я', $m, 0, 1);
$saved = preg_last_error();
preg_match('~BR~', 'NT');
echo $saved === PREG_BAD_UTF8_OFFSET_ERROR ? 'Старт внутри UTF-8' : 'Иное';
echo "\n", preg_last_error() === PREG_NO_ERROR ? 'Последняя проверка штатная' : 'Ошибка', "\n";
Ожидаемый результат
Старт внутри UTF-8
Последняя проверка штатная
Offset 1 попадает во второй байт Я. После штатного отсутствия BR ошибка сбрасывается: состояние не хранит историю.
Не сравнивайте числовые коды с выдуманными литералами: используйте имена констант. Ограничения backtrack/depth не измеряются числом символов или миллисекундами. Не повышайте их автоматически на недоверенном вводе: сначала ограничьте размер/упростите правило и обработайте отказ.
Попробуйте. Передайте offset=0 во втором примере: сохранённый код станет PREG_NO_ERROR.
Дальше: preg_last_error_msg, preg_match. Как устроен шаблон и вызов; ошибка или нет совпадения.
preg_last_error_msgПолучить понятную диагностическую строку для разработчика, сохранив машинный код отдельно.
preg_last_error_msg(): stringСразу после preg_match сохраняем и код, и сообщение. echo показывает учебную диагностику; в реальном API пользователю дают собственное сообщение, а техническую причину журналируют без полного конфиденциального ввода.
| Аргумент / результат | Договор |
|---|---|
| Результат | Текст последнего PCRE-состояния; без ошибки "No error". Строка предназначена для диагностики, не для выбора бизнес-ветки. |
| Ошибки | Сообщение отражает общее состояние PCRE, а не полный текст compile-warning с позицией и не историю всех попыток. |
| Версия | PHP 8.0+; встроенное расширение pcre |
- "BR41" соответствует правилу.
- Последний PCRE-статус штатный.
- Диагностика: No error.
<?php
declare(strict_types=1);
$found = preg_match('~\ABR[0-9]+\z~', 'NT8');
echo $found, ':', preg_last_error_msg(), "\n";
Ожидаемый результат
0:No error
Ноль — полезный результат проверки. Сообщение No error не утверждает, что совпадение найдено.
<?php
declare(strict_types=1);
$result = preg_match('~.~u', "\xFF");
$diagnostic = ['code' => preg_last_error(), 'message' => preg_last_error_msg()];
if ($result === false) {
echo $diagnostic['code'] === PREG_BAD_UTF8_ERROR ? 'input-invalid' : 'regex-failed';
echo "\n", $diagnostic['message'], "\n";
}
Ожидаемый результат
input-invalid
Malformed UTF-8 characters, possibly incorrectly encoded
Точная английская строка проверена на PHP8.5.11. Пользовательский ответ выбирается по стабильной константе; сообщение может измениться между версиями.
Вызовы preg_last_error/preg_last_error_msg сами не запускают regex, поэтому можно прочитать оба. Последующая preg-операция изменяет наблюдаемое состояние. Для PHP7 аналог строили через карту констант; сам этот API доступен с PHP8.
Попробуйте. Замените FF на обычное A: result станет 1, диагностический message — No error.
Дальше: preg_last_error, preg_match. Как устроен шаблон и вызов; ошибка или нет совпадения.
Источники и обсуждения
- PHP: book.pcre
- PHP: pcre.setup
- PHP: pcre.installation
- PHP: pcre.configuration
- PHP: pcre.constants
- PHP: pcre.examples
- PHP: pcre.pattern
- PHP: reference.pcre.pattern.syntax
- PHP: reference.pcre.pattern.modifiers
- PHP: reference.pcre.pattern.differences
- PHP: ref.pcre
- PHP: regexp.introduction
- PHP: regexp.reference.delimiters
- PHP: regexp.reference.meta
- PHP: regexp.reference.escape
- PHP: regexp.reference.unicode
- PHP: regexp.reference.anchors
- PHP: regexp.reference.dot
- PHP: regexp.reference.character-classes
- PHP: regexp.reference.alternation
- PHP: regexp.reference.internal-options
- PHP: regexp.reference.subpatterns
- PHP: regexp.reference.repetition
- PHP: regexp.reference.back-references
- PHP: regexp.reference.assertions
- PHP: regexp.reference.onlyonce
- PHP: regexp.reference.conditional
- PHP: regexp.reference.comments
- PHP: regexp.reference.recursive
- PHP: regexp.reference.performance
- PHP: function.preg-match
- PHP: function.preg-match-all
- PHP: function.preg-grep
- PHP: function.preg-split
- PHP: function.preg-quote
- PHP: function.preg-replace
- PHP: function.preg-filter
- PHP: function.preg-replace-callback
- PHP: function.preg-replace-callback-array
- PHP: function.preg-last-error
- PHP: function.preg-last-error-msg
- PCRE2 10.44: язык шаблонов
- PHP8.5.11: реализация оболочки pcre