Перейти к содержанию

Лекция 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 вводит пользователь — пусть вводит почти что угодно (минимально проверьте @ и .), а реальную проверку сделайте отправкой подтверждающего письма.

Документация и инструменты

Основы синтаксиса

Любая обычная строка (без спецсимволов .^$*+?{}[]\|()) сама по себе является регулярным выражением.

Шаблоны, соответствующие одному символу

Шаблон Описание Пример Применяем к тексту
. Любой символ, кроме \n м.л.ко молоко, малако, …м0л0ко
\d Любая цифра СУ\d\d СУ35, СУ11
\D Не-цифра 926\D123 926)123, 926-123
\s Пробельный символ бор\sода бор ода
\S Не-пробельный \S123 X123, я123
\w Буква, цифра или _ \w\w\w Год, f_3, qwe
\W Не-буква, не-цифра, не-_ сом\W сом!, сом?
[..] Один из символов в скобках или диапазон [0-9A-Fa-f] F, 9, b
[^..] Любой кроме перечисленных <[^>]> , <1>
\b Граница слова (позиция, не символ) \bвал вал, перевал, Перевал-ка
\B НЕ-граница слова \Bвал\B перевалка

Обратите внимание: \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))
# ['ОООО', 'ааааа']

Компиляция шаблонов

Если один и тот же шаблон используется многократно — компилируйте его один раз:

date_re = re.compile(r"\d\d\.\d\d\.\d{4}")
for m in date_re.finditer(text):
    process(m[0])

Регулярки в 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(), если шаблон используется многократно?