В этой статье мы представляем исчерпывающую статью о регулярных выражениях на языке 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 ), либо на одном из следующих сайтов онлайн-регулярных выражений (список не является исчерпывающим, и вы можете найти лучший сайт для своих нужд или вкуса):
- https://regex101.com/
- https://pythex.org/
- http://www.pyregex.com/
- https://www.debuggex.com/
- https://www.regextester.com/
- http://www.regexplanet.com/advanced/python/index.html
Самое простое регулярное выражение — это просто текстовая строка без каких-либо специальных символов, то есть литерала . Литералы — это самая простая форма сопоставления с образцом в регулярных выражениях. Они будут просто успешны всякий раз, когда этот литерал будет найден в тексте, в котором вы ищите. Например, давайте найдем шаблон 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, inIndexError: 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. Ссылки
- https://www.regular-expressions.info/
- Фридл ДЖЕФ (2006), Освоение регулярных выражений , 3-е изд., О’Рейли.
- Краснов А. (2017), «Учебник по регулярным выражениям Python» , WebCodeGeeks.
- Лопес Ф. и Ромеро В. (2014), Освоение регулярных выражений Python , Packt.