Регулярные выражения в PHP — рабочая лошадка для валидации форм, парсинга логов, очистки HTML и любой задачи, где нужно найти структуру внутри строки. В этом руководстве разберём весь набор PCRE-функций (preg_match, preg_match_all, preg_replace, preg_replace_callback, preg_split), синтаксис шаблонов и подводные камни, на которых обычно горят разработчики.

Анатомия шаблона: разделители, якоря и флаги
Любой PCRE-шаблон в PHP — это строка, обёрнутая в пару одинаковых символов-разделителей. Чаще всего используют слэш /, но если внутри шаблона много слэшей (например, при работе с URL), удобнее взять # или ~ — меньше экранирования.
// Эквивалентные шаблоны — проверяем https-ссылку
preg_match('/^https:\/\/[\w.-]+/', $url);
preg_match('#^https://[\w.-]+#', $url);
preg_match('~^https://[\w.-]+~', $url);
После закрывающего разделителя ставятся модификаторы:
i— игнорировать регистр (case-insensitive)u— режим UTF-8, обязателен при работе с кириллицейs— точка.совпадает в том числе с переводом строкиm— многострочный режим,^и$работают на каждой строкеx— расширенный режим, можно делать пробелы и комментарии в шаблоне
Якоря ^ и $ привязывают шаблон к началу и концу строки — без них совпадение может найтись где угодно в середине. Это самая частая ошибка валидации: шаблон /[a-z]+/ найдёт буквы в любой строке, включая 123abc!@#. Правильно — /^[a-z]+$/.
preg_match: проверка и захват групп
Функция preg_match возвращает 1 при первом совпадении, 0 если ничего не найдено и false при ошибке шаблона. Третий аргумент по ссылке — массив с захваченными группами.
// Валидация телефона в формате +7 (XXX) XXX-XX-XX
$phone = '+7 (495) 123-45-67';
$pattern = '/^\+7\s\((\d{3})\)\s(\d{3})-(\d{2})-(\d{2})$/';
if (preg_match($pattern, $phone, $matches)) {
// $matches[0] — вся подстрока целиком
// $matches[1..4] — захваченные группы
[$full, $code, $a, $b, $c] = $matches;
echo "Код города: {$code}, номер: {$a}-{$b}-{$c}";
}
Группы можно делать именованными — это намного читаемее, чем нумерованные индексы. Синтаксис: (?P<имя>...).
// Парсим строку лога Nginx: IP, дата, метод, URL, код ответа
$line = '192.168.1.1 - - [07/May/2026:10:23:45 +0300] "GET /api/users HTTP/1.1" 200';
$pattern = '/^(?P<ip>\d+\.\d+\.\d+\.\d+).*\[(?P<date>[^\]]+)\]\s"(?P<method>\w+)\s(?P<url>[^\s]+).*"\s(?P<code>\d{3})/';
if (preg_match($pattern, $line, $m)) {
echo "IP: {$m['ip']}, метод: {$m['method']}, URL: {$m['url']}, код: {$m['code']}";
}
Совет: всегда оборачивайте вызов в проверку === 1 или !== false, если важно отличить «не нашёл» от «ошибка в шаблоне». Сравнение через if (preg_match(...)) подходит только когда ошибки шаблона исключены — например, шаблон-константа.
preg_match_all: все совпадения сразу
Если в строке несколько вхождений и нужны все — preg_match не подойдёт, он находит только первое. Используем preg_match_all: возвращает количество найденных совпадений, в массив пишет все группы.
// Достаём все email-адреса из текста
$text = 'Пишите на support@example.com или sales@example.org, а лучше info@karoche.ru';
$pattern = '/[\w.+-]+@[\w-]+\.[\w.-]+/u';
preg_match_all($pattern, $text, $matches);
print_r($matches[0]);
// [0 => 'support@example.com', 1 => 'sales@example.org', 2 => 'info@karoche.ru']
Четвёртый аргумент управляет порядком группировки. По умолчанию (PREG_PATTERN_ORDER) $matches[0] — массив всех полных совпадений, $matches[1] — все первые группы и т.д. С флагом PREG_SET_ORDER структура «переворачивается»: каждое совпадение становится отдельным массивом — это удобнее, когда нужно итерировать по результатам.
// Извлекаем все теги img и их src+alt
$html = '<img src="a.jpg" alt="Кот"><img src="b.png" alt="Пёс">';
$pattern = '/<img\s+src="([^"]+)"\s+alt="([^"]+)"/u';
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
echo "Файл: {$m[1]}, подпись: {$m[2]}\n";
}
// Файл: a.jpg, подпись: Кот
// Файл: b.png, подпись: Пёс
Важно: для разбора реального HTML-документа лучше использовать DOMDocument или simplexml_load_string — regex плохо справляется с вложенностью и ломаной разметкой. Шаблон выше работает только на строго предсказуемых строках.
preg_replace и preg_replace_callback
Простая замена по шаблону — это preg_replace. В замене можно использовать обратные ссылки $1, $2 на группы из шаблона.
// Превращаем markdown-ссылки [текст](url) в HTML
$md = 'Подробнее в [статье](https://karoche.ru/regex)';
$html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $md);
// <a href="https://karoche.ru/regex">статье</a>
// Чистим лишние пробелы — несколько подряд → один
$clean = preg_replace('/\s+/u', ' ', ' это строка с пробелами ');
// 'это строка с пробелами'
Когда замена требует логики (преобразование значения, обращение к БД, расчёт) — нужен preg_replace_callback. Вторым аргументом передаётся функция, в которую попадает массив совпадений; то, что она вернёт, заменит совпадение.
// Подсветка цен: каждое число с "руб" оборачиваем в span и форматируем
$text = 'Цена 12000 руб, скидка 1500 руб';
$result = preg_replace_callback(
'/(\d+)\s*руб/u',
function ($m) {
$price = (int) $m[1];
$formatted = number_format($price, 0, '', ' '); // '12 000'
return "<span class=\"price\">{$formatted} ₽</span>";
},
$text
);
// 'Цена <span class="price">12 000 ₽</span>, скидка <span class="price">1 500 ₽</span>'
Колбэк удобен для генерации slug, замены тегов BB-кода на HTML, маскирования персональных данных в логах — всего, где сама замена не сводится к шаблону вида $1.
preg_split: разбиение строк
Аналог explode, но с регулярным выражением в роли разделителя. Незаменим, когда разделители разные или нужно отбросить пустые элементы.
// Разбиваем по запятым, точкам с запятой и пробелам
$tags = 'php, javascript; css regex,bitrix';
$parts = preg_split('/[\s,;]+/u', $tags, -1, PREG_SPLIT_NO_EMPTY);
// ['php', 'javascript', 'css', 'regex', 'bitrix']
// Парсим CSV-строку с экранированием — простой случай
$csv = 'Иван,"Петров, ст.",42,Москва';
$cells = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $csv);
// ['Иван', '"Петров, ст."', '42', 'Москва']
Флаг PREG_SPLIT_NO_EMPTY убирает пустые элементы (когда в строке два разделителя подряд). PREG_SPLIT_DELIM_CAPTURE сохраняет совпадения групп в результате — полезно для токенизации.
Подводные камни и производительность
Regex — мощный инструмент, но у него есть несколько ловушек, в которые попадают почти все:
- Кириллица без флага
u. Шаблон/[а-я]+/безu-флага работает на байтах, не на символах, и даёт неверные результаты в UTF-8. Всегда добавляйтеu:/[а-яё]+/iu. - Жадные квантификаторы. Шаблон
/<.*>/для строки<b>текст</b>схватит всё от первого<до последнего>— то есть всю строку. Решение — ленивый квантификатор:/<.*?>/или класс с инверсией:/<[^>]+>/. - Catastrophic backtracking. Шаблоны вида
/(a+)+b/на длинной строке безbв конце могут уронить процесс или упереться в лимитpcre.backtrack_limit. Избегайте вложенных квантификаторов; используйте атомарные группы(?>...)или posessive-квантификаторы++,*+. - Неэкранированные спецсимволы в литералах. Если подставляете пользовательский ввод в шаблон — оборачивайте через
preg_quote:preg_quote($search, '/'). - Экранирование в строке-замене. В
preg_replaceсимвол$в строке-замене интерпретируется как обратная ссылка. Если нужен буквальный знак доллара — пишите\\$.
Простой пример безопасной подстановки пользовательского значения:
// Подсвечиваем введённое слово в тексте — безопасно
function highlight(string $text, string $needle): string {
$escaped = preg_quote($needle, '/');
return preg_replace(
'/(' . $escaped . ')/iu',
'<mark>$1</mark>',
$text
);
}
echo highlight('PHP — это просто.', 'php');
// 'php' и 'PHP' будут обёрнуты в <mark>
По производительности: PCRE компилирует шаблон при каждом вызове, но в PHP внутренний кэш переиспользует уже скомпилированные шаблоны (см. pcre.recursion_limit и связанные настройки). Если в горячем цикле один и тот же шаблон применяется тысячи раз — производительность будет нормальной без дополнительных трюков. Однако для совсем простых задач (поиск подстроки, разбиение по одному символу) strpos и explode работают в разы быстрее regex — не используйте PCRE там, где хватит обычных строковых функций.
Итог: чеклист по работе с PCRE
- Используйте
u-флаг для всего, что касается UTF-8 (а это почти всегда). - Якоря
^и$для валидации — без них шаблон ловит подстроку в любой части ввода. - Именованные группы
(?P<name>...)читаемее нумерованных. - Для нескольких вхождений —
preg_match_allс флагомPREG_SET_ORDER. - Сложная замена с логикой —
preg_replace_callback. - Любой пользовательский ввод в шаблоне — через
preg_quote. - Жадные квантификаторы делайте ленивыми (
.*?) или сужайте через инверсию ([^>]+). - Не парсите HTML и JSON через regex — для этого есть
DOMDocument,json_decode,simplexml_load_string. - Тестируйте шаблоны на regex101.com с включённым PCRE2-режимом — там сразу видны группы, флаги и описание совпадений.
Регулярные выражения за пару часов разбора превращаются из загадочных закорючек в инструмент, который экономит десятки строк ручного парсинга. Главное — не пытаться запихнуть в один шаблон всё подряд: лучше два простых regex, чем один монструозный, который никто (включая автора через неделю) уже не прочитает.
