Регулярные выражения PHP: от основ до продвинутых техник
Регулярные выражения в PHP: обзор и эффективное решение
Регулярные выражения (regex) в PHP реализованы через библиотеку PCRE (Perl Compatible Regular Expressions). Они позволяют выполнять поиск, замену, разбиение строк и проверку шаблонов. В качестве наиболее эффективного и универсального подхода рекомендуется использовать функцию preg_match_all() с модификаторами - она подходит для большинства задач извлечения данных и сложного поиска.
Основное эффективное решение: preg_match_all с модификаторами
Как найти все вхождения подстроки по шаблону в строке?
$str = 'Код: 123, код: 456, и ещё код: 789';
$pattern = '/код: (\d+)/iu';
if (preg_match_all($pattern, $str, $matches)) {
print_r($matches[1]);
} else {
echo 'Совпадений нет'; }
Array
(
[0] => 123
[1] => 456
[2] => 789
)
Пояснение: Модификатор i делает поиск регистронезависимым, u включает поддержку UTF-8. Шаблон (\d+) захватывает последовательность цифр. Функция возвращает все совпадения в массив $matches, где индекс 1 соответствует первой захваченной группе.
Типичная ошибка: забыть экранировать слэши в шаблоне. Если разделитель - слэш, то внутри шаблона его нужно экранировать (\/) или использовать другой разделитель (например, #).
Проблема: несоответствие кодировки. Без модификатора u русские буквы могут обрабатываться некорректно. Решение - всегда добавлять u для строк в UTF-8.
Вариант 1: preg_match - первое совпадение
Как проверить, содержит ли строка заданный шаблон, и получить первое вхождение?
$email = 'user@example.com';
if (preg_match('/^[\w.-]+@[\w.-]+\.\w{2,}$/', $email)) {
echo 'Email корректен';
} else {
echo 'Неверный формат'; }
Функция preg_match() возвращает 1 при совпадении, 0 при отсутствии, false при ошибке. Полезна для валидации.
Типичная ошибка: использование preg_match для поиска всех совпадений - для этого нужна preg_match_all. Также часто забывают проверять возвращаемое значение на false при ошибках регулярки.
Вариант 2: preg_replace - замена по шаблону
Как заменить все вхождения одного шаблона на другой текст?
$text = 'Цена: 100 руб, 200 руб, 300 руб';
$result = preg_replace('/(\d+)\s*руб/u', '$1 USD', $text);
echo $result;
Цена: 100 USD, 200 USD, 300 USD
В шаблоне $1 обозначает первую захваченную группу. Для обратных ссылок можно также использовать \1. Функция может принимать массивы для выполнения нескольких замен.
Ошибка: неправильное экранирование символа доллара в строке замены. Если нужно вставить буквально $1, используйте \$1 (экранированный слэш).
Вариант 3: preg_split - разбиение строки по шаблону
Как разбить текст на части, используя сложный разделитель (например, несколько пробелов или знаки препинания)?
$sentence = 'Привет, мир! Как дела?';
$words = preg_split('/[\s,?!.]+/u', $sentence, -1, PREG_SPLIT_NO_EMPTY);
print_r($words);
Array
(
[0] => Привет
[1] => мир
[2] => Как
[3] => дела
)
Флаг PREG_SPLIT_NO_EMPTY исключает пустые элементы. Полезна для парсинга логов, CSV, URL.
Проблема: разбиение по строке, а не по регулярке (тогда лучше explode). preg_split может работать медленнее на простых разделителях.
Вариант 4: preg_grep - фильтрация массива по регулярке
Как отфильтровать массив, оставив только элементы, соответствующие шаблону?
$files = ['img1.jpg', 'doc.pdf', 'image2.jpg', 'readme.txt'];
$jpegs = preg_grep('/\.jpg$/i', $files);
print_r($jpegs);
Array
(
[0] => img1.jpg
[2] => image2.jpg
)
Функция возвращает массив с совпадающими элементами (ключи сохраняются). Третий аргумент PREG_GREP_INVERT инвертирует поиск. Используется для обработки списков.
Ошибка: передача не строкового массива - preg_grep работает только со строковыми элементами.
Вариант 5: preg_filter - замена с фильтрацией массива
Как выполнить замену во всех элементах массива, соответствующих шаблону, и вернуть только изменённые?
$data = ['test', 'data123', '123456', 'end'];
$filtered = preg_filter('/^(\d+)$/', 'число: $1', $data);
print_r($filtered);
Array
(
[2] => число: 123456
)
Отличие от preg_replace: preg_filter возвращает только те элементы, в которых была произведена замена. Удобно для выборочной обработки.
Нюанс: если замена не применяется ни к одному элементу, возвращается пустой массив, а не null.
Вариант 6: preg_replace_callback - замена с использованием пользовательской функции
Как выполнить сложную замену, где нужно обработать каждое совпадение каким-либо алгоритмом?
$str = 'Дата: 2024-03-15, следующая: 2025-01-10';
$result = preg_replace_callback(
'/(\d{4})-(\d{2})-(\d{2})/',
function ($m) {
return $m[3] . '.' . $m[2] . '.' . $m[1];
},
$str
);
echo $result;
Дата: 15.03.2024, следующая: 10.01.2025
В callback передаётся массив совпадений, можно выполнить любую логику. Полезно для переформатирования, преобразования данных.
Ошибка: забыть возвращать строку из callback - приведёт к ошибке или некорректному результату.
Производительность: использование анонимной функции с замыканием может быть медленнее, чем простой preg_replace, но даёт гибкость.
Общие рекомендации и частые проблемы
При работе с регулярными выражениями в PHP полезно помнить следующие моменты:
- Всегда используйте разделители (например, /, #, ~). Если внутри шаблона много слэшей, выбирайте # или ~.
- При работе с UTF-8 добавляйте модификатор u.
- Избегайте backtrack limit - слишком сложные регулярки с большим количеством вложенных квантификаторов могут вызывать ошибку PREG_BACKTRACK_LIMIT_ERROR. В таких случаях упрощайте шаблон или увеличивайте лимит через ini_set('pcre.backtrack_limit', ...).
- Проверяйте результат функций на false, особенно при работе с некорректными шаблонами (ошибка компиляции).
- Используйте preg_last_error() для диагностики ошибок.
Расширенные примеры регулярных выражений в PHP
Пример 1. Именованные группы и извлечение данных из URL
$url = 'https://example.com/page/42?ref=social';
$pattern = '#^(?P<scheme>https?)://(?P<host>[^/]+)(?P<path>/[^?]*)?(?:\?(?P<query>.*))?$#';
preg_match($pattern, $url, $matches);
echo 'Схема: ' . $matches['scheme'] . '\n';
echo 'Хост: ' . $matches['host'] . '\n';
echo 'Путь: ' . ($matches['path'] ?? '') . '\n';
echo 'Query: ' . ($matches['query'] ?? '');
Схема: https Хост: example.com Путь: /page/42 Query: ref=social
Именованные группы (?P<name>...) делают код читаемее. Обращение к ним по ключам вместо числовых индексов.
Пример 2. Lookahead и Lookbehind: поиск, но не включение в результат
$text = 'Стоимость: 500 руб, скидка 10%';
// Найти числа, после которых идёт "руб" (позитивный lookahead)
preg_match_all('/\d+(?=\s*руб)/u', $text, $prices);
print_r($prices[0]);
// Найти числа, перед которыми идёт "скидка " (позитивный lookbehind)
preg_match_all('/(?<=скидка\s)\d+/u', $text, $discounts);
print_r($discounts[0]);
Array
(
[0] => 500
)
Array
(
[0] => 10
)
Используйте lookahead (?=...) и lookbehind (?<=...) для сопоставления шаблона без захвата. Отрицательные варианты - (?!...) и (?<!...).
Пример 3. Рекурсивные шаблоны: парсинг вложенных скобок
$text = 'a(b(c)d)e';
$pattern = '/\((?:[^()]|(?R))*\)/';
preg_match_all($pattern, $text, $inner);
print_r($inner[0]);
Array
(
[0] => (b(c)d)
)
(?R) - рекурсия на весь шаблон, позволяет обрабатывать вложенные структуры. Ограничение: может быть ресурсоёмко.
Пример 4. Модификаторы: s, m, x, u, i
$multi = "Первая строка\nвторая строка\nТретья";
// Модификатор m (multiline) - ^ и $ работают по каждой строке
preg_match_all('/^\w+/m', $multi, $words_m);
print_r($words_m[0]);
// Модификатор s (dotall) - точка включает \n
preg_match('/Первая.+/s', $multi, $line);
echo $line[0];
// Модификатор x - игнорирует пробелы в шаблоне (для читаемости)
$pattern = '/ ^
\w+ # слово
\s* # пробелы
/x';
preg_match($pattern, $multi, $m);
print_r($m);
Array
(
[0] => Первая
[1] => вторая
[2] => Третья
)
Первая строка
вторая строка
Третья
Array
(
[0] => Первая
)
Комбинируйте модификаторы: /uis, /mix и т.д. Внимание: модификатор x изменяет синтаксис - нужны комментарии #.
Пример 5. Работа с UTF-8 и юникодными свойствами
$text_ru = 'Привет, мир! 123.';
// Найти все буквы (включая русские) с помощью \p{L}
preg_match_all('/\p{L}+/u', $text_ru, $letters);
print_r($letters[0]);
// Найти все цифры
preg_match_all('/\p{N}+/u', $text_ru, $digits);
print_r($digits[0]);
// Удалить всё, кроме букв
$only_letters = preg_replace('/\P{L}+/u', ' ', $text_ru);
echo trim($only_letters);
Array
(
[0] => Привет
[1] => мир
)
Array
(
[0] => 123
)
Привет мир
\p{...} обозначает категорию Unicode (L - буквы, N - цифры, Z - пробелы и т.д.). \P{...} - отрицание.
Пример 6. Обратные ссылки и модификаторы insensivity
$string = 'abacaba';
// Поиск повторяющихся подряд букв (захват группы и обратная ссылка)
preg_match_all('/(\w)\1/', $string, $doubles);
print_r($doubles[0]);
// Поиск палиндрома (более сложная рекурсия)
$palindrome = 'level';
$pattern = '/^((.)(?1)\2|.?)$/';
if (preg_match($pattern, $palindrome)) {
echo "Да, это палиндром";
}
Array
(
[0] => aa
[1] => bb
)
Да, это палиндром
Обратная ссылка \1 ссылается на первую захваченную группу. Рекурсия (?1) вызывает первую группу заново. Палиндромный шаблон сложен для понимания, но демонстрирует мощь PCRE.
Пример 7. preg_replace_callback с анонимной функцией и статической переменной
$text = 'id:5, id:23, id:7';
$result = preg_replace_callback(
'/id:(\d+)/',
function ($m) {
static $counter = 0;
$counter++;
return 'new_id:' . ($m[1] + $counter * 10);
},
$text
);
echo $result;
new_id:15, new_id:33, new_id:27
Статическая переменная сохраняет состояние между вызовами callback. Полезно для нумерации или преобразования с контекстом.
Пример 8. Использование preg_filter для выборочной замены и сохранения порядка
$data = ['apple', 'banana', 'cherry', 'avocado'];
// Оставить только те фрукты, которые начинаются на 'a', и удвоить их
$filtered = preg_filter('/^a(.+)/', 'a$1a', $data);
print_r($filtered);
// Исходный массив не изменился, а $filtered содержит только изменённые элементы
print_r($data);
Array
(
[0] => aapplea
[3] => aavocadoa
)
Array
(
[0] => apple
[1] => banana
[2] => cherry
[3] => avocado
)
preg_filter возвращает только те ключи, которые были изменены. Остальные элементы исходного массива игнорируются.
Пример 9. Обработка ошибок PCRE с помощью preg_last_error
// Создадим шаблон с превышением backtrack limit (очень сложный)
$bad = str_repeat('(a?)', 100);
$subject = str_repeat('a', 100);
$result = preg_replace($bad, '', $subject);
if ($result === null) {
$err = preg_last_error();
switch ($err) {
case PREG_BACKTRACK_LIMIT_ERROR:
echo 'Ошибка: превышен лимит backtrack';
break;
case PREG_INTERNAL_ERROR:
echo 'Внутренняя ошибка PCRE';
break;
default:
echo 'Неизвестная ошибка';
}
}
Ошибка: превышен лимит backtrack
preg_last_error() возвращает код ошибки после неудачного выполнения. Применяйте для отладки сложных регулярных выражений.
Пример 10. Использование PREG_OFFSET_CAPTURE для получения позиции совпадения
$str = 'The quick brown fox jumps over the lazy dog.';
preg_match('/\b\w{5}\b/', $str, $matches, PREG_OFFSET_CAPTURE);
print_r($matches[0]);
echo "Найдено: '" . $matches[0][0] . "' на позиции " . $matches[0][1];
Array
(
[0] => quick
[1] => 4
)
Найдено: 'quick' на позиции 4
Флаг PREG_OFFSET_CAPTURE добавляет к каждому элементу массива смещение в байтах. Удобно для замен с учётом контекста.