Время прочтения: 5 мин.

Поиск информации в неструктурированном тексте – одна из популярнейших задач. Существуют различные подходы к ее решению. Например, для извлечения именованных сущностей можно использовать библиотеку Natasha, можно искать вхождения определённых подстрок и почти всегда используются регулярные выражения. А регулярные выражения не очень понятны на первый взгляд.

В данной статье я постараюсь описать некоторый минимум, необходимый для понимания возможностей регулярных выражений, а также приведу примеры задач, для решения которых я использовал регулярные выражения. Использоваться будет библиотека Python 3.7 «re» и regex101.com, также, информация из статьи. Пример работы:

import re
text = '''
ИНН
.,
ИНН 1234567890
Инн2 - 9876543210
просто числа 1111111111
1377
'''
smf = re.search('\d{10}', text, re.M)
smf[0] if smf else 'Не найдено'

Результат: ‘1234567890’

Рассмотрим подробнее. После импорта библиотеки и создания переменной text была вызвана функция re.search(). Это одна из функций для реализации регулярных выражений в Python, принимает регулярку, ищет где нам нужно первое совпадение, флаг re.M обозначает многострочный поиск и возвращает объект re.Match с найденными координатами или None. Регулярное выражение в данном случае позволяет найти 10 чисел, идущих подряд. «\d» — это обозначение для числа, {10} обозначает количество символов.

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

\nИНН\n.,\nИНН 1234567890\nИнн2 - 9876543210\nпросто числа 1111111111\n1377\n

В Python re существуют следующие функции:

  • re.match(pattern, string, flags=0) ищет pattern в string с флагами flags, возвращает объекты match или None. Результаты можно получить как match.groups() и match.group(num=0).
  • re.search(pattern, string, flags=0) ищет pattern в string с флагами flags, возвращает первое совпадение как объект match или None.
  • re.findall(pattern, string, flags=0) ищет pattern в string с флагами flags, возвращает объекты список совпадений или пустой список.
  • re.sub(pattern, repl, string, max=0) ищет pattern в string и заменяет на repl, количество раз max, если оно указано.

Флаги re:

  • re.I — делает поиск нечувствительным к регистру.
  • re.L — ищет слова в соответствии с текущим языком. Эта интерпретация затрагивает алфавитную группу (\w и \W), а также поведение границы слова (\b и \B).
  • re.M — символ $ выполняет поиск в конце любой строки текста (не только конце текста) и символ ^ выполняет поиск в начале любой строки текста (не только в начале текста).
  • re.S — изменяет значение точки (.) на совпадение с любым символом, включая новую строку.
  • re.U— интерпретирует буквы в соответствии с набором символов Unicode. Этот флаг влияет на поведение \w, \W, \b, \B. В python 3+ этот флаг установлен по умолчанию.
  • re.X— позволяет многострочный синтаксис регулярного выражения. Он игнорирует пробелы внутри паттерна (за исключением пробелов внутри набора [] или при экранировании обратным слешем) и обрабатывает не экранированный “#” как комментарий.

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

ОбозначениеОписаниеПример в нашей строке
.любой одиночный символ, кроме «\n»«И»
\символ для экранированиядля «\.» результат – «.»
\wлюбая буква, цифра или «_»«И»
\dлюбая цифра«1»
\sлюбой символ пробела, табуляции, перевода строки«\n»
\bдолжен определять границу для \w символов, но в Python 3.7, видимо, не работаетв regex101 можно было бы использовать для поиска отдельно стоящих слов/цифр, например, «\b\d{3}\b» позволяет искать три идущих подряд цифры
*ноль или больше совпаденийдля «ИНН.*» результат: «ИНН»
+одно или больше совпаденийдля «ИНН.+» результат: «ИНН 1234567890»
^начало строкидля «^\d+» результат: «1377»
$конец строкидля «\d+$» результат: «1234567890»
|одно или другое«а|5» — буква «а» или цифра «5»
{ }обозначает количество искомых символов«\d{5}» — 5 цифр,
«\d{3,6}» — 3-6 цифр,
«\d{4,}» — 4 и более цифр
[ ]обозначает список символов для поиска, при этом, «^» внутри таких скобок будет означать отрицание«[дес]» — только буквы «д», «е», «с»,
«[ёа-я]» — все прописные буквы кириллицы (ё не входит в основной список),
«[^0-9]» — не цифры
()обозначает группы«(ИНН (\d{10}))» вернёт две группы — ‘ИНН 9876543210’ и ‘9876543210’

В регулярных выражениях достаточно удобно реализованы логические отрицания, например, «\D» — это любой нецифровой символ. Аналогично и для других обозначений: «\W», «\S», «\B». Пару слов о «жадности» и «ленивости» регулярных выражений. По умолчанию, возвращается максимально длинное совпадение, например:

text = '"Рога", "Копыта" и "Ко"'
re.findall('".+"', text, re.M)

Результат: [‘»Рога», «Копыта» и «Ко»‘]

Мы хотели найти «всё внутри двойных кавычек». И нашли всё, что заключено в кавычки, как единое совпадение. Но если нужно каждое совпадение в отдельности, следует использовать «ленивый поиск»:

text = '"Рога", "Копыта" и "Ко"'
re.findall('".+?"', text, re.M)

Результат: [‘»Рога»‘, ‘»Копыта»‘, ‘»Ко»‘]

Добавив «?» к конструкции «».+»», то есть, составив выражение «».+?»» мы получили каждое минимальное совпадение с конструкцией «всё внутри двойных кавычек».

Далее – опережающие и ретроспективные проверки или же, lookahead/lookbehind. Эти конструкции позволяют реализовать поиск чего-то после и/или до определённых выражений. То есть, мы можем, не используя группы (круглые скобки), вытащить, например, что-то между кавычек:

text = '"Рога", "Копыта" и "Ко"'
re.findall('(?<=").+?(?=")', text, re.M)

Результат: [‘Рога’, ‘, ‘, ‘Копыта’, ‘ и ‘, ‘Ко’]

Заметим, что вернулось вообще всё между кавычек, даже запятые и пробелы, поэтому стоит использовать данный инструмент аккуратно. Более показателен будет следующий пример:

text = 'какие-то 146%, нужные проценты значение 54%, шелуха значение 987, ещё значение - 159%'
re.findall('(?<=значение).+?(\d+)(?=%)', text, re.M)

Результат: [’54’, ‘159’]

Разберём пример подробнее. Допустим, нам нужно найти в тексте число, которое следует после определённого слова (у нас – «значение»), при этом, между словом и числом может быть пробел, тире, что-то ещё, и после числа точно должен быть «%». Для понимания конструкции выше понадобится описание опережающих и ретроспективных проверок:

  • (?<=pattern) —  положительное look-behind условие;
  • (?<!pattern) — отрицательное look-behind условие;
  • (?=pattern) — положительное look-ahead условие;
  • (?!pattern) — отрицательное look-ahead условие.

Если вы подумали об указании множественных условий для таких проверок, что-то вроде, (?<=значение1|значение2) то спешу разочаровать, так как re говорит нам:

«error: look-behind requires fixed-width pattern»

И это печально. Следует учитывать, что некоторые особенности регулярок могут по-разному работать в разных реализациях, поэтому стоит «обкатывать» регулярки не только на regex101, но и в самом Python.

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