Лекция 2. Регулярные выражения (Regular Expressions)¶
Регулярное выражение — это строка, задающая шаблон поиска подстрок в тексте. Одному шаблону может соответствовать много разных строк. Термин «Регулярные выражения» — перевод английского Regular expressions. Точнее было бы «шаблонные выражения».
Регулярное выражение состоит из обычных символов и специальных командных последовательностей. Например, \d задаёт любую цифру, \d+ — последовательность из одной или более цифр.
Работа с регулярками реализована во всех современных языках программирования. В Python используют модуль re, в Go — пакет regexp. Существует несколько «диалектов»: PCRE (Perl), POSIX, RE2 — функционал может различаться.
Go использует движок RE2: гарантирует линейное время выполнения, но не поддерживает backreferences и lookaround. Python использует PCRE-подобный движок — более выразительный, но в патологических случаях может работать экспоненциально медленно (ReDoS).
Примеры регулярных выражений¶
| Регулярка | Смысл |
|---|---|
simple text |
В точности текст «simple text» |
\d{5} |
Последовательности из 5 цифр |
\d\d/\d\d/\d{4} |
Даты в формате ДД/ММ/ГГГГ (и похожие куски) |
\b\w{3}\b |
Слова в точности из трёх букв |
[-+]?\d+ |
Целое число (со знаком), 7, +17, -42, 0013 |
[-+]?(?:\d+(?:\.\d*)?\|\.\d+)(?:[eE][-+]?\d+)? |
Действительное число, возможно в экспоненциальной записи |
Сила и ответственность¶
Регулярки — мощный инструмент. Но использовать их следует с умом и осторожностью, только там, где они действительно приносят пользу.
Плохо написанные регулярки работают медленно. Их сложно читать. Небольшое изменение задачи может потребовать существенной переработки шаблона. Поэтому про регулярки часто говорят, что это write-only code.
Шутят: «Некоторые люди, когда сталкиваются с проблемой, думают: ‘я знаю, я решу её с помощью регулярных выражений’. Теперь у них две проблемы.»
Антипример — write-only регулярка для валидации email (так делать НЕ нужно):
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}...
Если email вводит пользователь — пусть вводит почти что угодно (минимально проверьте @ и .), а реальную проверку сделайте отправкой подтверждающего письма.
Документация и инструменты¶
- Python
re: https://docs.python.org/3/library/re.html - Go
regexp: https://pkg.go.dev/regexp - Подробная документация: https://www.regular-expressions.info/
- Тонкости с примерами: https://www.rexegg.com/
- Онлайн-отладка (Python flavor): https://regex101.com
- Онлайн-визуализация: https://www.debuggex.com/
Основы синтаксиса¶
Любая обычная строка (без спецсимволов .^$*+?{}[]\|()) сама по себе является регулярным выражением.
Шаблоны, соответствующие одному символу¶
Обратите внимание:
\d ≈ [0-9],\D ≈ [^0-9],\w ≈ [0-9a-zA-Z_]плюс юникод-буквы и цифры. Буква «ё» в общий диапазон\wне включается явно — но\wеё ловит как «букву» в юникодном режиме.
Квантификаторы¶
| Шаблон | Описание | Пример | Применяем к |
|---|---|---|---|
{n} |
Ровно n повторений |
\d{4} |
1234 |
{m,n} |
От m до n |
\d{2,4} |
12, 123, 1234 |
{m,} |
Не менее m |
\d{3,} |
123, 1234, 12345 |
{,n} |
Не более n |
\d{,2} |
1, 12 |
? |
Ноль или одно ({0,1}) |
валы? |
вал, валы |
* |
Ноль или более ({0,}) |
СУ\d* |
СУ, СУ1, СУ12 |
+ |
Одно или более ({1,}) |
a\)+ |
a), a)) |
Жадность vs ленивость¶
По умолчанию квантификаторы жадные — захватывают максимально возможное число символов. Добавление ? делает их ленивыми:
| Жадный | Ленивый | Описание |
|---|---|---|
\(.*\) |
(a + b) * (c + d) | максимально длинная подстрока |
\(.*?\) |
(a + b) * (c + d) | минимально длинная |
Группы¶
(abc)— захватывающая группа, доступна как\1,\2, … в подстановке.(?:abc)— не-захватывающая группа (только группировка).(?P<name>abc)(Python) /(?P<name>abc)(Go) — именованная группа.(?=abc)— положительный lookahead (только Python, не Go!).(?!abc)— отрицательный lookahead (только Python, не Go!).
Регулярки в Python¶
Функции для работы — в модуле re. Основные:
| Функция | Назначение |
|---|---|
re.search(pattern, string) |
Первое вхождение шаблона |
re.fullmatch(pattern, string) |
Соответствует ли вся строка целиком |
re.match(pattern, string) |
Соответствует ли начало строки |
re.split(pattern, string, maxsplit=0) |
Разделение по шаблону |
re.findall(pattern, string) |
Все непересекающиеся вхождения |
re.finditer(pattern, string) |
Итератор по match-объектам |
re.sub(pattern, repl, string, count=0) |
Замена всех вхождений |
Пример:
import re
m = re.search(r"\d\d\D\d\d", "Телефон 123-12-12")
print(m[0] if m else "Not found") # 23-12
print(re.fullmatch(r"\d\d\D\d\d", "12-12") is not None) # True
print(re.split(r"\W+", "Где, скажите мне, мои очки??!"))
# ['Где', 'скажите', 'мне', 'мои', 'очки', '']
print(re.findall(r"\d\d\.\d\d\.\d{4}",
"Эта строка написана 19.01.2026, а могла бы и 01.09.2025"))
# ['19.01.2026', '01.09.2025']
for m in re.finditer(r"\d\d\.\d\d\.\d{4}", "19.01.2026 / 01.09.2025"):
print("Дата", m[0], "с позиции", m.start())
print(re.sub(r"\d\d\.\d\d\.\d{4}", "DD.MM.YYYY",
"19.01.2026 / 01.09.2025"))
# DD.MM.YYYY / DD.MM.YYYY
Экранирование и сырые строки¶
В Python символ \ экранирующий — для шаблона \n (буквально два символа) в обычной строке пришлось бы писать "\\n". Чтобы избежать нагромождения слешей, используют сырые строки с префиксом r:
r"\n"— два символа:\иn;"\n"— один символ перевода строки.
Шаблоны регулярных выражений в Python всегда пишут сырыми строками.
Флаги¶
Каждой функции можно передать flags:
| Флаг | Назначение |
|---|---|
re.ASCII |
\w, \d и т. п. соответствуют только ASCII (иначе — юникод). |
re.IGNORECASE |
Игнорировать регистр. |
re.MULTILINE |
^ и $ соответствуют началу/концу каждой строки. |
re.DOTALL |
Точка . соответствует и \n. |
re.VERBOSE |
Разрешает пробелы и комментарии в шаблоне. |
print(re.findall(r"\d+", "12 + ٦٧")) # ['12', '٦٧']
print(re.findall(r"\d+", "12 + ٦٧", flags=re.ASCII)) # ['12']
print(re.findall(r"[уеыаоэяию]+", "ОООО ааааа")) # ['ааааа']
print(re.findall(r"[уеыаоэяию]+", "ОООО ааааа", flags=re.IGNORECASE))
# ['ОООО', 'ааааа']
Компиляция шаблонов¶
Если один и тот же шаблон используется многократно — компилируйте его один раз:
Регулярки в Go (regexp)¶
package main
import (
"fmt"
"regexp"
)
func main() {
// MustCompile — panic при ошибке компиляции; используется для констант.
re := regexp.MustCompile(`\d{2}\.\d{2}\.\d{4}`)
text := "Эта строка написана 19.01.2026, а могла бы и 01.09.2025"
fmt.Println(re.FindString(text)) // 19.01.2026
fmt.Println(re.FindAllString(text, -1)) // [19.01.2026 01.09.2025]
fmt.Println(re.ReplaceAllString(text, "DD.MM.YYYY"))
// Группы
re2 := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
m := re2.FindStringSubmatch("Дата 2026-06-01")
fmt.Println(m) // [2026-06-01 2026 06 01]
// Именованные группы
re3 := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})`)
m3 := re3.FindStringSubmatch("2026-06")
for i, name := range re3.SubexpNames() {
if name != "" {
fmt.Println(name, "=", m3[i])
}
}
}
В Go нет lookahead/lookbehind и backreferences — это плата за гарантированно линейную сложность.
Сравнение Python re и Go regexp¶
| Возможность | Python re |
Go regexp |
|---|---|---|
| Поиск, замена, разбиение | ✅ | ✅ |
| Захватывающие и именованные группы | ✅ | ✅ |
| Lookahead / lookbehind | ✅ | ❌ |
Backreferences (\1) в шаблоне |
✅ | ❌ |
| Гарантированно линейное время | ❌ (возможен ReDoS) | ✅ |
| Скорость на простых шаблонах | средняя | высокая |
Эксперименты¶
Попробуйте сами:
- Найдите все натуральные числа в тексте (возможно, окружённые буквами).
- Найдите все слова, написанные капсом (заглавными), даже внутри слов (
аааБББввв). - Найдите слова, в которых есть русская буква, а когда-нибудь после неё — цифра.
- Найдите все натуральные числа, не находящиеся внутри слов.
- Найдите строки, в которых есть открывающая и когда-нибудь потом закрывающая скобка.
- Найдите пустые строки.
Контрольные вопросы¶
- Что такое регулярное выражение?
- Что такое жадные и ленивые квантификаторы?
- Зачем в Python нужны сырые строки (
r"...") для шаблонов? - Какие флаги
reвам известны? Когда применятьre.IGNORECASE,re.MULTILINE,re.DOTALL? - В чём отличие движка RE2 (Go) от PCRE (Python)?
- Почему
re.compile()лучше прямого вызоваre.search(), если шаблон используется многократно?