owned this note
owned this note
Published
Linked with GitHub
---
title: Тема 9. Регулярные выражения
tags: Python Advanced
slideOptions:
allottedMinutes: 80
transition: slide
theme: beige
slideNumber: c
spotlight:
enabled: true
---
## ++Тема 9++<br>Регулярные выражения
(c) Яценко Р.Н., 2019-2021
[Учебный центр компьютерных технологий "Кит"](http://kit.kh.ua/)
<img src="https://i.imgur.com/Kh901c1.png" style="width: 150px; position: fixed; top: 10px; right: 10px; border: 0; box-shadow: none;">
---
## 1. Введение в регулярные выражения (re)
----
### Назначение
- Проще говоря, регулярное выражение используется для поиска паттернов в указанной строке. Паттерном может быть все что угодно
- Можно создавать паттерны соответствия электронной почте или мобильному номеру. Можно создать паттерны, которые ищут слова в строке, начинающиеся на “a” и заканчивающиеся на “z”
----
### Где используются
1. Проверка соответствия фрагментов текста некоторым критериям. Например, наличие символа обозначения валюты и последующих за ним цифр, проверка адреса электронной почты и т.д.
2. Поиск подстрок, которые в том числе могут иметь несколько форм. Например, поиск `'pet.png'`, `'pet.jpg'`, `'pet.jpeg'` или `'pet.svg'`, но не обнаруживались подстроки `'carpet.png'` и подобные ей
----
3. Замена всего, что совпадает с шаблоном, на указанную строку. Например, поиск подстроки `'устройство передвижения, движимое мускульной силой'` и замена подстрокой `'велосипед'`
4. Разбиение строки по точкам совпадения с шаблоном. Например, разбиение строки по `':'` или `'='`
----
### Пример кода
```python=
import re
pattern = r"times"
string = "It was the best of times, it was the worst of times."
print(len(re.findall(pattern,string))) # 2
```
----
### Примеры выражений
Для написания и отладки регулярных выражений удобно использовать онлайн-сервисы, например https://regex101.com, которые в том числе содержат библиотеку готовых выражений и могут генерировать код для различных языков программирования
https://regex101.com/r/HDScOu/1
---
## 2. Язык регулярных выражений
----
### Определение
[Регулярные выражения](https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%83%D0%BB%D1%8F%D1%80%D0%BD%D1%8B%D0%B5_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F) (_англ._ Regular Expressions)
: это компактная форма записи (шаблон, маска или паттерн) представления о коллекции строк. Гибкость и мощь регулярных выражений обусловлена тем, что единственное регулярное выражение может представлять неограниченное число строк, подходящий под заданный шаблон
----
### Строка-шаблон
1. Флаги
2. Символы и классы символов
3. Квантификаторы
4. Группировка и выбор
5. Проверка границ (привязки)
----
### Флаги
Флаги не входят непосредственно в регулярное выражение, однако расширяют его функции:
* `'g'` -- глобальный поиск (обрабатываются все совпадения с шаблоном поиска)
* `'i'` -- регистр букв не имеет значения (по умолчанию любой поиск регистрозависим)
* `'m'` -- многострочный поиск
* и др.
Флаг указывается после паттерна, например: `'/[0-9]/m'`
----
### Поиск символов и строк
Одним из наиболее простых случаев является поиск отдельных символов и строк

https://regex101.com/r/Y67E75/1
----
### Экранирование специальных символов
Специальные символы ([символы-джокеры](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%BC%D0%B2%D0%BE%D0%BB%D1%8B-%D0%B4%D0%B6%D0%BE%D0%BA%D0%B5%D1%80%D1%8B) должны экранироваться символом обратного слеша `'\'`. К ним относятся:
```
\ . ~ $ ? + * { } [ ] ( ) |
```
В пределах регулярных выражений можно также использовать большинство стандартных экранированных последовательностей языка Python, например, `'\n'`, `'\t'` и др.
----
### Поиск множества символов
Для поиска не конкретной последовательности символов, а некоторого их множества предназначены классы символов
[группа_символов]
: Соответствует любому одиночному символу, входящему в группа_символов. По умолчанию при сопоставлении учитывается регистр
----
*Пример:*
- выражение `'[аеёиоуэюя]'` позволяет найти все гласные буквы в строке
- выражение `'п[ое]л'` даст совпадение для слов `'пел'` и `'пол'`, но не найдет слова `'поел'` или `'пил'`
----
[^группа_символов]
: Соответствует любому одиночному символу, НЕ входящему в `группа_символов`. По умолчанию при сопоставлении учитывается регистр.
*Пример:*
* выражение `'[^аеёиоуэюя]'` обнаружит все символы в тексте, кроме гласных букв
----
[первый-последний]
: Диапазон символов: соответствует одному символу в диапазоне от `первый` до `последний`
*Пример:*
* выражение `'[0-9]'` найдет все цифры в тексте
----
`.` (точка)
: Специальный знак `'.'` соответствует какому-либо одному знаку, кроме `'\n'`. Для поиска точки необходимо использовать экранирование: `'\.'`
*Пример:*
* выражение `'м.л'` найдет в тексте `'мел'`, `'мул'` и т.д.
----
`\w`
: Соответствует любому алфавитно-цифровому знаку
*Пример:*
* выражение `'\w1'` найдет в тексте `'11'` или `'я1'`
`\W`
: Соответствует любому символу, НЕ являющимся алфавитно-цифровым знаком
----
`\s`
: Соответствует любому пробельному символу (пробел, табуляция и др.)
`\S`
: Соответствует любому знаку, НЕ являющемуся пробельным
----
`\d`
: Соответствует любой десятичной цифре
*Пример:*
* выражение `'\d-й'` найдет в тексте `'1-й'` или `'2-й'`
`\D`
: Соответствует любому символу, НЕ являющемуся десятичной цифрой
----
### Квантификаторы
Зачастую требуется не только найти слово по символам или группе символов, но и указать количество возможных повторений, для чего в регулярных выражениях используются квантификаторы
Квантификаторы записываются после символа/строки/множества
----
### Виды квантификаторов
Квантификатор|Описание
-|-
`'*'`|Ноль или более совпадений
`'+'`|Одно или больше совпадений
`'?'`|Ноль или одно совпадение
`'{m}'`|Совпадение ровно `m` раз
`'{m,}'`|Совпадение `m` и более раз
`'{m, n}'`|Совпадение от `m` до `n` раз
----
### Жадность и лень
Каждый квантификатор может быть:
* жадный (_англ._ greedy): находит как можно больше подходящих символов
* ленивый (_англ._ lazy): находит как можно меньше подходящих символов
По умолчанию, все квантификаторы являются жадными; для включения «ленивого» режима необходимо поставить знак `'?'` после квантификатора
----
### Разница между жадным и ленивым режимом
https://regex101.com/r/grLIWd/1
https://regex101.com/r/grLIWd/2
----
### Задания
https://regex101.com/r/aGn8QC/2
1. Найдите все натуральные числа (возможно, окружённые буквами)
2. Найдите все «слова», написанные капсом (то есть строго заглавными), возможно внутри настоящих слов (ааа<u><font color="#007d5b">БББ</font></u>ввв)
3. Найдите слова, в которых есть русская буква, а когда-нибудь за ней цифра
4. Найдите все слова, начинающиеся с русской или латинской большой буквы (`\b` — граница слова)
----
5. Найдите слова, которые начинаются на гласную (`\b` — граница слова)
6. Найдите все натуральные числа, не находящиеся внутри или на границе слова
7. Найдите строчки, в которых есть символ `*` (`.` — это точно не конец строки!)
8. Найдите строчки, в которых есть открывающая и когда-нибудь потом закрывающая скобки
9. Выделите одним махом весь кусок оглавления (в конце примера, вместе с тегами)
10. Выделите одним махом только текстовую часть оглавления, без тегов
---
## 3. Модуль re
----
### "Сырые" строки
- В коде регулярные выражения обычно записываются в виде «сырых» строк (с префиксом `r`, подавляющих экранирование).
- Так, при поиске переноса строки удобнее указать `r"\n"` вместо `"\\n"`, избежав необходимости дублировать экранирующий символ
----
### Способы работы
1. Если предполагается однократная проверка, достаточно использовать функции модуля `re`. При этом во время вызова функции, произойдет компиляция регулярного выражения и дальнейший поиск соответствий
2. Если проверка будет осуществляется не один раз, эффективным будет предварительно один раз скомпилировать выражение (получив специальный объект класса `re.regex`), а затем использовать его методы
----
### Функции модуля re
Функция|Описание
-|-
`re.search(pattern,` `string)`|Найти в строке `string` первую строчку, подходящую под шаблон `pattern`
`re.fullmatch(pattern,` `string)`|Проверить, подходит ли строка `string` под шаблон `pattern`
----
Функция|Описание
-|-
`re.split(pattern,` `string,` `maxsplit=0)`|Аналог `str.split()`, только разделение происходит по подстрокам, подходящим под шаблон `pattern`
`re.findall(pattern,` `string)`|Найти в строке `string` все непересекающиеся шаблоны `pattern`
----
Функция|Описание
-|-
`re.finditer(pattern,` `string)`|Итератор всем непересекающимся шаблонам `pattern` в строке `string` (выдаются `match`-объекты)
`re.sub(pattern,` `repl,` `string,` `count=0)`|Заменить в строке `string` все непересекающиеся шаблоны `pattern` на `repl`
----
### Примеры
``` python=
import re
match = re.search(r'\d\d\D\d\d', r'Телефон 123-12-12')
print(match[0] if match else 'Not found')
# 23-12
match = re.search(r'\d\d\D\d\d', r'Телефон 1231212')
print(match[0] if match else 'Not found')
# Not found
```
----
``` python=
match = re.fullmatch(r'\d\d\D\d\d', r'12-12')
print('YES' if match else 'NO')
# YES
match = re.fullmatch(r'\d\d\D\d\d', r'Т. 12-12')
print('YES' if match else 'NO')
# NO
print(re.split(r'\W+', 'Где, скажите мне, мои очки??!'))
# ['Где', 'скажите', 'мне', 'мои', 'очки', '']
```
----
``` python=
print(re.findall(r'\d\d\.\d\d\.\d{4}',
r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017'))
# ['19.01.2018', '01.09.2017']
for m in re.finditer(r'\d\d\.\d\d\.\d{4}',
r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017'):
print('Дата', m[0], 'начинается с позиции', m.start())
# Дата 19.01.2018 начинается с позиции 20
# Дата 01.09.2017 начинается с позиции 45
```
----
``` python=
print(re.sub(r'\d\d\.\d\d\.\d{4}',
r'DD.MM.YYYY',
r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017'))
# Эта строка написана DD.MM.YYYY, а могла бы и DD.MM.YYYY
```
----
### Дополнительные флаги
Константа|Назначение
-|-
`re.ASCII`| По умолчанию `\w`, `\W`, `\b`, `\B`, `\d`, `\D`, `\s`, `\S` соответствуют все юникодные символы с соответствующим качеством.<br> `re.ASCII` ускоряет работу, если все соответствия лежат внутри ASCII
----
Константа|Назначение
-|-
`re.IGNORECASE`| Не различать заглавные и маленькие буквы. Работает медленнее, но иногда удобно
`re.MULTILINE`| Специальные символы `^` и `$` соответствуют началу и концу каждой строки
`re.DOTALL`| По умолчанию символ `\n` конца строки не подходит под точку. С этим флагом точка — вообще любой символ
----
### Примеры
``` python=
print(re.findall(r'\d+', '12 + ٦٧'))
# ['12', '٦٧']
print(re.findall(r'\w+', 'Hello, мир!'))
# ['Hello', 'мир']
print(re.findall(r'\d+', '12 + ٦٧', flags=re.ASCII))
# ['12']
print(re.findall(r'\w+', 'Hello, мир!', flags=re.ASCII))
# ['Hello']
print(re.findall(r'[уеыаоэяию]+', 'ОООО ааааа ррррр ЫЫЫЫ яяяя'))
# ['ааааа', 'яяяя']
print(re.findall(r'[уеыаоэяию]+', 'ОООО ааааа ррррр ЫЫЫЫ яяяя',
flags=re.IGNORECASE))
# ['ОООО', 'ааааа', 'ЫЫЫЫ', 'яяяя']
```
----
``` python=
text = r"""
Торт
с вишней1
вишней2
"""
print(re.findall(r'Торт.с', text))
# []
print(re.findall(r'Торт.с', text, flags=re.DOTALL))
# ['Торт\nс']
print(re.findall(r'виш\w+', text, flags=re.MULTILINE))
# ['вишней1', 'вишней2']
print(re.findall(r'^виш\w+', text, flags=re.MULTILINE))
# ['вишней2']
```
----
### Компиляция регулярок
`re.compile(pattern, flags=0)`
: Компилирует регулярное выражение `pattern`, используя флаги `flags` (например, `re.MULTILINE`) и возвращает объект класса `re.regex`
*Пример:*
```python=
import re
regex = re.compile("^.+?(\d)")
print(regex.search("Привет1 Привет12")[0])
```
----
### Match-объекты
- Если функции `re.search`, `re.fullmatch` не находят соответствие шаблону в строке, то они возвращают `None`, функция `re.finditer` не выдаёт ничего
- Если соответствие найдено, то возвращается `match`-объект. Эта штука содержит в себе кучу полезной информации о соответствии шаблону
----
### Атрибуты match
Метод | Описание
-|-
`match[0]`, `match.group()`|Подстрока, соответствующая шаблону
```python=
match = re.search(r'\w+', r'$$ What??')
print(match[0])
# 'What'
```
----
Метод | Описание
-|-
`match.start()`|Индекс в исходной строке, начиная с которого идёт найденная подстрока
`match.end()`|Индекс в исходной строке, который следует сразу за найденной подстрокой
```python=
match = re.search(r'\w+', r'$$ What??')
print(match.start()) # 3
print(match.end()) # 7
```
----

---
## 4. Группировка и выбор
----
### Группирующие скобки (...)
- Если в шаблоне регулярного выражения встречаются скобки `(...)` без `?:`, то они становятся _группирующими_
- В match-объекте, который возвращают `re.search`, `re.fullmatch` и `re.finditer`, по каждой такой группе можно получить ту же информацию, что и по всему шаблону:
- часть подстроки, которая соответствует `(...)`
- индексы начала и окончания в исходной строке
----
### Пример с группами
```python=
import re
pattern = r'\s*([А-Яа-яЁё]+)(\d+)\s*'
string = r'--- Опять45 ---'
match = re.search(pattern, string)
print(f'Найдена строка >{match[0]}< с позиции {match.start(0)}',
f'до {match.end(0)}')
print(f'Группа букв >{match[1]}< с позиции {match.start(1)}',
f'до {match.end(1)}')
print(f'Группа цифр >{match[2]}< с позиции {match.start(2)}',
f'до {match.end(2)}')
```
```
Найдена строка > Опять45 < с позиции 3 до 16
Группа букв >Опять< с позиции 6 до 11
Группа цифр >45< с позиции 11 до 13
```
----


----
### Особенности групп с квантификатором
- Если к группирующим скобкам применён квантификатор (то есть указано число повторений), то подгруппа в `match`-объекте будет создана только для последнего соответствия
----
- Например, если бы в для строки <br>`'--- Опять45 ---'` <br>квантификаторы были снаружи от скобок <br>`'\s*([А-Яа-яЁё])+(\d)+\s*'`, то вывод был бы таким:
```
Найдена подстрока > Опять45 < с позиции 3 до 16
Группа букв >ь< с позиции 10 до 11
Группа цифр >5< с позиции 12 до 13
```
----
### Вложенные скобки
- Внутри группирующих скобок могут быть и другие группирующие скобки.
- В этом случае их нумерация производится в соответствии с номером появления открывающей скобки в шаблоне
----
```python=
import re
pattern = r'((\d)(\d))((\d)(\d))'
string = r'123456789'
match = re.search(pattern, string)
print(f'Найдена строка >{match[0]}< с позиции {match.start(0)}',
f'до {match.end(0)}')
for i in range(1, 7):
print(f'Группа №{i} >{match[i]}< с позиции {match.start(i)}',
f'до {match.end(i)}')
```
```
Найдена строка >1234< с позиции 0 до 4
Группа №1 >12< с позиции 0 до 2
Группа №2 >1< с позиции 0 до 1
Группа №3 >2< с позиции 1 до 2
Группа №4 >34< с позиции 2 до 4
Группа №5 >3< с позиции 2 до 3
Группа №6 >4< с позиции 3 до 4
```
----
### Использование групп при заменах
- Использование групп добавляет замене (`re.sub`) очень удобную возможность: в шаблоне для замены можно ссылаться на соответствующую группу при помощи
`\1, \2, \3, ...`
----
- Например, если нужно даты из неудобного формата `ММ/ДД/ГГГГ` перевести в удобный `ДД.ММ.ГГГГ`, то можно использовать такую регулярку:
```python=
import re
text = """
We arrive on 03/25/2018.
So you are welcome after 04/01/2018.
"""
print(re.sub(r'(\d\d)/(\d\d)/(\d{4})', r'\2.\1.\3', text))
```
```
We arrive on 25.03.2018.
So you are welcome after 01.04.2018.
```
----
### Невыделяемая группа
`(?:часть_выражения)`
: Определяет невыделяемую группу (без захвата)
*Пример:*
* выражение `'(?:\w+)\s'` выполнит в тексте поиск слов, отделенных пробельным символом; при этом слово НЕ будет захвачено в отдельную группу
----
### Выбор
Операция выбора позволяет захватить одно из нескольких выражений в качестве результата поиска
`выражение1|выражение2|выражение3`
: Соответствует любому элементу, разделенному вертикальной чертой `'|'`
----
### Пример
* выражение `'красн(?:ый|оватый|енький)'` найдет в тексте слова `'красный'`, `'красноватый'`, `'красненький'`; при этом окончание не будет захвачено в группу
---
## 5. Проверка границ (привязки)
----
### Простые шаблоны
Шаблон | Описание | Пример | Результат
-|-|-|-
`^`|Начало всего текста или начало строчки текста, если `flag=re.MULTILINE`|`^Привет`|
`$`|Конец всего текста или конец строчки текста, если `flag=re.MULTILINE`|`Будь здоров!$`|
----
Шаблон | Описание | Пример | Результат
-|-|-|-
`\A`|Строго начало всего текста| |
`\Z`|Строго конец всего текста | |
`\b`|Начало или конец слова (слева пусто или не-буква, справа буква и наоборот)|`\bвал`|<u>вал</u>, перевал, Перевалка
----
Шаблон | Описание | Пример | Результат
-|-|-|--
`\B`|Не граница слова: либо и слева, и справа буквы, либо и слева, и справа НЕ буквы|`\Bвал`| пере<u>вал</u>, вал, Пере<u>вал</u>ка|
| |`\Bвал\B`|перевал, вал, Пере<u>вал</u>ка
----

----
### Сложные шаблоны (_lookaround_)
Шаблон|Описание|Пример|Результат
-|-|-|-
`(?=...)`|_lookahead assertion_, соответствует каждой позиции, сразу после которой начинается соответствие шаблону `...`|`Isaac (?=Asimov)`|<u>Isaac </u>Asimov, Isaac other
----
Шаблон|Описание|Пример|Результат
-|-|-|-
`(?!...)`|_negative lookahead assertion_, соответствует каждой позиции, сразу после которой НЕ может начинаться шаблон `...`|`Isaac (?!Asimov)`|Isaac Asimov, <u>Isaac </u>other
----
Шаблон|Описание|Пример|Результат
-|-|-|-
`(?<=...)`|_positive lookbehind assertion_, соответствует каждой позиции, которой может заканчиваться шаблон `...`<br>Длина шаблона должна быть фиксированной|`(?<=abc)def`|abc<u>def</u>, bcdef
----
Шаблон|Описание|Пример|Результат
-|-|-|-
`(?<!...)`|_negative lookbehind assertion_, соответствует каждой позиции, которой НЕ может заканчиваться шаблон `...`|`(?<!abc)def`|abcdef, bc<u>def</u>
----

----
### Примеры
Шаблон | Комментарий | Результат
-|-|-
`(?<!\d)\d(?!\d)`|Цифра, окружённая не-цифрами|Text ABC 123 A<u>1</u>B<u>2</u>C<u>3</u>!
`(?<=#START#).*?(?=#END#)`|Текст от `#START#` до `#END#`|text from #START#<u> till </u>#END#
----
Шаблон | Комментарий | Результат
-|-|-
`\d+(?=_(?!_))`|Цифра, после которой идёт ровно одно подчёркивание|<u>12</u>_34__56
`^(?:(?!boo).)*?$`|Строка, в которой нет boo (то есть нет такого символа, перед которым есть boo)| <u>a foo and</u><br>boo and zoo>br><u>and others</u>
----
Шаблон | Комментарий | Результат
-|-|-
`^(?:(?!boo)(?!foo).)*?$`|Строка, в которой нет ни boo, ни foo|a foo and<br>boo and zoo<br><u>and others</u>
---
## Спасибо за внимание!

(c) Яценко Р.Н., 2019-2021
[Учебный центр компьютерных технологий "Кит"](http://kit.kh.ua/)