🐘 PHP

Expresiones regulares en PHP: guía detallada

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К вам приходит читатель и говорит: «Найдите мне все книги, где на 15-й странице есть номер телефона, начинающийся на +7, и имя автора заканчивается на "ов"». Без специальных инструментов вам пришлось бы открывать каждую книгу вручную и проверять — это заняло бы годы.

Регулярные выражения (Regular Expressions, или сокращённо regex) — это как волшебная лупа, которая умеет искать в тексте не просто конкретные слова, а шаблоны (паттерны). Вы говорите лупе: «Найди всё, что похоже на телефон», — и она мгновенно подсвечивает все номера в тексте.

В программировании регулярные выражения используются повсеместно:

  • Проверка, правильно ли пользователь ввёл email или номер телефона
  • Поиск всех ссылок на сайте
  • Замена «плохих» слов на звёздочки в комментариях
  • Извлечение данных из логов сервера
  • Валидация форм (не дать отправить пустое поле или поле с недопустимыми символами)
💡 Важно: Регулярные выражения — это не изобретение PHP. Это универсальный язык поиска, который существует во многих языках программирования (Python, JavaScript, Java, C# и даже в текстовых редакторах вроде VS Code).

Теория: как это работает изнутри

Давайте заглянем «под капот» регулярных выражений. Представьте, что регулярное выражение — это инструкция для робота-поисковика, который движется по тексту символ за символом.

Основные строительные блоки

1. Литералы (обычные символы)
Самый простой случай. Если вы напишете шаблон cat, то найдёте в тексте именно слово «cat». Робот просто ищет точное совпадение.

2. Метасимволы (специальные команды)
Это символы, которые не ищут сами себя, а дают команды. Вот главные из них:

Символ Значение Пример
. (точка) Любой один символ (кроме перевода строки) c.t найдёт «cat», «cot», «c3t»
* Предыдущий символ повторяется 0 или более раз ca*t найдёт «ct», «cat», «caat», «caaat»
+ Предыдущий символ повторяется 1 или более раз ca+t найдёт «cat», «caat», но не «ct»
? Предыдущий символ может быть, а может не быть (0 или 1 раз) colou?r найдёт и «color», и «colour»
^ Начало строки ^Привет найдёт только «Привет» в начале строки
$ Конец строки конец$ найдёт только «конец» в конце строки
d Любая цифра (0-9) ddd найдёт любые три цифры подряд
w Любая буква, цифра или знак подчёркивания w+ найдёт целое слово
s Любой пробельный символ (пробел, табуляция, перевод строки) s+ найдёт один или несколько пробелов

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

  • {n} — ровно n раз (например, d{3} — три цифры)
  • {n,} — n или более раз
  • {n,m} — от n до m раз

4. Классы символов (группировка вариантов)
Квадратные скобки [] позволяют указать набор допустимых символов:

  • [abc] — один из символов a, b или c
  • [a-z] — любая строчная буква латинского алфавита
  • [0-9] — любая цифра (то же самое, что d)
  • [^abc] — любой символ, кроме a, b или c (знак ^ внутри скобок означает отрицание)

5. Группы и альтернативы
Круглые скобки () выполняют две функции:

  • Группируют часть шаблона для применения квантификатора (например, (ab)+ найдёт «ab», «abab», «ababab»)
  • Запоминают найденное для последующего использования

Вертикальная черта | означает «или»: кот|собака найдёт либо «кот», либо «собака».

Как PHP обрабатывает регулярные выражения

В PHP есть два набора функций для работы с регулярными выражениями:

  • POSIX-стиль (устаревший, не используйте!) — функции ereg(), eregi()
  • PCRE-стиль (Perl Compatible Regular Expressions) — современный стандарт. Все функции начинаются с preg_: preg_match(), preg_match_all(), preg_replace(), preg_split()
⚠️ Важное замечание: В PHP регулярное выражение обязательно заключается в разделители. Чаще всего используют косую черту /, но можно и другие символы: #, ~, !. Пример: /шаблон/ или #шаблон#.

Синтаксис в PHP: все формы и варианты

Давайте разберём все основные функции PHP для работы с регулярными выражениями. Я покажу их в виде удобной таблицы-шпаргалки.

Основные функции PCRE

Функция Что делает Возвращает
preg_match($pattern, $subject, &$matches) Проверяет, есть ли в строке хотя бы одно совпадение с шаблоном 1 — если есть совпадение, 0 — если нет, false — при ошибке
preg_match_all($pattern, $subject, &$matches) Находит все совпадения с шаблоном в строке Количество найденных совпадений (может быть 0)
preg_replace($pattern, $replacement, $subject) Заменяет все найденные совпадения на указанный текст Изменённую строку
preg_split($pattern, $subject) Разбивает строку на массив по указанному шаблону (как explode, но мощнее) Массив строк
preg_grep($pattern, $array) Фильтрует массив, оставляя только элементы, совпадающие с шаблоном Отфильтрованный массив

Модификаторы (флаги) — меняют поведение поиска

Модификаторы ставятся после закрывающего разделителя. Например: /шаблон/i — регистронезависимый поиск.

Модификатор Описание
i Регистронезависимый поиск (не различает заглавные и строчные буквы)
m Многострочный режим. Символы ^ и $ начинают работать для каждой строки, а не для всего текста
s Точка . начинает совпадать с любым символом, включая перевод строки
u Включает поддержку UTF-8 (обязательно для работы с кириллицей!)
x Разрешает пробелы и комментарии внутри шаблона (для читаемости)
U Ленивый режим (квантификаторы ищут минимальное совпадение, а не максимальное)
🔥 Совет: При работе с русским языком ВСЕГДА добавляйте модификатор u. Иначе регулярка будет воспринимать кириллицу как набор странных символов. Пример: /[а-яё]+/u — найдёт русское слово.

Примеры кода — от простого к сложному

Теперь самое интересное — практика. Каждый пример я разберу построчно.

Пример 1: Проверка, есть ли в строке цифры

<?php
// Создаём строку для проверки
$text = "Мой номер телефона: 123-456-789";

// Шаблон: d — любая цифра, + — одна или более подряд
$pattern = '/d+/';

// Выполняем поиск
$result = preg_match($pattern, $text);

// preg_match возвращает 1 (истина) или 0 (ложь)
if ($result) {
    echo "✅ В тексте есть цифры!";
} else {
    echo "❌ Цифр не найдено";
}
// Выведет: ✅ В тексте есть цифры!
?>

Пример 2: Извлечение всех email-адресов из текста

<?php
$text = "Свяжитесь с нами: support@example.com или admin@test.org. 
         Также пишите на info@company.net";

// Шаблон для email:
// [w.-]+ — имя пользователя (буквы, цифры, точки, дефисы)
// @ — собачка
// [w.-]+ — домен
// . — точка (экранируем, так как точка — метасимвол)
// [a-z]{2,} — доменная зона (ru, com, org и т.д.)
$pattern = '/[w.-]+@[w.-]+.w{2,}/';

// Ищем ВСЕ совпадения
$count = preg_match_all($pattern, $text, $matches);

echo "Найдено email-адресов: $count
";
print_r($matches[0]);
/*
Выведет:
Array
(
    [0] => support@example.com
    [1] => admin@test.org
    [2] => info@company.net
)
*/
?>

Пример 3: Замена всех ссылок на кликабельные HTML-ссылки

<?php
$text = "Посетите наш сайт https://example.com и страницу http://test.org/about";

// Шаблон для URL:
// https?:// — протокол (s может не быть)
// [^s]+ — любые непробельные символы (сам адрес)
$pattern = '/https?://[^s]+/';

// Заменяем найденные URL на HTML-ссылку
// $0 — это специальная переменная, которая содержит всё найденное совпадение
$replacement = '$0';

$newText = preg_replace($pattern, $replacement, $text);

echo $newText;
// Выведет: Посетите наш сайт https://example.com и страницу http://test.org/about
?>

Пример 4: Валидация номера телефона в формате +7 (XXX) XXX-XX-XX

<?php
function validatePhone($phone) {
    // Шаблон для российского номера:
    // ^ — начало строки
    // +7 — символ + и цифра 7
    // s? — необязательный пробел
    // ( — открывающая скобка (экранируем)
    // d{3} — ровно 3 цифры
    // ) — закрывающая скобка
    // s? — необязательный пробел
    // d{3} — 3 цифры
    // - — дефис
    // d{2} — 2 цифры
    // - — дефис
    // d{2} — 2 цифры
    // $ — конец строки
    $pattern = '/^+7s?(d{3})s?d{3}-d{2}-d{2}$/';
    
    if (preg_match($pattern, $phone)) {
        return "✅ Номер корректен";
    } else {
        return "❌ Неверный формат номера";
    }
}

echo validatePhone("+7 (123) 456-78-90"); // ✅
echo "
";
echo validatePhone("8-123-456-78-90");    // ❌ (нет +7 и скобок)
?>

Пример 5: Разбор лога сервера на части

<?php
$logLine = "192.168.1.1 - - [10/Oct/2023:13:55:36 +0000] "GET /index.php HTTP/1.1" 200 2326";

// Шаблон для извлечения IP, даты, метода и статуса
// Используем группы (скобки) для захвата отдельных частей
$pattern = '/^(S+) .* [(.*?)] "(w+) .*" (d+) /';

preg_match($pattern, $logLine, $matches);

echo "IP-адрес: " . $matches[1] . "
";      // 192.168.1.1
echo "Дата: " . $matches[2] . "
";          // 10/Oct/2023:13:55:36 +0000
echo "Метод: " . $matches[3] . "
";         // GET
echo "Статус: " . $matches[4] . "
";        // 200

// Объяснение шаблона:
// ^(S+) — захватываем первые непробельные символы (IP)
// .* — пропускаем всё до...
// [(.*?)] — захватываем содержимое квадратных скобок (дата)
// "(w+) — захватываем слово после кавычки (метод HTTP)
// .*" — пропускаем остаток до кавычки
// (d+) — захватываем число (код статуса)
?>

Пример 6: Фильтрация массива — оставить только email

<?php
$data = [
    "user@example.com",
    "not_an_email",
    "admin@site.org",
    "12345",
    "test@test.com"
];

// Шаблон для проверки, что строка — это email
$pattern = '/^[w.-]+@[w.-]+.w{2,}$/';

// preg_grep возвращает только те элементы, которые совпадают с шаблоном
$validEmails = preg_grep($pattern, $data);

print_r($validEmails);
/*
Array
(
    [0] => user@example.com
    [2] => admin@site.org
    [4] => test@test.com
)
*/
?>

Пример 7: Сложная замена — цензура с сохранением первой буквы

<?php
$text = "Этот дурак написал плохой отзыв, а этот болван вообще ничего не понял.";

// Список плохих слов
$badWords = ['дурак', 'болван', 'плохой'];

// Для каждого плохого слова создаём шаблон и замену
foreach ($badWords as $word) {
    // Шаблон: слово целиком (границы слова )
    // Захватываем первую букву в группу
    $pattern = '/(' . $word[0] . ')' . substr($word, 1) . '/u';
    
    // Замена: первая буква ($1) + столько звёздочек, сколько букв во второй части
    $replacement = '$1' . str_repeat('*', mb_strlen($word) - 1);
    
    $text = preg_replace($pattern, $replacement, $text);
}

echo $text;
// Выведет: Этот д**** написал п***** отзыв, а этот б***** вообще ничего не понял.
?>

Пример 8: Разделение строки с сохранением разделителей

<?php
$sentence = "Привет! Как дела? Я изучаю PHP. А ты?";

// Разбиваем по знакам препинания, но сохраняем их
// Используем PREG_SPLIT_DELIM_CAPTURE — он сохраняет то, что в скобках
$parts = preg_split('/([!?.])/u', $sentence, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

print_r($parts);
/*
Array
(
    [0] => Привет
    [1] => !
    [2] =>  Как дела
    [3] => ?
    [4] =>  Я изучаю PHP
    [5] => .
    [6] =>  А ты
    [7] => ?
)
*/
?>

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python или планируете его изучать, вот ключевые различия в работе с регулярными выражениями:

Характеристика PHP (PCRE) Python (re)
Импорт модуля Не требуется (встроено) import re
Разделители шаблона Обязательны: /шаблон/ Не нужны: "шаблон"
Поиск первого совпадения preg_match() re.search()
Поиск всех совпадений preg_match_all() re.findall()
Замена preg_replace() re.sub()
Разделение строки preg_split() re.split()
Регистронезависимость Модификатор i: /шаблон/i Флаг re.IGNORECASE: re.search(..., flags=re.I)
Работа с Unicode Модификатор u По умолчанию поддерживает Unicode
Именованные группы (?P<name>шаблон) (?P<name>шаблон) — синтаксис одинаковый
Производительность Высокая (PCRE — один из самых быстрых движков) Хорошая, но чуть медленнее PCRE на сложных шаблонах
💡 Главное отличие для новичка: В PHP вы пишете preg_match('/шаблон/i', $text), а в Python — re.search('шаблон', text, re.I). Суть одна, синтаксис разный.

Типичные ошибки и подводные камни

Даже опытные программисты иногда спотыкаются на регулярках. Вот список самых частых ошибок:

Ошибка 1: Забыли про разделители

// ❌ Неправильно — нет разделителей
preg_match('d+', $text);

// ✅ Правильно
preg_match('/d+/', $text);

Ошибка 2: Не экранировали спецсимволы

// ❌ Неправильно — точка означает "любой символ"
preg_match('/example.com/', $text);  // Найдёт "exampleXcom"

// ✅ Правильно — экранируем точку
preg_match('/example.com/', $text);

Список символов, которые нужно экранировать (ставить перед ними обратную косую черту): . + * ? [ ^ ] $ ( ) { } = ! < > | : -

Ошибка 3: Жадность квантификаторов

$text = "<b>Жирный</b> и <b>тоже жирный</b>";

// ❌ Жадный поиск — захватит всё от первого < до последнего >
preg_match('/<b>.*</b>/', $text, $m);
echo $m[0]; // <b>Жирный</b> и <b>тоже жирный</b>

// ✅ Ленивый поиск — остановится на первом закрывающем теге
preg_match('/<b>.*?</b>/', $text, $m);
echo $m[0]; // <b>Жирный</b>

Ошибка 4: Проблемы с кириллицей

// ❌ Неправильно — без модификатора u
preg_match('/[а-я]+/', 'привет');  // Может не сработать

// ✅ Правильно — добавляем u
preg_match('/[а-яё]+/u', 'привет');

Ошибка 5: Путаница между preg_match и preg_match_all

$text = "a b c";

// preg_match найдёт ТОЛЬКО первое совпадение
preg_match('/w/', $text, $m);
print_r($m[0]); // 'a'

// preg_match_all найдёт ВСЕ совпадения
preg_match_all('/w/', $text, $m);
print_r($m[0]); // ['a', 'b', 'c']

Ошибка 6: Забыли про якоря ^ и $ при валидации

// ❌ Найдёт "123" даже в строке "abc123def"
preg_match('/d+/', 'abc123def');  // true

// ✅ Найдёт только если вся строка состоит из цифр
preg_match('/^d+$/', 'abc123def'); // false
preg_match('/^d+$/', '123');       // true

Лучшие практики и советы профессионалов

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

1. Начинайте с простого и тестируйте

Не пытайтесь написать идеальное регулярное выражение с первого раза. Начните с простого шаблона, проверьте его на тестовых данных, потом постепенно усложняйте. Используйте онлайн-тестеры вроде regex101.com — они подсвечивают совпадения и объясняют, что делает каждый символ.

2. Используйте именованные группы для сложных шаблонов

// Вместо безликих $matches[1], $matches[2]...
$pattern = '/(?P<year>d{4})-(?P<month>d{2})-(?P<day>d{2})/';
preg_match($pattern, '2023-10-15', $matches);
echo $matches['year'];  // 2023
echo $matches['month']; // 10
echo $matches['day'];   // 15

3. Не изобретайте велосипед для сложных валидаций

Для проверки email, URL, IP-адресов есть готовые функции PHP:

  • filter_var($email, FILTER_VALIDATE_EMAIL) — проверка email
  • filter_var($url, FILTER_VALIDATE_URL) — проверка URL
  • filter_var($ip, FILTER_VALIDATE_IP) — проверка IP

Регулярные выражения для этих задач могут быть очень сложными и содержать ошибки. Доверьтесь встроенным функциям.

4. Комментируйте сложные регулярки

// Используем модификатор x для пробелов и комментариев
$pattern = '/^
    +7            # Код страны
    s?            # Необязательный пробел
    (d{3})      # Код города в скобках
    s?            # Необязательный пробел
    d{3}-d{2}-d{2}  # Основной номер
$/x';

preg_match($pattern, '+7 (123) 456-78-90', $matches);

5. Ограничивайте длину строки перед обработкой

Регулярные выражения могут работать медленно на очень длинных строках (особенно с жадными квантификаторами). Если вы ожидаете строки длиннее 10 000 символов, подумайте об ограничении:

$text = substr($text, 0, 5000); // Берём только первые 5000 символов

6. Используйте PREG_OFFSET_CAPTURE для получения позиции совпадения

$text = "Найти слово в этом предложении";
preg_match('/слово/', $text, $matches, PREG_OFFSET_CAPTURE);
echo "Найдено на позиции: " . $matches[0][1]; // Выведет: 10

Вопросы для самопроверки

Проверьте, как хорошо вы усвоили материал. Ответы спрятаны под спойлером — попробуйте ответить сначала сами!

  1. Что такое разделители в PHP-регулярках и зачем они нужны?
  2. Какая функция найдёт ВСЕ совпадения с шаблоном в строке?
  3. Как сделать поиск регистронезависимым?
  4. Что означает шаблон /^[A-Z]d{3}$/?
  5. Чем отличается .* от .*??
  6. Какой модификатор обязателен при работе с кириллицей?
  7. Что вернёт preg_match('/d+/', 'abc')?
  8. Как в шаблоне обозначить группу, которая запоминает найденное?
  9. Какая функция заменит все совпадения на другой текст?
  10. Почему для валидации email лучше использовать filter_var(), а не регулярку?
📝 Показать ответ