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

Регулярные выражения — мощный язык для сопоставления текстовых шаблонов. На этой странице дается базовое введение в сами регулярные выражения, достаточное для наших упражнений на Python, и показано, как регулярные выражения работают в Python. Модуль Python «re» обеспечивает поддержку регулярных выражений.

В Python поиск по регулярному выражению обычно записывается так:

match = re.search(pat, str)

Метод re.search() принимает шаблон регулярного выражения и строку и ищет этот шаблон внутри строки. Если поиск успешен, функция search() возвращает объект соответствия или None в противном случае. Поэтому за поиском обычно сразу следует оператор if, чтобы проверить, удался ли поиск, как показано в следующем примере, который ищет шаблон «слово:», за которым следует слово из 3 букв (подробности ниже):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

Код match = re.search(pat, str) сохраняет результат поиска в переменной с именем match. Затем оператор if проверяет совпадение: если оно истинно, поиск удался и match.group() представляет собой совпадающий текст (например, «слово: кот»). В противном случае, если совпадение ложное (точнее, нет), поиск не удался и соответствующего текста нет.

Символ «r» в начале строки шаблона обозначает «необработанную» строку Python, которая без изменений проходит через обратную косую черту, что очень удобно для регулярных выражений (Java очень нуждается в этой функции!). Я рекомендую вам всегда писать шаблонные строки с буквой «r», просто по привычке.

Основные узоры

Сила регулярных выражений в том, что они могут указывать шаблоны, а не только фиксированные символы. Вот самые основные шаблоны, соответствующие отдельным символам:

  • a, X, 9, < — обычные символы точно соответствуют самим себе. Метасимволы, которые не совпадают друг с другом, поскольку имеют особое значение: . ^ $ * + ? { [ ] \ | ( ) (подробности ниже)
  • . (точка) — соответствует любому одиночному символу, кроме символа новой строки '\n'
  • \w -- (строчная буква w) соответствует символу слова: букве, цифре или подчеркиванию [a-zA-Z0-9_]. Обратите внимание: хотя мнемоникой для этого является слово «слово», оно соответствует только одному слову char, а не целому слову. \W (заглавная буква W) соответствует любому символу, не являющемуся словом.
  • \b — граница между словом и несловом
  • \s -- (строчная буква s) соответствует одному символу пробела -- пробел, новая строка, возврат, табуляция, форма [ \n\r\t\f]. \S (заглавная буква S) соответствует любому символу без пробелов.
  • \t, \n, \r -- вкладка, новая строка, возврат
  • \d — десятичная цифра [0-9] (некоторые старые утилиты регулярных выражений не поддерживают \d, но все они поддерживают \w и \s)
  • ^ = начало, $ = конец — соответствует началу или концу строки
  • \ -- подавлять "особенность" персонажа. Так, например, используйте \. для соответствия точке или \\ для соответствия косой черте. Если вы не уверены, имеет ли символ особое значение, например «@», попробуйте поставить перед ним косую черту \@. Если это недопустимая escape-последовательность, например \c, ваша программа Python завершится с ошибкой.

Основные примеры

Шутка: как называют свинью с тремя глазами? пиииг!

Основные правила поиска шаблона внутри строки с помощью регулярных выражений:

  • Поиск продолжается по строке от начала до конца, останавливаясь на первом найденном совпадении.
  • Должен совпадать весь шаблон, но не вся строка.
  • Если match = re.search(pat, str) выполнен успешно, значение match не равно None и, в частности, match.group() является совпадающим текстом.
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Повторение

Все становится интереснее, если вы используете + и * для указания повторения в шаблоне.

  • + -- 1 или несколько вхождений шаблона слева от него, например 'i+' = один или несколько i
  • * -- 0 или более вхождений шаблона слева от него
  • ? -- сопоставить 0 или 1 вхождение шаблона слева от него

Крайний левый и самый большой

Сначала поиск находит самое левое совпадение с шаблоном, а во-вторых, он пытается использовать как можно большую часть строки - т.е. + и * идут как можно дальше (+ и * называются "жадными").

Примеры повторения

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Пример электронной почты

Предположим, вы хотите найти адрес электронной почты внутри строки «xyz alice-b@google.com Purple Monkey». Мы будем использовать это в качестве рабочего примера, чтобы продемонстрировать дополнительные возможности регулярных выражений. Вот попытка использования шаблона r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

В этом случае поиск не дает полного адреса электронной почты, поскольку \w не соответствует '-' или '.' в адресе. Мы исправим это, используя функции регулярных выражений ниже.

Квадратные скобки

Квадратные скобки можно использовать для обозначения набора символов, поэтому [abc] соответствует «a», «b» или «c». Коды \w, \s и т. д. также работают внутри квадратных скобок, за одним исключением: точка (.) означает просто буквальную точку. В случае с электронной почтой квадратные скобки — простой способ добавить «.». и '-' для набора символов, которые могут появляться вокруг @ с шаблоном r'[\w.-]+@[\w.-]+', чтобы получить полный адрес электронной почты:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(дополнительные функции квадратных скобок) Для обозначения диапазона также можно использовать тире, чтобы [az] соответствовало всем строчным буквам. Чтобы использовать тире без указания диапазона, ставьте тире последним, например [abc-]. Верхняя шляпка (^) в начале набора квадратных скобок инвертирует его, поэтому [^ab] означает любой символ, кроме «a» или «b».

Групповое извлечение

Функция «группировки» регулярного выражения позволяет вам выбирать части совпадающего текста. Предположим, что для проблемы с электронной почтой мы хотим извлечь имя пользователя и хост отдельно. Для этого добавьте круглые скобки ( ) вокруг имени пользователя и хоста в шаблоне, например: r'([\w.-]+)@([\w.-]+)'. В этом случае круглые скобки не меняют то, чему будет соответствовать шаблон, а вместо этого создают логические «группы» внутри текста совпадения. При успешном поиске match.group(1) — это текст совпадения, соответствующий первой левой скобке, а match.group(2) — это текст, соответствующий второй левой скобке. Простой match.group() по-прежнему представляет собой весь текст совпадения, как обычно.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

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

найти все

findall() — вероятно, самая мощная функция в модуле re. Выше мы использовали re.search() для поиска первого соответствия шаблону. findall() находит *все* совпадения и возвращает их в виде списка строк, где каждая строка представляет одно совпадение.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

findall с файлами

Для файлов у вас может быть привычка писать цикл для перебора строк файла, а затем вы можете вызывать findall() для каждой строки. Вместо этого позвольте findall() выполнить итерацию за вас — гораздо лучше! Просто введите весь текст файла в функцию findall() и позвольте ей вернуть список всех совпадений за один шаг (напомним, что f.read() возвращает весь текст файла в одной строке):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall и группы

Механизм группировки круглых скобок ( ) можно комбинировать с функцией findall(). Если шаблон включает в себя две или более группы круглых скобок, то вместо того, чтобы возвращать список строк, findall() возвращает список *кортежей*. Каждый кортеж представляет одно совпадение шаблона, а внутри кортежа находятся данные group(1), group(2).. Таким образом, если к шаблону электронного письма добавлены две группы круглых скобок, то findall() возвращает список кортежей, каждый длиной 2, содержащий имя пользователя и хост, например («alice», «google.com»).

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Получив список кортежей, вы можете выполнить по нему циклические вычисления для каждого кортежа. Если шаблон не содержит скобок, то findall() возвращает список найденных строк, как в предыдущих примерах. Если шаблон включает один набор круглых скобок, то findall() возвращает список строк, соответствующих этой единственной группе. (Непонятная дополнительная функция: иногда в шаблоне есть группы скобок ( ), но вы не хотите их извлекать. В этом случае напишите скобки с ?: в начале, например (?: ), и эта левая скобка будет не засчитывается как групповой результат.)

Рабочий процесс и отладка RE

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

Параметры

Функции re принимают параметры для изменения поведения сопоставления с образцом. Флаг опции добавляется в качестве дополнительного аргумента в функцию search() или findall() и т. д., например, re.search(pat, str, re.IGNORECASE).

  • IGNORECASE — игнорировать различия в верхнем и нижнем регистре при сопоставлении, поэтому «a» соответствует как «a», так и «A».
  • DOTALL — разрешить точке (.) соответствовать символу новой строки — обычно она соответствует чему угодно, кроме символа новой строки. Это может вас сбить с толку — вы думаете, что .* соответствует всему, но по умолчанию оно не выходит за конец строки. Обратите внимание, что \s (пробелы) включает символы новой строки, поэтому, если вы хотите сопоставить пробелы, которые могут включать новую строку, вы можете просто использовать \s*
  • MULTILINE — внутри строки, состоящей из многих строк, позвольте ^ и $ соответствовать началу и концу каждой строки. Обычно ^/$ соответствует началу и концу всей строки.

Жадный против нежадного (необязательно)

Это необязательный раздел, в котором показан более продвинутый метод регулярных выражений, не необходимый для упражнений.

Предположим, у вас есть текст с тегами: <b>foo</b> и <i>так далее</i>.

Предположим, вы пытаетесь сопоставить каждый тег с шаблоном «(<.*>)» — чему он соответствует в первую очередь?

Результат немного неожиданный, но жадный аспект .* заставляет его сопоставлять все '<b>foo</b> и <i>so on</i>' как одно большое совпадение. Проблема в том, что .* идет настолько далеко, насколько это возможно, вместо того, чтобы остановиться на первом > (он же «жадный»).

Существует расширение регулярного выражения, в которое вы добавляете знак ? в конце, например .*? или .+?, изменив их на нежадные. Теперь они останавливаются, как только могут. Таким образом, шаблон '(<.*?>)' получит только '<b>' в качестве первого совпадения и '</b>' в качестве второго совпадения, и так далее, получая каждую пару <..> по очереди. Обычно используется стиль .*? сразу за которым следует конкретный маркер (в данном случае >), которому соответствует .*? пробег вынужден продлить.

*? Расширение возникло в Perl, а регулярные выражения, включающие расширения Perl, известны как Perl-совместимые регулярные выражения — pcre. Python включает поддержку pcre. Многие утилиты командной строки и т. д. имеют флаг, позволяющий принимать шаблоны pcre.

Более старый, но широко используемый метод кодирования идеи «всех этих символов, кроме остановки на X», использует стиль квадратных скобок. Для приведенного выше примера вы можете написать шаблон, но вместо .*, чтобы получить все символы, используйте [^>]*, который пропускает все символы, кроме > (ведущий ^ "инвертирует" набор квадратных скобок, поэтому он соответствует любой символ, не заключенный в скобки).

Замена (необязательно)

Функция re.sub(pat, replace, str) ищет все экземпляры шаблона в заданной строке и заменяет их. Строка замены может включать «\1», «\2», которые относятся к тексту из группы(1), группы(2) и т. д. из исходного соответствующего текста.

Вот пример, который ищет все адреса электронной почты и изменяет их, чтобы сохранить пользователя (\1), но указать в качестве хоста yo-yo-dyne.com.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Упражнение

Чтобы попрактиковаться в использовании регулярных выражений, см. Упражнение «Имена детей» .