Статьи

Регулярные выражения Python

В этой статье мы представляем исчерпывающую статью о регулярных выражениях на языке Python.

1. Что такое регулярное выражение

Если вы когда-либо искали файл, который точно не помнили его имени, то вы могли использовать один или несколько специальных (или подстановочных ) символов для определения символов, которые вы не могли запомнить. Например, чтобы найти все файлы .txt , начинающиеся с b вы, возможно, набрали « b*.txt в приложении файлового менеджера (Windows Explorer, или Nautilus, или Finder), или в оболочке Linux или в окне DOS. Вышеупомянутое выражение ( b*.txt ) является примером сопоставления с шаблоном , которое называется более конкретно « глобализация» , и оно соответствует именам b.txt ba.txt bla.txt

Распространенные персонажи:

  • ? соответствует любому отдельному символу
  • * соответствует любому количеству символов
  • [...] соответствует любому отдельному символу в наборе, например, [ac] соответствует только символам a, b и c .

Давайте посмотрим некоторые определения из Википедии:

Итак, в приведенном выше примере с поиском файлов b*.txt является шаблоном поиска, который применяется, например, к именам b.txt ba.txt bla.txt и соответствует именам файлов b.txt ba.txt bla.txt .

В этой статье мы узнаем о шаблонах поиска или регулярных выражениях, которые поддерживаются Python, а также о том, какие команды использовать для использования регулярных выражений в ваших программах на Python.

2. Введение в регулярные выражения в Python

Представьте, что у вас есть задача поиска электронных писем в тексте, например

1
2
>>> text = 'You may reach us at this email address:
java.info@javacodegeeks.com. Opening hours: @9am-@17pm'

Как вы будете искать адрес электронной почты в тексте выше?

1
index = text.find('@')

чтобы найти индекс @ а затем вам нужно найти другие части адреса электронной почты (оставьте читателю в качестве упражнения; пожалуйста, не используйте регулярные выражения). Используя регулярные выражения, вы наберете что-то вроде:

1
2
>>> import re
>>> ans = re.search('@', text)

Намного проще, не правда ли? Далее мы увидим, как мы можем уточнить нашу строку регулярного выражения внутри метода search() , чтобы иметь возможность правильно идентифицировать адрес электронной почты.

В качестве другого примера, давайте посмотрим, как мы можем искать шаблон b*.txt в нашей текущей папке:

1
2
3
>>> import glob
>>> print (glob.glob('b*.txt'))
['b.txt' 'ba.txt' 'bla.txt']

Не так ли сложно, не так ли?

2.1 Методы регулярных выражений Python

Модуль re предоставляет ряд функций сопоставления с образцом:

метод объяснение
match(regex, string) находит regex в начале string и возвращает объект Match с методами start() и end() для получения индексов или None
search(regex, string) находит regex любом месте string и возвращает объект Match с методами start() и end() чтобы получить индексы или None
fullmatch(regex, string) возвращает объект Match если regex соответствует string , в противном случае возвращается None
findall(regex, string) находит все вхождения regex в string и возвращает список совпадающих строк
finditer(regex, string) возвращает итератор для цикла по совпадениям с regex в string , например, for m in finditer(regex, string ), где m имеет тип Match
sub(regex, replacement, string) заменяет все совпадения regex в string с replacement и возвращает новую строку с заменами
split(regex, string) возвращает список строк, который содержит части string между всеми совпадениями regex в string
compile(regex) компилирует регулярное выражение в объект регулярного выражения

Аналогично, модуль glob предоставляет функцию сопоставления с образцом:

  • glob(pattern) находит файлы, которые удовлетворяют (globbing) pattern

В оставшейся части этой статьи мы сосредоточимся на регулярных выражениях (а не на глобализации).

Вы можете протестировать регулярные выражения в этой статье либо в своей среде Python (например, irb ), либо на одном из следующих сайтов онлайн-регулярных выражений (список не является исчерпывающим, и вы можете найти лучший сайт для своих нужд или вкуса):

Самое простое регулярное выражение – это просто текстовая строка без каких-либо специальных символов, то есть литерала . Литералы – это самая простая форма сопоставления с образцом в регулярных выражениях. Они будут просто успешны всякий раз, когда этот литерал будет найден в тексте, в котором вы ищите. Например, давайте найдем шаблон email в text определенном выше, с помощью API Python:

1
2
3
4
5
6
>>> regex = 'email'
>>> re.match(regex, text)
>>> re.search(regex, text)
<_sre.SRE_Match object; span=(25, 30), match='email'>
>>> re.findall(regex, text)
['email']

Вы понимаете результаты? re.match( ) пытается найти шаблон в начале text но text не начинается со слова email . re.search() пытается найти шаблон в любом месте text и возвращает экземпляр match говоря, что шаблон был найден, начиная с позиции 25 и заканчивая позицией 30 text (нумерация начинается с 0 ). Наконец, re.findall() возвращает список совпадающих строк, поэтому 'email » была найдена только один раз. После вышеприведенного объяснения следующий результат должен быть самоочевидным:

1
2
>>> re.findall('@', text)
['@', '@', '@']

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

1
2
3
4
>>> regex = '@'
>>> replacement = '#'
>>> re.sub(regex, replacement, text)
'You may reach us at this email address: java.info#javacodegeeks.com. Opening hours: #9am-#17pm'

Обратите внимание, что text не изменяется.

Чтобы повысить производительность, например, когда мы повторно используем регулярное выражение в ряде совпадений, мы можем скомпилировать регулярное выражение в объект регулярного выражения:

1
2
3
>>> regex = re.compile('@')
>>> regex.findall(text)
['@', '@', '@']

2.2 Объект Match

Матчобъект содержит ряд методов:

  • group() возвращает часть строки, совпадающую со всем регулярным выражением
  • start() возвращает смещение в строке начала матча (начинается с 0)
  • end() возвращает смещение символа после окончания матча
  • span() возвращает 2 кортежа start() и end()
1
2
3
4
5
6
7
8
9
>>> match = re.search(regex, text)
>>> match.group()
'email'
>>> match.start()
25
>>> match.end()
30
>>> match.span()
(25, 30)

Распространенная ошибка заключается в следующем:

1
2
3
4
5
>>> match = re.search("python", text)
>>> match.group()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'

По этой причине создайте такой полезный метод, как этот:

1
2
3
4
5
6
def findMatch(regex, text):
    match = re.search(regex, text)
    if match:
       print(match.group())
    else:
       print("Pattern not found!")

Более эффективным, чем re.findall(regex, text) является re.finditer(regex, text) . Он возвращает итератор, который позволяет циклически повторять совпадения с регулярным выражением в тексте:

1
2
3
4
>>> for m in re.finditer(regex, text):
...    print('%02d-%02d: %s' % (m.start(), m.end(), m.group(0)))
...
25-30: email

Переменная цикла for m – это объект Match со сведениями о текущем совпадении.

3. Мета-символы

Следующие символы имеют специальные значения в регулярных выражениях:

Мета-символ Смысл
. Любой отдельный персонаж
[ ], [^ ] Любой отдельный символ в наборе (символов) или нет ( ^ ) в наборе (порядок не имеет значения)
? Квантор: необязательный, т. Е. Ноль или одно из предшествующих регулярных выражений
* Quantifier: ноль или более предыдущего регулярного выражения
+ Квантификатор: одно или несколько из предыдущего регулярного выражения
| Или же
^ Привязать шаблон к началу строки
$ Шаблон привязки к концу строки
( ) Группа персонажей
{ } Quantifier: количество времени предыдущего регулярного выражения.
{n} означает ровно n раз
{n, m} или {nm} означает n и m раз (включительно)
{n,} или {,m} означает не менее n или не более m раз
\ Избегает метасимвола, то есть это означает, что следующий за ним символ не является метасимволом.

Поэтому, если мы хотим выполнить поиск вышеуказанных метасимволов, нам нужно экранировать их, используя метасимвол escape ( \ ). В следующей таблице приведены примеры метасимволов escape.

Непечатный символ Смысл
\n Новая линия
\r Возврат каретки
\e Побег
\t табуляция
\v Вертикальная вкладка
\f Форма подачи
\uXXXX Символы Юникода, например, \u20AC представляет

Давайте посмотрим несколько примеров:

Точка ( . ) Соответствует одному символу, кроме символов разрыва строки.

1
2
3
4
>>> regex = "gr.y"
>>> text = "gr gry grey gray gryy grrrr graaaay gr%y"
>>> re.findall(regex, text)
['grey', 'gray', 'gryy', 'gr%y']

Если мы хотим сопоставить слова только с 3 символами, которые заканчиваются на y :

01
02
03
04
05
06
07
08
09
10
11
12
>>> regex = "...y"
>>> re.findall(regex, text)
[' gry', 'grey', 'gray', ' gry', 'aaay', 'gr%y']
>>> regex = "gr.*y"
>>> re.findall(regex, text)
['gr gry grey gray gryy grrrr graaaay gr%y']
>>> regex = "gr.+y"
>>> re.findall(regex, text)
['gr gry grey gray gryy grrrr graaaay gr%y']
>>> regex = "gr.?y"
>>> re.findall(regex, text)
['gry', 'grey', 'gray', 'gryy', 'gr%y']

.* означает любой отдельный символ ноль или более раз .+ означает любой отдельный символ один или несколько раз и . ? означает любой отдельный символ ноль или один раз. Вы можете быть удивлены результатами "gr.*y" и "gr.+y" но они верны; они просто соответствуют всему text потому что он действительно начинается с gr и заканчивается y (словом gr%y ), содержащим все остальные символы между ними.

Эти модификаторы называются жадными , потому что они пытаются найти максимально возможное совпадение, чтобы получить максимально возможный результат совпадения. Вы можете преобразовать их в не жадные , добавив дополнительный знак вопроса в квантификатор; например, ?? *? или +? , Квантификатор, помеченный как не жадный или неохотный, пытается получить наименьшее возможное соответствие

1
2
3
>>> regex = "gr.*?y"
>>> re.findall(regex, text)
['gr gry', 'grey', 'gray', 'gry', 'grrrr graaaay', 'gr%y']

Если нам нужно сопоставить один или несколько метасимволов в нашем text , то метасимвол escape показывает его использование:

1
2
3
4
>>> text="How is life?"
>>> regex="life\?"
>>> findMatch(regex, text)
life?

Вы также можете разбить строку:

1
2
3
4
>>> text = "gr, gry, grey, gray, gryy, grrrr, graaaay, gr%y"
>>> regex=", "
>>> re.split(regex, text)
['gr', 'gry', 'grey', 'gray', 'gryy', 'grrrr', 'graaaay', 'gr%y']

4. Наборы символов или классы

Но что, если мы хотим найти только правильно написанные слова, то есть только grey и gray ? Наборы символов (или классы символов) для спасения:

1
2
3
>>> regex = "gr[ae]y"
>>> re.findall(regex, text)
['grey', 'gray']

Набор символов или класс соответствует только одному из нескольких символов. Порядок символов внутри класса символов не имеет значения. Дефис ( - ) внутри класса символов указывает диапазон символов. Например, [0-9] соответствует одной цифре от 0 до 9. Вы можете использовать более одного диапазона, например, [A-Za-z] соответствует одному символу, без учета регистра.

Как бы вы соответствовали адресу электронной почты в первом тексте (повторяется здесь)?

1
>>> text = 'You may reach us at this email address: java.info@javacodegeeks.com. Opening hours: @9am-@17pm'

Использование классов персонажей не должно быть таким сложным. Адрес электронной почты состоит из заглавных или строчных букв, например, [A-Za-z] и, возможно, точки ( . ) В нашем примере, например, [A-Za-z.] (Не нужно экранировать точку внутри набора символов). , Но это соответствует только одному символу в наборе. Чтобы соответствовать любому количеству этих символов, нам нужно (как вы уже догадались) [A-Za-z.] +. Таким образом, вы в конечном итоге:

1
2
3
>>> regex = "[A-Za-z.]+@[A-Za-z]+\.[A-Za-z]{2,3}"
>>> re.findall(regex, text)
['java.info@javacodegeeks.com']
Элемент объяснение
[A-Za-z.] Соответствует любой латинской букве и точке
+ Один или несколько раз
@ соответствует символу @
[A-Za-z] соответствует любой латинской букве
+ Один или несколько раз
\. Соответствует точке
[A-Za-z] соответствует любой латинской букве
{2,3} 2 или 3 раза

Поздравляем! Вы написали свое первое фактическое регулярное выражение. Обратите внимание, что вам нужно экранировать точку вне класса символов. Чтобы ограничить определенное количество символов, используйте { } . {2, 3} означает максимум 2 или 3 символа, так как последняя часть письма обычно состоит из 2 или 3 символов, например, eu или com . (Конечно, в настоящее время есть много других доменов, например, info или ac.u k, но это оставлено читателю в качестве упражнения).

Мы сопоставили точку в домене, избежав ее:

1
>>> regex = "[A-Za-z.]+@[A-Za-z]+\.[A-Za-z]{2,3}"

Если бы мы этого не сделали, это все равно сработало бы:

1
2
3
>>> regex = "[A-Za-z.]+@[A-Za-z]+.[A-Za-z]{2,3}"
>>> re.findall(regex, text)
['java.info@javacodegeeks.com']

но

1
2
3
>>> email="java.info@javacodegeeks~com"
>>> re.findall(regex, email)
['java.info@javacodegeeks~com']

потому что точка (.) соответствует любому символу.

Если вам нужно, чтобы символы не входили в набор символов, используйте символ ^ как в [^A-Za-z] что означает любой символ, который не является буквой.

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

стенография Набор символов Спички
\d (\D) [0-9] ([^0-9]) цифра (не цифра)
\w (\W) [A-Za-z0-9_] ([^A-Za-z0-9_]) слово (не слово)
\s (\S) [ \t\r\n\f] ([^ \t\r\n\f]) пробелы (без пробелов)
\A Начало строки
\Z Конец строки
\b (\B) Граница слова, например, пробелы, запятые, двоеточия, дефисы и т. Д. (Без границы слова)

Итак, предыдущее регулярное выражение также может быть записано как:

1
2
3
>>> regex = "[\w.]+@[\w]+\.[\w]{2,3}"
>>> re.findall(regex, text)
['java.info@javacodegeeks.com']

и чтобы избежать совпадения ".@javacodegeeks.com" :

1
2
3
4
>>> email = ".@javacodegeeks.com"
>>> regex = "\w[\w.]+@[\w]+\.[\w]{2,3}"
>>> re.findall(regex, email)
[]

Чтобы найти первое и последнее слово:

1
2
3
4
5
6
>>> regex = "^\w+"
>>> findMatch(regex, text)
You
>>> regex = "\w+$"
>>> findMatch(regex, text)
17pm

Давайте посмотрим на другой пример:

1
2
3
4
>>> text = "Hello do you want to play Othello?"
>>> regex = "[Hh]ello"
>>> re.findall(regex, text)
['Hello', 'hello']

Откуда приходит второе 'hello' ? От 'Ot hello ' . Как мы можем сказать Python, что хотим соответствовать только целым словам?

1
2
3
>>> regex = r"\b[Hh]ello\b"
>>> re.findall(regex, text)
['Hello']

Обратите внимание, что мы определяем регулярное выражение как необработанную строку, в противном случае мы должны будем ввести:

1
>>> regex = "\\b[Hh]ello\\b"

В Python 3.4 добавлена ​​новая re.fullmatch() которая возвращает объект Match только если регулярное выражение соответствует всей строке, в противном случае возвращается None . re.fullmatch(regex, text) аналогичен re.search("\Aregex\Z", text) . Если text – пустая строка, тогда fullmatch() оценивается как True для любого регулярного выражения, которое может найти совпадение нулевой длины.

Будьте осторожны при использовании отрицательных сокращений внутри квадратных скобок. Например, [\D\S] не совпадает с [^\d\s] . Последний соответствует любому символу, который не является ни цифрой, ни пробелом. Первый, однако, соответствует любому символу, который не является цифрой или не является пробелом. Поскольку все цифры не являются пробелами, а все символы пробелов не являются цифрами, [\D\S] соответствует любому символу; цифра, пробел или другое.

5. Группировка

Представьте, что мы хотим сопоставить только несколько TLD (домен верхнего уровня) с адресами электронной почты:

1
2
3
4
>>> email = "java.info@javacodegeeks.net"
>>> regex = "\w[\w.]+@[\w]+\.com|net|org|edu"
>>> re.findall(regex, email)
['net']

Видимо, | (или) метасимвол здесь не работает. Нам нужно сгруппировать TLD:

1
2
3
>>> regex = "\w[\w.]+@[\w]+\.(com|net|org|edu)"
>>> findMatch(regex, email)
java.info@javacodegeeks.net

Это также может быть полезно, если мы хотим сопоставить имя и домен адреса электронной почты, например

01
02
03
04
05
06
07
08
09
10
11
12
>>> regex = "(\w[\w.]+)@([\w]+\.[\w]{2,3})"
>>> match = re.search(regex, email)
>>> match
<_sre.SRE_Match object; span=(0, 27), match='java.info@javacodegeeks.net'>
>>> match.group()
'java.info@javacodegeeks.net'
>>> match.groups()
('java.info', 'javacodegeeks.net')
>>> match.group(1)
'java.info'
>>> match.group(2)
'javacodegeeks.net'

match.group() или match.group(0) возвращает все совпадение. Чтобы продвинуть пример немного дальше (вложенные группы):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
>>> regex = "(\w[\w.]+)@([\w]+\.(com|net|org|edu))"
>>> match = re.search(regex, email)
>>> match
<_sre.SRE_Match object; span=(0, 27), match='java.info@javacodegeeks.net'>
>>> match.group()
'java.info@javacodegeeks.net'
>>>  match.groups()
('java.info', 'javacodegeeks.net', 'net')
>>> match.group(1)
'java.info'
>>> match.group(2)
'javacodegeeks.net'
>>> match.group(3)
'net'

Если вы обратите внимание на группы скобок, вы увидите 3 группы:

1
2
3
(\w[\w.]+)
([\w]+\.(com|net|org|edu))
(com|net|org|edu)

Результаты match.group() теперь должны быть очевидны.

Если вам не нужна группа для захвата совпадения, используйте группу без захвата с синтаксисом (?:regex) . Например, если мы не хотим включать TLD в наш матч:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
>>> regex = "(\w[\w.]+)@([\w]+\.(?:com|net|org|edu))"
>>> match = re.search(regex, email)
>>> match
<_sre.SRE_Match object; span=(0, 27), match='java.info@javacodegeeks.net'>
>>> match.group()
'java.info@javacodegeeks.net'
>>> match.groups()
('java.info', 'javacodegeeks.net')
>>> match.group(1)
'java.info'
>>> match.group(2)
'javacodegeeks.net'
>>> match.group(3)
Traceback (most recent call last):
  File "", line 1, in
IndexError: no such group

Python был первым языком программирования, который ввел именованные группы захвата. Синтаксис (?P<name>regex) фиксирует совпадение regex в имени обратной ссылки. name должно быть буквенно-цифровой последовательностью, начинающейся с буквы. Вы можете ссылаться на содержимое группы с помощью именованной обратной ссылки \g<name> .

1
2
3
4
>>> regex = "(\w[\w.]+)@([\w]+\.(?P<TLD>com|net|org|edu))"
>>> match = re.search(regex, email)
>>> match.group("TLD")
'net'

Python не позволяет нескольким группам использовать одно и то же имя. Это даст ошибку компиляции регулярного выражения.

В качестве упражнения напишите регулярное выражение адреса в Нидерландах, например:

1
text = 'George Maduroplein 1, 2584 RZ, The Hague, The Netherlands'

Убедитесь, что вы можете вернуть независимые совпадения улицы и номера дома, почтового индекса, города или страны.

5.1 Обратные ссылки

Обратные ссылки соответствуют тому же тексту, что и ранее сопоставляемая группой захвата. Возможно, самый известный пример – это регулярное выражение для поиска дублированных слов.

1
2
3
4
>>> text = "hello hello world"
>>> regex = r"(\w+) \1"
>>> re.findall(regex, text)
['hello']

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

1
>>> regex = "(\w+) \\1"

Обратные ссылки можно использовать с первыми 99 группами. Именованные группы, которые мы видели ранее, могут помочь уменьшить сложность в случае многих групп в регулярном выражении. Для обратной ссылки на именованную группу используйте синтаксис (?P=name) :

1
2
3
>>> regex = r"(?P<word>\w+) (?P=word)"
>>> re.findall(regex, text)
['hello']

6. Согласование режимов

search(regex, string, modes) и match(regex, string, modes) принимают третий параметр, называемый режимами соответствия .

Режим соответствия Группировка письма объяснение
re.I или re.IGNORECASE i Игнорирует дело
re.S или re.DOTALL s делает точку (.) совпадением с новой строкой
re.M или re.MULTILINE m делает совпадение ^ и $ после и до переноса строки
re.L или re.LOCALE L заставляет \w соответствовать всем символам, которые считаются буквами с учетом текущих настроек локали
re.U или re.UNICODE u обрабатывает все буквы из всех сценариев как символы слова

1
2
3
4
>>> text = "gry Grey grey gray gryy grrrr graaaay"
>>> regex="gr[ae]y"
>>> re.search(regex, text, re.I)
<_sre.SRE_Match object; span=(4, 8), match='Grey'>

Используйте | метасимвол для указания нескольких подходящих режимов.

Или вы можете использовать букву группировки, упомянутую в таблице выше:

1
2
3
4
>>> text = "gry Grey grey gray gryy grrrr graaaay"
>>> regex=r"(?i)gr[ae]y"
>>> re.search(regex, text)
<_sre.SRE_Match object; span=(4, 8), match='Grey'>

7. Юникод

Начиная с версии 3.3, Python обеспечивает хорошую поддержку сопоставления с регулярным выражением Unicode. Как упоминалось выше, должен использоваться синтаксис \uFFFF . Например, чтобы соответствовать одной или нескольким цифрам, заканчивающимся :

1
2
3
4
>>> text = 'This item costs 33€.'
>>> regex = "\d+\u20AC"
>>> re.findall(regex, text)
['33€']

8. Резюме

В этом руководстве мы предоставили обзор регулярных выражений и увидели, как мы можем выполнять регулярные выражения в Python. Python предоставляет модуль re для этой работы. Мы видели множество примеров для использования в ваших реальных проектах. Я надеюсь, что после этого урока вы будете меньше бояться регулярных выражений. Эта статья ни в коем случае не является исчерпывающей. Заинтересованный читатель должен посмотреть ссылки для более глубокого знания регулярных выражений. Чтобы проверить себя, в чем разница между символами [] , () и {} в регулярных выражениях?

10. Ссылки

  1. https://www.regular-expressions.info/
  2. Фридл ДЖЕФ (2006), Освоение регулярных выражений , 3-е изд., О’Рейли.
  3. Краснов А. (2017), «Учебник по регулярным выражениям Python» , WebCodeGeeks.
  4. Лопес Ф. и Ромеро В. (2014), Освоение регулярных выражений Python , Packt.

9. Скачать исходный код

Скачать
Вы можете скачать полный исходный код этой статьи здесь: Регулярные выражения Python