Регулярные выражения Python для новичков: что это, зачем и для чего

Автор оригинала: Sunil Ray
  • Перевод
image

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

И вот здесь-то нам как раз и нужны регулярные выражения. Парсинг всего текста или его фрагментов с веб-страниц, анализ данных Twitter или подготовка данных для анализа текста — регулярные выражения приходят на помощь.

Кстати, свои советы по некоторым функциям добавил Алексей Некрасов — лидер направления Python в МТС, программный директор направления Python в Skillbox. Чтобы было понятно, где перевод, а где — комментарии, последние мы выделим цитатой.

Зачем нужны регулярные выражения?


Они помогают быстро решить самые разные задачи при работе с данными:
  • Определить нужный формат данных, включая телефонный номер или e-mail адрес.
  • Разбивать строки на подстроки.
  • Искать, извлекать и заменять символы.
  • Быстро выполнять нетривиальные операции.

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

Когда регулярные выражения не нужны? Когда есть аналогичная встроенная в Python функция, а таких немало.

А что там с регулярными выражениями в Python?


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

import re

Что касается самых востребованных методов, предоставляемых модулем, то вот они:

  • re.match()
  • re.search()
  • re.findall()
  • re.split()
  • re.sub()
  • re.compile()

Давайте рассмотрим каждый из них.

re.match(pattern, string)

Метод предназначен для поиска по заданному шаблону в начале строки. Так, если вызвать метод match() на строке «AV Analytics AV» с шаблоном «AV», то его получится успешно завершить.

import re
result = re.match(r'AV', 'AV Analytics Vidhya AV')
print(result) 
Результат:
<_sre.SRE_Match object at 0x0000000009BE4370>

Здесь мы нашли искомую подстроку. Для вывода ее содержимого используется метод group(). При этом используется «r» перед строкой шаблона, чтобы показать, что это raw-строка в Python.

result = re.match(r'AV', 'AV Analytics Vidhya AV')
print(result.group(0))
 
Результат:
AV

Окей, теперь давайте попробуем найти «Analythics» в этой же строке. У нас ничего не получится, поскольку строка начинается на «AV», метод возвращает none:

result = re.match(r'Analytics', 'AV Analytics Vidhya AV')
print(result)
 
Результат:
None

Методы start() и end() используются для того, чтобы узнать начальную и конечную позицию найденной строки.

result = re.match(r'AV', 'AV Analytics Vidhya AV')
print(result.start())
print(result.end())
 
Результат:
0
2

Все эти методы крайне полезны в ходе работы со строками.

re.search(pattern, string)

Этот метод похож на match(), но его отличие в том, что ищет он не только в начале строки. Так, search() возвращает объект, если мы пробуем найти «Analythics».

result = re.search(r'Analytics', 'AV Analytics Vidhya AV')
print(result.group(0))
 
Результат:
Analytics

Что касается метода search (), то он ищет по всей строке, возвращая, впрочем, лишь первое найденное совпадение.

re.findall(pattern, string)

Здесь у нас возврат всех найденных совпадений. Так, у метода findall() нет никаких ограничений на поиск в начале или конце строки. Например, если искать «AV» в строке, то мы получим возврат всех вхождений «AV». Для поиска рекомендуется использовать как раз этот метод, поскольку он умеет работать как re.search(), так и как re.match().

result = re.findall(r'AV', 'AV Analytics Vidhya AV')
print(result)
 
Результат:
['AV', 'AV']

re.split(pattern, string, [maxsplit=0])

Этот метод разделяет строку по заданному шаблону.

result = re.split(r'y', 'Analytics')
print(result)
 
Результат:
['Anal', 'tics']

В указанном примере слово «Analythics» разделено по букве «y». Метод split() здесь принимает и аргумент maxsplit со значением по умолчанию, равным 0. Таким образом он разделяет строку столько раз, сколько это возможно. Правда, если указать этот аргумент, то разделение не может быть выполнено более указанного количества раз. Вот несколько примеров:

result = re.split(r'i', 'Analytics Vidhya')
print(result)
 
Результат:
['Analyt', 'cs V', 'dhya'] # все возможные участки.
 
result = re.split(r'i', 'Analytics Vidhya', maxsplit=1)
print(result)
 
Результат:
['Analyt', 'cs Vidhya']

Здесь параметр maxsplit установлен равным 1, в результате чего строка разделена на две части вместо трех.

re.sub(pattern, repl, string)

Помогает найти шаблон в строке, заменяя на указанную подстроку. Если же искомое не найдено, то строка остается неизменной.

result = re.sub(r'India', 'the World', 'AV is largest Analytics community of India')
print(result)
 
Результат:
'AV is largest Analytics community of the World'

re.compile(pattern, repl, string)

Здесь мы можем собрать регулярное выражение в объект, который в свою очередь можно использовать для поиска. Такой вариант позволяет избежать переписывания одного и того же выражения.

pattern = re.compile('AV')
result = pattern.findall('AV Analytics Vidhya AV')
print(result)
result2 = pattern.findall('AV is largest analytics community of India')
print(result2)
 
Результат:
['AV', 'AV']
['AV']

До этого момента мы рассматривали вариант с поиском определенной последовательности символов? В этом случае никакого шаблона нет, набор символов требуется вернуть в порядке, соответствующему определенным правилам. Такая задача встречается часто при работе с извлечением информации из строк. И сделать это несложно, нужно лишь написать выражение с использованием спец. символов. Вот те, что встречаются чаще всего:

  • . Один любой символ, кроме новой строки \n.
  • ? 0 или 1 вхождение шаблона слева
  • + 1 и более вхождений шаблона слева
  • * 0 и более вхождений шаблона слева
  • \w Любая цифра или буква (\W — все, кроме буквы или цифры)
  • \d Любая цифра [0-9] (\D — все, кроме цифры)
  • \s Любой пробельный символ (\S — любой непробельный символ)
  • \b Граница слова
  • [..] Один из символов в скобках ([^..] — любой символ, кроме тех, что в скобках)
  • \ Экранирование специальных символов (\. означает точку или \+ — знак «плюс»)
  • ^ и $ Начало и конец строки соответственно
  • {n,m} От n до m вхождений ({,m} — от 0 до m)
  • a|b Соответствует a или b
  • () Группирует выражение и возвращает найденный текст
  • \t, \n, \r Символ табуляции, новой строки и возврата каретки соответственно

Понятно, что символов может быть и больше. Информацию о них можно найти в документации для регулярных выражений в Python 3.

Несколько примеров использования регулярных выражений


Пример 1. Возвращение первого слова из строки

Давайте сначала попробуем получить каждый символ с использованием (.)

result = re.findall(r'.', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['A', 'V', ' ', 'i', 's', ' ', 'l', 'a', 'r', 'g', 'e', 's', 't', ' ', 'A', 'n', 'a', 'l', 'y', 't', 'i', 'c', 's', ' ', 'c', 'o', 'm', 'm', 'u', 'n', 'i', 't', 'y', ' ', 'o', 'f', ' ', 'I', 'n', 'd', 'i', 'a']


Теперь сделаем то же самое, но чтобы в конечный результат не попал пробел, используем \w вместо (.)

result = re.findall(r'\w', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['A', 'V', 'i', 's', 'l', 'a', 'r', 'g', 'e', 's', 't', 'A', 'n', 'a', 'l', 'y', 't', 'i', 'c', 's', 'c', 'o', 'm', 'm', 'u', 'n', 'i', 't', 'y', 'o', 'f', 'I', 'n', 'd', 'i', 'a']

Ну а теперь проделаем аналогичную операцию с каждым словом. Используем при этом * или +.

result = re.findall(r'\w*', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', '', 'is', '', 'largest', '', 'Analytics', '', 'community', '', 'of', '', 'India', '']

Но и здесь в результате оказались пробелы. Причина — * означает «ноль или более символов». "+" поможет нам их убрать.

result = re.findall(r'\w+', 'AV is largest Analytics community of India')
print(result)
Результат:
['AV', 'is', 'largest', 'Analytics', 'community', 'of', 'India']

Теперь давайте извлечем первое слово с использованием
^:

result = re.findall(r'^\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV']

А вот если использовать $ вместо ^, то получаем последнее слово, а не первое:

result = re.findall(r'\w+$', 'AV is largest Analytics community of India')
print(result)
 
Результат:
[‘India’]
 

Пример 2. Возвращаем два символа каждого слова

Здесь, как и выше, есть несколько вариантов. В первом случае, используя \w, извлекаем два последовательных символа, кроме тех, что с пробелами, из каждого слова:

result = re.findall(r'\w\w', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', 'is', 'la', 'rg', 'es', 'An', 'al', 'yt', 'ic', 'co', 'mm', 'un', 'it', 'of', 'In', 'di']


Теперь пробуем извлечь два последовательных символа с использованием символа границы слова (\b):

result = re.findall(r'\b\w.', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', 'is', 'la', 'An', 'co', 'of', 'In']

Пример 3. Возвращение доменов из списка адресов электронной почты.

На первом этапе возвращаем все символы после @:

result = re.findall(r'@\w+', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')
print(result)
 
Результат:
['@gmail', '@test', '@analyticsvidhya', '@rest']

В итоге части «.com», «.in» и т. д. не попадают в результат. Чтобы исправить это, нужно поменять код:

result = re.findall(r'@\w+.\w+', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')
print(result)
 
Результат:
['@gmail.com', '@test.in', '@analyticsvidhya.com', '@rest.biz']

Второй вариант решения той же проблемы — извлечение лишь домена верхнего уровня с использованием "()":

result = re.findall(r'@\w+.(\w+)', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')
print(result)
 
Результат:
['com', 'in', 'com', 'biz']

Пример 4. Получение даты из строки

Для этого необходимо использовать \d

result = re.findall(r'\d{2}-\d{2}-\d{4}', 'Amit 34-3456 12-05-2007, XYZ 56-4532 11-11-2011, ABC 67-8945 12-01-2009')
print(result)
 
Результат:
['12-05-2007', '11-11-2011', '12-01-2009']

Для того, чтобы извлечь только год, помогают скобки:

result = re.findall(r'\d{2}-\d{2}-(\d{4})', 'Amit 34-3456 12-05-2007, XYZ 56-4532 11-11-2011, ABC 67-8945 12-01-2009')
print(result)
 
Результат:
['2007', '2011', '2009']

Пример 5. Извлечение слов, начинающихся на гласную

На первом этапе нужно вернуть все слова:

result = re.findall(r'\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', 'is', 'largest', 'Analytics', 'community', 'of', 'India']

После этого лишь те, что начинаются на определенные буквы, с использованием "[]":
result = re.findall(r'[aeiouAEIOU]\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', 'is', 'argest', 'Analytics', 'ommunity', 'of', 'India']

В полученном примере есть два укороченные слова, это «argest» и «ommunity». Для того, чтобы убрать их, нужно воспользоваться \b, что необходимо для обозначения границы слова:
result = re.findall(r'\b[aeiouAEIOU]\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['AV', 'is', 'Analytics', 'of', 'India']


Кроме того, можно использовать и ^ внутри квадратных скобок, что помогает инвертировать группы:

result = re.findall(r'\b[^aeiouAEIOU]\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
[' is', ' largest', ' Analytics', ' community', ' of', ' India']

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

result = re.findall(r'\b[^aeiouAEIOU ]\w+', 'AV is largest Analytics community of India')
print(result)
 
Результат:
['largest', 'community']

Пример 6. Проверка формата телефонного номера

В нашем примере длина номера — 10 знаков, начинается он с 8 или 9. Для проверки списка телефонных номеров используем:

li = ['9999999999', '999999-999', '99999x9999']
 
for val in li:
    if re.match(r'[8-9]{1}[0-9]{9}', val) and len(val) == 10:
            print('yes')
    else:
            print('no')
 
Результат:
yes
no
no

Пример 7. Разбиваем строку по нескольким разделителям

Здесь у нас несколько вариантов решения. Вот первое:

line = 'asdf fjdk;afed,fjek,asdf,foo' # String has multiple delimiters (";",","," ").
result = re.split(r'[;,\s]', line)
print(result)
 
Результат:
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

Кроме того, можно использовать метод re.sub() для замены всех разделителей пробелами:

line = 'asdf fjdk;afed,fjek,asdf,foo'
result = re.sub(r'[;,\s]', ' ', line)
print(result)
 
Результат:
asdf fjdk afed fjek asdf foo

Пример 8. Извлекаем данные из html-файла

В этом примере извлекаем данные из html-файла, которые заключены между и , кроме первого столбца с номером. Считаем также, что html-код содержится в строке.

Пример файла

1 Noah Emma
2 Liam Olivia
3 Mason Sophia
4 Jacob Isabella
5 William Ava
6 Ethan Mia
7 Michael Emily

Для того, чтобы решить эту задачу, выполняем следующую операцию:

result=re.findall(r'<td>\w+</td>\s<td>(\w+)</td>\s<td>(\w+)</td>',str)
print(result)
Output:
[('Noah', 'Emma'), ('Liam', 'Olivia'), ('Mason', 'Sophia'), ('Jacob', 'Isabella'), ('William', 'Ava'), ('Ethan', 'Mia'), ('Michael', 'Emily')]


Комментарий Алексея

При написании любых regex в коде придерживаться следующих правил:

  • Используйте re.compile для любых более менее сложных и длинных регулярных выражениях. А также избегайте многократного вызова re.compile на один и тот же regex.
  • Пишите подробные регулярные выражения используя дополнительный аргумент re.VERBOSE. При re.compile используйте флаг re.VERBOSE пишите regex в несколько строк с комментариями о том что происходит. Смотрите документацию по ссылкам тут и тут.

Пример:
компактный вид

pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
re.search(pattern, 'MDLV')


Подробный вид

pattern = """
    ^                   # beginning of string
    M{0,3}              # thousands - 0 to 3 Ms
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    """
re.search(pattern, 'M’, re.VERBOSE)

Используйте named capture group для всех capture group, если их больше чем одна (?P...). (даже если одна capture, тоже лучше использовать).
regex101.com отличный сайт для дебага и проверки regex

При разработке регулярного выражения, нужно не забывать и про его сложность выполнения иначе можно наступить на те же грабли, что и относительно недавно наступила Cloudflare.
Skillbox
Онлайн-университет профессий будущего

Комментарии 7

    –3

    Епрст.
    Что за петросяны.
    После примера "Anal vid hya" уже дальше читать страшно.
    Я понимаю, что если погрузиться в классическое изучение perl regexp (про которое, кстати, новичку вообще ни гугу), то можно в конце и узреть подобное… Но чтобы так… Сразу.

      –1

      Анонимные злобные минусаиоры подскачили. Аргументов нет, но есть "свое мнение". Але, гараж! Дедушка Фрейд от ваших примеров вертится, как Моцарт.

      –1

      Если у вас есть проблема, которую можно решить с помощью регэкспов, значит у вас 2 проблемы ;)

        +1

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


        Например, берём структурированные данные и гоняем регулярки, как в Пример 4. Получение даты из строки и Пример 8. Извлекаем данные из html-файла. Годные костыли для мелких задач очень удобно, а вот в долговременные проекты лучше не пихать.


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


        Я комфортно работаю с регулярками и поэтому при ревью их не пролистываю в ужасе.
        Большие выражения часто являются источником проблем.

          +1

          Парсить html регэкспами — это точно очень плохая идея.

            0

            Именно это я и имел ввиду под "простые операции сложно и дорого"

            0
            Пример 4. Получение даты из строки

            Лью горькие слёзы

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое