📦 Репозиторий занятия 60

Урок 60. Регулярные выражения

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
PythonИсполняемый пример44 строк

Знакомство с регулярными выражениями: зачем они нужны

less_31__regex__walrus/theory_01__first_acquaintance.py

Вводный текстовый блок объясняет regex как самостоятельный макроязык (аналогия с SQL) и перечисляет четыре типовые задачи, которые он решает: извлечь нужное из текста, проверить наличие паттерна, заменить, разбить по сложным правилам. Практическая часть — не regex вовсе, а обычный text.count('mo') на сгенерированном lorem-тексте, показывающий базовую задачу подсчёта повторений без использования модуля re.

  • Комментарий: regex — макроязык, используется в большинстве языков
  • Четыре причины использовать regex (извлечь/проверить/заменить/разбить)
  • from lorem_text import lorem — генерация текста-заглушки
  • text.count('mo') — подсчёт вхождений БЕЗ regex, как отправная точка

Что выводит: Падает с ModuleNotFoundError: No module named 'lorem_text' — пакет lorem_text не установлен в окружении. Без строки импорта (и с готовым текстом text) скрипт напечатал бы 6 — число вхождений подстроки 'mo'.

Начало файла
"""
1. Язык регулярных выражений - это самостоятельный макроязык.
    Как, например, SQL.
    (впрочем, некоторые считают его всего лишь "формальным языком шаблонов")

2. Этот макроязык используется в большинстве языков высокого уровня.
    (в той или иной степени).


Зачем он вообще нужен?

1. Чтобы очень легко и удобно "вытащить" из текста ровно то, что нам нужно.
2. Чтобы проверить, действительно ли в тексте есть то, что нам нужно.
3. Чтобы быстро и удобно заменить в тексте ненужное на нужное.
Показать файл целиком (44 строк)
"""
1. Язык регулярных выражений - это самостоятельный макроязык.
    Как, например, SQL.
    (впрочем, некоторые считают его всего лишь "формальным языком шаблонов")

2. Этот макроязык используется в большинстве языков высокого уровня.
    (в той или иной степени).


Зачем он вообще нужен?

1. Чтобы очень легко и удобно "вытащить" из текста ровно то, что нам нужно.
2. Чтобы проверить, действительно ли в тексте есть то, что нам нужно.
3. Чтобы быстро и удобно заменить в тексте ненужное на нужное.
4. Чтобы легко и удобно разбить текст по сложным правилам.


С основными элементами макроязыка регулярных выражений (regex) можно ознакомиться здесь:
https://it4each.com/blog/reguliarnye-vyrazheniia-predislovie/


Ниже пример простейшего поиска кол-ва повторений паттерна в тексте.
Это делается на чистом Python без всякого regex.
Поскольку задача очень простая.

Но если вместо pattern использовать регулярное выражение,
то можно получить ГОРАЗДО более интересные результаты.
"""

from lorem_text import lorem

# print(lorem.paragraph())

text = """Consequatur est quos repellat obcaecati, corporis corrupti cupiditate qui 
tempore nesciunt possimus dolores animi voluptates culpa suscipit, eum excepturi 
laboriosam nemo commodi ullam? Odit placeat nemo sint voluptatum in illum accusamus 
saepe asperiores, distinctio odio harum? Dignissimos necessitatibus quaerat modi, 
magni ex debitis voluptas, neque error totam odit, molestias odio voluptatem obcaecati 
minus vel nostrum repudiandae iure, blanditiis corporis corrupti ducimus aperiam.
"""

patten = r'mo'

print(text.count(patten))  # 6
Проверьте себя: Какой модуль нужно установить (pip install lorem-text), чтобы этот файл запустился, и что он делает — на что похож lorem-text по сути?
Открыть файл →
2
PythonИсполняемый пример59 строк

findall() против finditer(): ловушка группировки

less_31__regex__walrus/theory_02__re_findall_vs_finditer.py

Центральный пример модуля про грабли findall() с группами: без скобок в паттерне findall возвращает список полных совпадений, а стоит добавить группу — findall неожиданно возвращает список содержимого ТОЛЬКО этой группы, а не всего совпадения. Файл показывает два решения: обернуть весь паттерн в дополнительную внешнюю группу (тогда findall вернёт кортежи, и совпадение нужно доставать по индексу [0]) или использовать finditer(), где каждый Match хранит .group(0) независимо от внутренних групп.

  • pattern_without_group — findall() без групп даёт список строк целиком
  • pattern_group с (-| |\/) — findall() возвращает только разделители
  • Решение 1: pattern_double_group — внешняя группировка + x[0] из кортежей
  • Решение 2: re.finditer(pattern_group, TEXT) + match.group(0)

Что выводит: Список полных совпадений ['111 22 33', '232-15-11', '555-22-78', '223/25/98', '258-85-45']; затем список одних разделителей [' ', '-', '-', '/', '-']; затем список кортежей (совпадение, разделитель) и список совпадений через x[0]; затем пять объектов re.Match со span и match, и в конце снова список полных совпадений через finditer.

Начало файла
import re

TEXT = """ 111 22 33-aad 232-15-11 ff 15 
15 555-22-78 223/99-37 223/25/987 123-35 26 258-85-45 
"""

pattern = r'\d{3}-\d{2}-\d{2}|\d{3}\/\d{2}\/\d{2}|\d{3} \d{2} \d{2}'

"""С применением регулярных выражений в конкретных методах Python-пакета re, 
нужно быть очень внимательными - у каждого метода могут быть свои особенности.

То, что сработало в regex101 - совсем не факт, что сработает в каждом re-методе!

findall() - отлично работает с паттернами без группировки
Показать файл целиком (59 строк)
import re

TEXT = """ 111 22 33-aad 232-15-11 ff 15 
15 555-22-78 223/99-37 223/25/987 123-35 26 258-85-45 
"""

pattern = r'\d{3}-\d{2}-\d{2}|\d{3}\/\d{2}\/\d{2}|\d{3} \d{2} \d{2}'

"""С применением регулярных выражений в конкретных методах Python-пакета re, 
нужно быть очень внимательными - у каждого метода могут быть свои особенности.

То, что сработало в regex101 - совсем не факт, что сработает в каждом re-методе!

findall() - отлично работает с паттернами без группировки
"""

pattern_without_group = r'\d{3}-\d{2}-\d{2}|\d{3}\/\d{2}\/\d{2}|\d{3} \d{2} \d{2}'
result = re.findall(pattern_without_group, TEXT)
print(result)
# ['111 22 33', '232-15-11', '555-22-78', '223/25/98', '258-85-45']

"""
Однако, добавление группировки всё сводит на нет:
"""
pattern_group = r'\d{3}(-| |\/)\d{2}\1\d{2}'

result = re.findall(pattern_group, TEXT)
print(result)
# [' ', '-', '-', '/', '-']

"""
Решение 1:
Окружить результат ещё одной группировкой.
(ОБРАТИТЕ ВНИМАНИЕ: 
    - теперь группировка разделителей становится по счёту второй
    - дополнительно ещё придётся обрабатывать и tuple группировок)
"""

pattern_double_group = r'(\d{3}(-| |\/)\d{2}\2\d{2})'

intermediate_result = re.findall(pattern_double_group, TEXT)
print(intermediate_result)
# [('111 22 33', ' '), ('232-15-11', '-'), ('555-22-78', '-'), ('223/25/98', '/'), ('258-85-45', '-')]

result = [x[0] for x in intermediate_result]
print(result)
# ['111 22 33', '232-15-11', '555-22-78', '223/25/98', '258-85-45']

"""
Решение 2:
Для группировок использовать более сложный метод finditer()
"""

for item in re.finditer(pattern_group, TEXT):
    print(item)

result = [match.group(0) for match in re.finditer(pattern_group, TEXT)]
print(result)
# ['111 22 33', '232-15-11', '555-22-78', '223/25/98', '258-85-45']
Проверьте себя: Почему добавление группы (-| |\/) в паттерн меняет то, что возвращает findall(), хотя сам факт совпадения текста не изменился?
Открыть файл →
3
PythonИсполняемый пример32 строк

re.search() против re.match(): где в строке ищем

less_31__regex__walrus/theory_03__re_search_vs_match.py

Показывает ключевое различие двух методов: search() ищет совпадение в любом месте строки и возвращает None, если паттерна нет вообще, а match() проверяет совпадение строго с начала строки — даже один лишний пробел в начале текста делает результат None, хотя паттерн присутствует чуть дальше.

  • re.search(pattern, TEXT) — находит совпадение в середине текста, .group()
  • re.search с несуществующим паттерном — возвращает None
  • re.match(pattern, TEXT.lstrip()) — совпадение строго с начала строки
  • re.match(pattern, ' ' + TEXT) — None из-за сдвига на пробелы

Что выводит: '111 22 33', затем None, затем снова '111 22 33' (после lstrip), затем None (после добавления двух пробелов в начало — match не сдвигается по строке).

Начало файла
import re

TEXT = """111 22 33-aad 232-15-11 ff 15 
15 555-22-78 223/99-37 223/25/987 123-35 26 258-85-45 
"""

pattern = r'\d{3} \d{2} \d{2}|\d{3}-\d{2}-\d{2}|\d{3}\/\d{2}\/\d{2}'

""" re.search - если находит совпадение, возвращает полную информацию
Если не находит - возвращает None
"""

match = re.search(pattern, TEXT)
print(match.group())  # 111 22 33
Показать файл целиком (32 строк)
import re

TEXT = """111 22 33-aad 232-15-11 ff 15 
15 555-22-78 223/99-37 223/25/987 123-35 26 258-85-45 
"""

pattern = r'\d{3} \d{2} \d{2}|\d{3}-\d{2}-\d{2}|\d{3}\/\d{2}\/\d{2}'

""" re.search - если находит совпадение, возвращает полную информацию
Если не находит - возвращает None
"""

match = re.search(pattern, TEXT)
print(match.group())  # 111 22 33

match = re.search(r'asdfasdfasdf', TEXT)
print(match)  # None


""" re.match - проверка СТРОГО с начала строки
Если находит совпадение, возвращает полную информацию
Если не находит - возвращает None
"""

match = re.match(pattern, TEXT.lstrip())
print(match.group())  # 111 22 33


# Сдвигаем строку на пробел и получаем ошибку,
# так как совпадения, начиная с начала строки, больше нет
match = re.match(pattern, '  ' + TEXT)
print(match)  # None
Проверьте себя: Почему re.match возвращает None после добавления двух пробелов в начало строки, хотя тот же паттерн чуть дальше в строке точно есть?
Открыть файл →
4
PythonИсполняемый пример36 строк

re.sub() и re.subn(): замена по шаблону с обратной ссылкой на группу

less_31__regex__walrus/theory_04__re_sub_vs_subn.py

Паттерн (\b\w+)\s\1 ловит два одинаковых слова подряд через обратную ссылку \1 на первую группу. re.sub заменяет найденное на первое слово, окружённое звёздочками; re.subn делает то же самое, но дополнительно возвращает число выполненных замен в кортеже. В конце — вариант с flags=re.IGNORECASE, который окружает звёздочками КАЖДОЕ отдельное слово независимо от регистра, используя другой паттерн без обратной ссылки.

  • pattern = r'(\b\w+)\s\1' — обратная ссылка на повторяющееся слово
  • re.findall(pattern, text) — находит парные повторы
  • re.sub(pattern, r'***\1***', text) — замена дублей на одно слово в звёздочках
  • re.subn(...) — то же плюс счётчик замен в кортеже
  • new_pattern с flags=re.IGNORECASE — окружает каждое слово, регистр не важен

Что выводит: ['hello', 'world']; затем '***hello*** ***world***'; затем ('***hello*** ***world***', 2); затем '***Hello*** ***hello*** ***world*** ***worLd***'.

Начало файла
""" re.sub(<паттерн поиска>, <паттерн замены>, <строка>)"""

import re

pattern = r"(\b\w+)\s\1"
text = "hello hello world world"

result = re.findall(pattern, text)
print(result)  # ['hello', 'world']

""" re.sub заменяет все вхождения шаблона pattern на указанный текст.

В данном случае, каждый раз, когда шаблон находит два повторяющихся слова, 
он заменяет их на первое слово (\1), окруженное ***.
Показать файл целиком (36 строк)
""" re.sub(<паттерн поиска>, <паттерн замены>, <строка>)"""

import re

pattern = r"(\b\w+)\s\1"
text = "hello hello world world"

result = re.findall(pattern, text)
print(result)  # ['hello', 'world']

""" re.sub заменяет все вхождения шаблона pattern на указанный текст.

В данном случае, каждый раз, когда шаблон находит два повторяющихся слова, 
он заменяет их на первое слово (\1), окруженное ***.

Таким образом, строка "hello hello world world" преобразуется в "hello world".
"""

replaced_text = re.sub(pattern, r"***\1***", text)
print(replaced_text)
# ***hello*** ***world***


# re.subn() - не только находит и заменяет, но и подсчитывает число замен
replaced_text_and_count = re.subn(pattern, r"***\1***", text)
print(replaced_text_and_count)
# ('***hello*** ***world***', 2)


"""Что надо сделать, если мы хотим окружить КАЖДОЕ слово,
причём независимо от регистра.
"""
new_pattern = r"(\b[a-z]+\b)"
new_text = "Hello hello world worLd"
replaced_text = re.sub(new_pattern, r"***\1***", new_text, flags=re.IGNORECASE)
print(replaced_text)
Проверьте себя: Почему в pattern используется именно \1, а не повторение (\b\w+)\s(\b\w+) — что изменится, если убрать обратную ссылку?
Открыть файл →
5
PythonИсполняемый пример15 строк

re.search в связке с оператором walrus (:=)

less_31__regex__walrus/theory_05__re_search_and_walrus.py

Короткий пример показывает типичную пару if match := re.search(...): — присвоить результат поиска переменной и сразу проверить его в условии одним выражением. В активном коде используется обычная развёрнутая форма (match = re.search(...); if match), а walrus-вариант оставлен закомментированным как эквивалентная альтернатива для сравнения.

  • match = re.search(pattern, text) — обычный поиск в две строки
  • if match: / else: — проверка найденного совпадения
  • Закомментированный аналог: if match := re.search(pattern, text):

Что выводит: Совпадение найдено: amazing

Файл целиком (15 строк)
import re

text = "Python is an amazing programming language."
pattern = "amazing"

match = re.search(pattern, text)
if match:
    print("Совпадение найдено:", match.group())
else:
    print("Совпадение не найдено")

# if match := re.search(pattern, text):
#     print("Совпадение найдено:", match.group())
# else:
#     print("Совпадение не найдено")
Проверьте себя: Чем закомментированный вариант с walrus эквивалентен активному коду и в чём его практическое преимущество — на сколько строк он короче?
Открыть файл →
6
MarkdownРазбор концепции64 строк

Оператор walrus (:=): присваивание внутри выражения

less_31__regex__walrus/theory_06__operator_walrus.md

Объясняет оператор walrus, появившийся в Python 3.8, — присваивание значения переменной прямо внутри выражения, без отдельной строки кода. Разбирает три типовых места применения: условие цикла while, условие if (избегая повторного вызова выражения) и list comprehension с фильтрацией. Отдельно предупреждает: walrus нельзя использовать в определении параметров функции — это синтаксическая ошибка, но можно свободно использовать внутри её тела.

  • while (n := len(input_string)) > 0: — чтение с сокращением кода
  • if (num := len(my_list)) > 5: — избегаем повторного вычисления
  • [square for num in numbers if (square := num ** 2) > 20] — walrus в comprehension
  • Ограничение: walrus нельзя в определении параметров функции — SyntaxError
  • Но можно свободно использовать walrus внутри тела функции
Показать начало файла (64 строк всего)
Оператор "walrus" (`:=`), введённый в Python 3.8, позволяет присваивать значение переменной внутри выражений.  

### 1. В цикле `while`
Один из самых популярных случаев использования оператора `walrus` — это чтение данных в цикле, когда нужно присваивать значение переменной и использовать его сразу в условии.

```
# Пример с циклом while
while (n := len(input_string)) > 0:
    print(f"Длина строки: {n}")
    input_string = input_string[:-1]
```

Здесь строка получает свою длину и сразу проверяется в условии, с присваиванием этого значения переменной `n`.

### 2. В условных выражениях (if)
Можно использовать `walrus` внутри условия `if`, чтобы не вызывать функцию или выражение дважды.

```
# Пример с условием if
if (num := len(my_list)) > 5:
    print(f"Список слишком длинный! Длина: {num}")
```

Это сокращает код, позволяя избежать повторного вычисления длины списка.

### 3. В list comprehensions (и прочих comprehensions при фильтрации данных)
Оператор `walrus` также может быть полезен внутри list comprehension, чтобы выполнять вычисления и присваивания в одном выражении.

```
# Пример с list comprehension
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
squared = [square for num in numbers if (square := num ** 2) > 20]
print(squared)  # Выведет [25, 36, 49, 64, 81]
```

Здесь для каждого элемента списка `numbers` мы сразу вычисляем квадрат и сохраняем его в переменную `square`,  
затем проверяем, больше ли он 20, и добавляем в новый список.


### 4. В функциях
Оператор `walrus` нельзя использовать в определении параметров функции в Python.

Например, следующий код вызовет ошибку:
```
# ⚠️ Неправильный пример
def my_function((x := 5)):
    print(x)
```
Этот код приведёт к синтаксической ошибке. Оператор walrus не поддерживается в местах, где ожидаются выражения для объявления параметров, таких как в определении функции.

Однако, мы можем использовать оператор walrus внутри тела функции или в выражениях. Например:

```
# Правильный пример
def my_function(input_string):
    if (length := len(input_string)) > 5:
        print(f"Длина строки: {length}")
    else:
        print("Строка слишком короткая")
```
…
Проверьте себя: Почему `def my_function((x := 5)):` — синтаксическая ошибка, хотя `if (x := 5):` внутри тела той же функции — абсолютно рабочий код?
7
MarkdownРазбор концепции179 строк

Иерархия модуля re: методы, флаги, объекты Pattern и Match

less_31__regex__walrus/theory_07__re_hierarchy.md

Полная карта модуля re в виде дерева: функции-методы (search, match, fullmatch, findall, finditer, sub, subn, split, compile, escape) с типами возвращаемых значений, константы-флаги (IGNORECASE, MULTILINE, DOTALL, VERBOSE, ASCII, LOCALE) с возможностью объединять их через `|`, и два объекта — Pattern (скомпилированный шаблон, работает быстрее и содержит почти все методы re) и Match с подробным разбором методов .group()/.groups()/.groupdict() и позиционных .start()/.end()/.span().

  • Таблица методов re: search/match/fullmatch/findall/finditer/sub/subn/split/compile/escape
  • Флаги: IGNORECASE, MULTILINE, DOTALL, VERBOSE, ASCII, LOCALE — и объединение через |
  • re.compile(pattern, flags) — объект Pattern, работает быстрее
  • Match: .group([n]), .groups(), .groupdict() — именованные группы (?P<name>...)
  • Match: .start()/.end()/.span() — позиции совпадения в строке
  • Match: .re, .string, .pos/.endpos — служебные атрибуты
Показать начало файла (179 строк всего)
# Иерархия объектов модуля `re`

```
re (модуль)
├── Методы
│   ├── search()       → Match or None          → Находит первое совпадение в строке 
│   ├── match()        → Match or None          → Проверяет начало строки
│   ├── fullmatch()    → Match or None          → Проверяет, соответствует ли вся строка
│   ├── findall()      → list[str or tuple]     → Возвращает все совпадения в виде списка
│   ├── finditer()     → iterator[Match]        → Возвращает итератор Match-объектов  
│   ├── sub()          → str                    → Заменяет по шаблону (аналог str.replace)  
│   ├── subn()         → (str, int)             → То же, что sub(), но возвращает ещё и число замен  
│   ├── split()        → list[str]              → Разделяет строку по шаблону (аналог str.split) 
│   ├── compile()      → Pattern object         → Компилирует строку шаблона в объект Pattern  
│   └── escape()       → str (экранирует        → Эквивалент raw-строки, где вручную добавлены   
│                             спец. символы)                            символы экранирования '\'
│
│
├── Константы-флаги
│   ├── re.IGNORECASE / re.I     → Игнорирует регистр (например, 'A' == 'a')
│   ├── re.MULTILINE / re.M      → ^ и $ работают в начале/конце каждой строки (а не всего текста)
│   ├── re.DOTALL / re.S         → Точка (.) захватывает и символ новой строки \n
│   ├── re.VERBOSE / re.X        → Позволяет писать шаблон с пробелами и комментариями (удобно для сложных regex)
│   ├── re.ASCII / re.A          → Символы \w, \b, \d и т.п. работают только с ASCII (не Unicode)
│   └── re.LOCALE                → Поведение \w, \W, \b и т.п. зависит от локали (устаревшее и редко используемое)
│                                        (пример установки локали: locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8'))
│
│                           ⚠️ Несколько флагов можно объединять с помощью символа "|" 
│                                (flags = re.IGNORECASE | re.DOTALL)
│
│
├── Объекты
│   ├── Pattern (скомпилированный шаблон)
│   │   ├── search()
│   │   ├── match()
│   │   ├── fullmatch()
│   │   ├── findall()
│   │   ├── finditer()
│   │   ├── sub()
│   │   ├── subn()
│   │   ├── split()
│   │   └── .pattern (атрибут, возвращающий шаблон регулярного выражения)
│   │
│   └── Match
│       ├── group([n])
│       ├── groups()
│       ├── groupdict()
│       ├── start([n])
│       ├── end([n])
│       ├── span([n])
│       └── .string (атрибут, возвращающий исходный текст)

```

## Методы

Принимают паттерн и текст, и возвращают результат в указанном виде

## Константы-флаги

…
Проверьте себя: Чем объект Pattern, полученный через re.compile(), выгоднее прямого вызова re.search(pattern, text) каждый раз заново, и какой единственный метод у Pattern отсутствует по сравнению с модулем re?