Регулярные выражения
| Основа | регулярные языки (Клини, 1951–1956) |
|---|---|
| Первые реализации | QED и ed (Кен Томпсон, 1968) |
| Диалекты | POSIX BRE/ERE, PCRE/Perl, JavaScript, Python re, RE2 |
| Движки | обратный поиск (backtracking), автоматный (Thompson NFA/DFA) |
| Риск | ReDoS — экспоненциальное время на «плохих» шаблонах |
| Инструменты | grep, sed, ripgrep, regex101 |
Регулярные выражения (англ. regular expressions, regex) — компактный язык шаблонов для описания множеств строк. С их помощью ищут подстроки, проверяют формат ввода, извлекают части текста и выполняют замены. Регулярные выражения встроены почти во все языки программирования, редакторы и утилиты командной строки, от grep и sed до IDE и СУБД.[1]
История
Математик Стивен Клини в 1950-х описал регулярные языки и нотацию для них. Кен Томпсон в 1968 году реализовал её в редакторе QED, а затем в ed, откуда появился grep (g/re/p — «глобально найти регулярное выражение и напечатать»). Позднее POSIX стандартизировал базовый и расширенный синтаксис, а Perl в 1990-х добавил множество расширений; библиотека PCRE сделала этот диалект стандартом де-факто.
Основной синтаксис
| Конструкция | Значение | Пример |
|---|---|---|
. |
любой символ (кроме перевода строки) | a.c → abc, a-c |
[abc], [a-z], [^0-9] |
класс символов, диапазон, отрицание | [А-Яа-яЁё] |
\d, \w, \s |
цифра, «словесный» символ, пробельный | \d{3} |
*, +, ? |
0 и больше, 1 и больше, 0 или 1 | colou?r |
{n}, {n,m} |
ровно n, от n до m повторений | \d{2,4} |
^, $ |
начало и конец строки | ^# |
\b |
граница слова | \bcat\b |
a|b |
альтернатива | jpe?g|png |
( ) |
группа с захватом | (\d+)-(\d+) |
(?:...) |
группа без захвата | |
(?P<name>...), (?<name>...) |
именованная группа | |
(?=...), (?!...), (?<=...) |
просмотр вперёд и назад | \d+(?= ₽) |
Квантификаторы по умолчанию жадные — захватывают как можно больше; *?, +? делают их ленивыми. Флаги меняют поведение: i — без учёта регистра, m — ^/$ для каждой строки, s — точка включает перевод строки, x — допускаются пробелы и комментарии в шаблоне.
Примеры в языках
import re
re.findall(r"\b\d{4}-\d{2}-\d{2}\b", "релизы 2024-05-01 и 2026-11-09")
# ['2024-05-01', '2026-11-09']
m = re.search(r"(?P<user>[\w.+-]+)@(?P<host>[\w.-]+)", "почта: ivan@example.com")
m.group("host") # 'example.com'
re.sub(r"\s+", " ", "много пробелов\n тут") # 'много пробелов тут'
const re = /(\d+)\s*₽/g;
[..."100 ₽ и 250₽".matchAll(re)].map(m => Number(m[1])); // [100, 250]
grep -E '^(ERROR|WARN)' app.log
rg -o 'v\d+\.\d+\.\d+' CHANGELOG.md
В Python шаблоны пишут «сырыми» строками r"...", чтобы обратные слеши не обрабатывались дважды; часто используемые выражения компилируют один раз через re.compile.
Диалекты и движки
Синтаксис отличается в деталях: в POSIX BRE (grep без -E) скобки и + нужно экранировать, \d в POSIX отсутствует (вместо него [[:digit:]]), а просмотр назад и обратные ссылки поддерживаются не везде.
Важнее различие движков:[3]
- Движки с обратным поиском (backtracking) — Perl, PCRE, Python
re, JavaScript, Java, .NET. Поддерживают обратные ссылки ((\w)\1) и просмотр, но на некоторых шаблонах работают экспоненциально долго. - Автоматные движки (NFA/DFA по Томпсону) — RE2, пакет
regexpв Go, крейтregexв Rust, ripgrep. Гарантируют время, линейное по длине входа, зато не поддерживают обратные ссылки и просмотр.[4]
ReDoS
Шаблон с вложенными квантификаторами, например ^(a+)+$, на строке aaaaaaaaaaaaaaaaaaaaaaaa! заставляет движок с обратным поиском перебрать экспоненциальное число вариантов. Если такой шаблон применяется к пользовательскому вводу, одна строка может занять процессор на минуты — это атака ReDoS (regular expression denial of service).[5] В июле 2019 года одно регулярное выражение в правилах WAF Cloudflare загрузило процессоры по всему миру и вызвало глобальный сбой на 27 минут.[6]
Защита: избегать вложенных и перекрывающихся квантификаторов ((a|a)*, (.*)*), ограничивать длину входа, задавать тайм-ауты, для недоверенных данных использовать линейные движки (RE2, Go, Rust), проверять шаблоны линтерами.
Unicode
\w и \d в Python 3 по умолчанию понимают Unicode: \d совпадёт и с арабско-индийскими цифрами, поэтому для строгой проверки лучше [0-9] или флаг re.ASCII. В JavaScript для корректной работы с Unicode нужен флаг u (или v), тогда доступны классы вида \p{L} (любая буква) и \p{Script=Cyrillic}. Требования к Unicode-регулярным выражениям описывает стандарт UTS #18.[7]
Когда не использовать
Регулярные выражения не умеют разбирать вложенные структуры произвольной глубины: HTML, JSON, языки программирования требуют настоящих парсеров. Для email и URL надёжнее библиотечные валидаторы и проверка подтверждением, чем «идеальная» регулярка на сотни символов. Длинные шаблоны стоит документировать флагом x и покрывать тестами; для отладки удобен сайт regex101, показывающий разбор и шаги движка. Языковые модели неплохо пишут и объясняют регулярные выражения, но их результат тоже нужно проверять на примерах.
См. также
Источники
- Python — re: Regular expression operations
- MDN — Regular expressions (JavaScript)
- Russ Cox — Regular Expression Matching Can Be Simple And Fast
- RE2 syntax
- OWASP — Regular expression Denial of Service (ReDoS)
- Cloudflare — Details of the Cloudflare outage on July 2, 2019
- Unicode Technical Standard #18 — Unicode Regular Expressions