Почему регулярное выражение PHP некорректно работает с длинными доменными именами
Узнайте причины катастрофического возврата в регулярных выражениях PHP при проверке доменных имен с длинными TLD и эффективные решения проблемы.
Почему регулярное выражение в PHP некорректно работает с доменными именами, содержащими 19 и более символов в домене верхнего уровня? Я написал регулярное выражение для проверки доменного имени: $reg = "#^(([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)*[a-zA-Z0-9]{2,}$#"; preg_match($reg, $domain, $res);. Регулярное выражение корректно работает для доменных имен с доменом верхнего уровня до 18 символов, но при добавлении 19-го символа возвращает false. Примеры: 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.1.rusreasreasreasreasssreasreasreasreasss.reasreasreasreasss (true), rusreasreasreasreasssreasreasreasreasss.reasreasreasreasss (true), reasreasreasreasss (true), но reasreasreasreassss (false). В чем причина такого поведения регулярного выражения и как это исправить?
Ваше регулярное выражение сталкивается с проблемой катастрофического возврата (catastrophic backtracking) при работе с длинными доменными именами. Это происходит из-за вложенных квантификаторов в вашем шаблоне, которые создают экспоненциальное количество возможных комбинаций для проверки.
Содержание
- Анализ проблемы
- Причина некорректной работы
- Решение проблемы
- Альтернативные подходы
- Оптимизация производительности
Анализ проблемы
Ваше регулярное выражение:
$reg = "#^(([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)*[a-zA-Z0-9]{2,}$#";
Имеет структуру, которая вызывает проблемы при обработке длинных строк:
- Вложенные квантификаторы
(([a-zA-Z0-9]+-?)*[a-zA-Z0-9]\.)* - Каждый
*означает “ноль или больше раз” - При обработке длинных доменов возникает комбинаторный взрыв
Причина некорректной работы
Как видно из документации PHP, PHP имеет ограничения на возврат (backtrack limit) и рекурсию для PCRE:
When you use preg_match() for security purpose or huge data processing, maybe you should make consideration for backtrack_limit and recursion_limit.
Ваше регулярное выражение сталкивается с проблемой:
- Катастрофический возврат - при анализе длинных доменов движок регулярных выражений пытается все возможные комбинации
- Превышение лимита - по умолчанию
pcre.backtrack_limitравен 100000, и при обработке 19+ символьных доменов этот лимит превышается - “Fail silently” - PHP возвращает
falseвместо исключения, когда достигает лимита
Ваш пример reasreasreasreassss (16 символов) возвращает false, потому что при обработке:
- Движок пытается все возможные комбинации символов
- Каждая позиция создает новые ветви для проверки
- Комбинаторика растет экспоненциально
Решение проблемы
1. Упрощение регулярного выражения
Замените сложный шаблон на более эффективный:
$reg = "#^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$#";
Этот шаблон:
- Использует некаптурные группы
(?:...) - Ограничивает длину каждого поддомена 63 символами
- Избегает вложенных квантификеров
2. Разделение на части
Разделите проверку на более простые шаги:
function validateDomain($domain) {
// Проверка основной структуры
if (!preg_match('/^[a-zA-Z0-9.-]+$/', $domain)) {
return false;
}
// Проверка длинны домена
if (strlen($domain) > 253) {
return false;
}
// Проверка TLD отдельно
$parts = explode('.', $domain);
$tld = end($parts);
if (strlen($tld) < 2 || strlen($tld) > 63) {
return false;
}
return true;
}
3. Использование функций PHP
Для более надежной проверки используйте встроенные функции:
function isValidDomain($domain) {
$domain = idn_to_ascii($domain);
return filter_var($domain, FILTER_VALIDATE_DOMAIN) !== false;
}
Альтернативные подходы
1. Использование DNS-проверки
function domainExists($domain) {
// Проверка существования домена через DNS
return checkdnsrr($domain, 'A') || checkdnsrr($domain, 'MX');
}
2. Использование специализированных библиотек
Рассмотрите использование библиотеки:
// Использование Respect/Validation
use Respect\Validation\Validator as v;
$validator = v::domain()->noWhitespace();
$result = $validator->validate($domain);
3. Разбор URL с помощью PHP
function parseDomain($url) {
$parsed = parse_url($url);
if (isset($parsed['host'])) {
return $parsed['host'];
}
return false;
}
Оптимизация производительности
1. Увеличение лимитов
Если вам необходимо работать с очень длинными доменами:
// Установка лимитов перед проверкой
ini_set('pcre.backtrack_limit', 1000000);
ini_set('pcre.recursion_limit', 1000000);
2. Кеширование результатов
$domainCache = [];
function validateDomainCached($domain) {
global $domainCache;
if (isset($domainCache[$domain])) {
return $domainCache[$domain];
}
$result = isValidDomain($domain);
$domainCache[$domain] = $result;
return $result;
}
3. Использование более быстрых функций
Для простых проверок используйте strpos() или explode() вместо сложных регулярных выражений:
function hasValidTld($domain) {
$parts = explode('.', $domain);
$tld = end($parts);
return strlen($tld) >= 2 && strlen($tld) <= 63;
}
Источники
- PHP: preg_match - Manual
- PHP backtrack limit explanation
- Catastrophic backtracking in regex
- Domain name validation best practices
Заключение
Проблема с вашим регулярным выражением заключается в катастрофическом возврате при обработке длинных доменных имен. Для решения проблемы:
- Упростите регулярное выражение, избегая вложенных квантификаторов
- Используйте встроенные функции PHP для валидации доменов
- Разделите сложную проверку на несколько простых шагов
- Рассмотрите специализированные библиотеки для доменной валидации
- При необходимости увеличьте лимиты PCRE, но помните о производительности
Оптимальным решением будет использование filter_var() с FILTER_VALIDATE_DOMAIN для надежной и производительной проверки доменных имен.