Введение в программирование на Python

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

Показывать лекцию целиком

По ссылке youtube выложено видео с русскими титрами.

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

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

Регулярные выражения представляют собой почти что небольшой самостоятельный язык программирования для поиска и разбора строк. Ему зачастую посвящают целые книги. В этой главе мы затронем лишь базовые возможности регулярных выражений. Более детальное описание доступно по адресам: http://en.wikipedia.org/wiki/Regular_expression и http://docs.python.org/library/re.html.

Библиотека для работы с регулярными выражениями должна быть импортирована в вашу программу. Простейший пример использования регулярных выражений – функция поиска search(). Следующая программа демонстрирует тривиальное использование функции поиска.

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('From:', line) :
print line
  

Здесь открывается файл и в цикле просматриваются все его строки; функция поиска регулярного выражения search() используется только для того, чтобы напечатать строки, содержащие фрагмент "From:". Данная программа не использует всей силы регулярных выражений, поскольку мы могли бы попросту воспользоваться строковым методом line.find() и получить тот же самый результат. Сила регулярных выражений раскрывается, когда мы добавляем специальные символы в шаблон поиска, что позволяет более точно задавать соответствие строк шаблону. Применение этих специальных символов в регулярном выражении дает возможность устанавливать сложное сопоставление и извлечение фрагментов текста с помощью совсем короткого программного кода.

Например, символ "^" (крышка) в регулярном выражении означает фрагмент в начале строки. Можно изменить нашу программу так, чтобы она находила только строки, содержащие слово "From: " в начале строки:

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:', line) :
print line
  

Теперь шаблону сопоставляются только строки, начинающиеся с фрагмента "From: ". Это всё ещё совсем простой пример, который мы могли бы реализовать и с использованием метода startswith() из библиотеки работы со строками. Но он показывает, как специальные символы в регулярных выражениях расширяют наши возможности при сопоставлении фрагментов текста шаблону поиска.

23.1. Сопоставление символов в регулярных выражениях

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

В следующем примере регулярное выражение "F..m:" сопоставляется любой из строк "From:", "Fxxm:", "F12m:", or "F!@m:", поскольку точка в регулярном выражении соответствует любому символу.

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^F..m:', line) :
print line
  

Это особенно эффективно в сочетании с возможностью указать, что символ может быть повторен произвольное количество раз, – для этого в регулярном выражении применяются специальные символы "*" или "+". Они означают, что вместо одного символа строки поиска сопоставляется ноль или более символов в случае звёздочки и один или более символов в случае знака плюс.

Мы можем еще более сузить множество строк, которые сопоставляются регулярному выражению, используя повторение произвольного символа (т.е. точки) в следующем примере:

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:.+@', line) :
print line
  

Шаблон поиска "^From:.+@" успешно сопоставляется всем строкам, начинающимся со слова "From:", за которым следует один или более произвольных символов и затем символ @. Например, он соответствует следующей строке:

From: stephen.marquard@uct.ac.za

Часть шаблона ".+" можно представлять себе как фрагмент, который расширяется до соответствия всем символам между двоеточием и символом @.

From:.+@

Удобно представлять плюс и звездочку как расширяющие символы. Например, в следующей строке символу @ шаблона будет соответствовать последний символ @ строки, тогда как фрагмент ".+" шаблона расширяется до всех символов после двоеточия, предшествующих ему:

From: stephen.marquard@uct.ac.za, csev@umich.edu, and cwen@iupui.edu

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

23.2. Извлечение данных с помощью регулярных выражений

Для выделения данных из строки Питон предоставляет метод findall(), извлекающий все подстроки, соответствующие регулярному выражению. Пусть мы хотим извлечь всё, что похоже на адрес электронной почты, из любой строки, независимо от ее формата. Например, мы хотим извлечь e-mail адрес из любой из следующих строк:

From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008
Return-Path: <postmaster@collab.sakaiproject.org>
for <source@collab.sakaiproject.org>;
Received: (from apache@localhost)
Author: stephen.marquard@uct.ac.za
  

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

import re
s = 'Hello from csev@umich.edu to cwen@iupui.edu about the meeting @2PM'
lst = re.findall('\S+@\S+', s)
print lst
  

Метод findall() производит поиск регулярного выражения в строке, переданной ему в качестве второго аргумента, и возвращает список всех ее подстрок, которые выглядят как e-mail адреса. Мы используем специальную двухсимвольную последовательность \S длины 2, которая сопоставляется любому непробельному (non-whitespace) символу. На выходе программы получим:

['csev@umich.edu', 'cwen@iupui.edu']

Сопоставляя регулярное выражение, мы находим все подстроки, имеющие хотя бы один непробельный символ, за которым следует символ @, после которого в свою очередь идет один или более непробельный символ. Шаблон "\S+" аналогично сопоставляется максимально длинной последовательности непробельных символов (это называют "жадным" поведением в регулярных выражениях).

Регулярное выражение сопоставляется дважды (csev@umich.edu и cwen@iupui.edu), но оно не соответствует подстроке "@2PM", поскольку перед символом @ нет непробельных символов. Мы можем использовать это регулярное выражение в программе, читающей все строки в файле и печатающей всё, что выглядит как e-mail адрес:

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('\S+@\S+', line)
if len(x) > 0 :
print x
  

Из каждой прочитанной строки извлекаются все подстроки, соответствующие нашему регулярному выражению. Поскольку метод findall() возвращает список, достаточно проверить, что число его элементов больше нуля, чтобы напечатать строки, содержащие e-mail адреса.

Применив программу к файлу mbox.txt, получим следующий результат:

['wagnermr@iupui.edu']
['cwen@iupui.edu']
['<postmaster@collab.sakaiproject.org>']
['<200801032122.m03LMFo4005148@nakamura.uits.iupui.edu>']
['<source@collab.sakaiproject.org>;']
['<source@collab.sakaiproject.org>;']
['<source@collab.sakaiproject.org>;']
['apache@localhost)']
['source@collab.sakaiproject.org;']
  

Некоторые из этих e-mail адресов содержат некорректные символы "<" или ";" в начале либо конце. Укажем, что нас интересует лишь часть строки, начинающаяся и заканчивающаяся буквой или цифрой. Для этого используем другую возможность, предоставляемую регулярными выражениями. Квадратные скобки в них применяются для указания множества допустимых символов, используемых при сопоставлении. В этом смысле шаблон "\S" соответствует множеству всех непробельных символов. Теперь мы более явно укажем множество сопоставляемых символов.

Вот наше новое регулярное выражение:

[a-zA-Z0-9]\S*@\S*[a-zA-Z]

Оно несколько сложнее, теперь уже видно, почему регулярные выражения можно считать самостоятельным языком. Данное регулярное выражение означает, что мы ищем подстроки, начинающиеся с одиночной строчной буквы, прописной буквы или цифры "[a-zA-Z0-9]", за которой следует ноль или более непробельных символов "\S*", дальше идет символ @, потом ноль или более непробельных символов "\S*" и в конце строчная или прописная буква. Отметим, что мы заменили "+ " на "* " для указания нуля или более непробельных символов, поскольку фрагмент " [a-zA-Z0-9] " уже представляет собой один непробельный символ.

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

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('[a-zA-Z0-9]\S+@\S+[a-zA-Z]', line)
if len(x) > 0 :
print x
['wagnermr@iupui.edu']
['cwen@iupui.edu']
['postmaster@collab.sakaiproject.org']
['200801032122.m03LMFo4005148@nakamura.uits.iupui.edu']
['source@collab.sakaiproject.org']
['source@collab.sakaiproject.org']
['source@collab.sakaiproject.org']
['apache@localhost']
  

Отметим, что из строк, содержащих фрагмент "source@collab.sakaiproject.org", наше регулярное выражение исключило два символа в конце строки (">;"). Это произошло потому, что, добавив фрагмент "[a-zA-Z]" в конец нашего регулярного выражения, мы указали, что подстрока, которая сопоставляется регулярному выражению, обязательно должна заканчиваться буквой. Поэтому сопоставление заканчивается, как только мы доходим до символа ">" в конце "sakaiproject.org>;" – на последней "сопоставимой" букве (в данном случае "g"). Также отметим, что каждая строчка вывода программы представляет собой одноэлементный список Питона, единственным элементом которого является строка.

23.3. Сочетание поиска и извлечения

Пусть мы хотим найти числа во всех строках, которые начинаются с фрагмента "X-", например:

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
  

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

^X-.*: [0-9.]+
  

Здесь указывается, что мы выбираем только строки, начинающиеся с фрагмента "X-", за которым следуют ноль или более произвольных символов ".*", затем двоеточие ":" и пробел. После пробела ищем один или более символов, каждый из которых является либо цифрой 0-9, либо точкой "[0-9.]+". Отметим, что точка внутри квадратных скобок обозначает обычный символ точки, а не произвольный символ (т.е. подстановка произвольного символа не действует внутри квадратных скобок).

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

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^X\S*: [0-9.]+', line) :
print line
  

Выполнив программу, мы успешно отфильтруем только нужные нам строки.

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
X-DSPAM-Confidence: 0.6178
X-DSPAM-Probability: 0.0000
  

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

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

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^X\S*: ([0-9.]+)', line)
if len(x) > 0 :
print x
  

Вместо вызова метода search() мы заключаем в круглые скобки ту часть регулярного выражения, которая представляет число в плавающем формате; тем самым мы указываем, что метод findall() должен возвращать лишь часть сопоставленной подстроки, представляющую число.

На выходе этой программы получаем:

['0.8475']
['0.0000']
['0.6178']
['0.0000']
['0.6961']
['0.0000']
..
  

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

Еще один пример применения этой техники: рассмотрим файл, который содержит некоторое количество строк в форме:

Details: http://source.sakaiproject.org/viewsvn/?view=revrev=39772

Пусть нам нужно извлечь все номера ревизий (целые числа в концах подобных строк). Можно использовать следующую программу:

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^Details:.*rev=([0-9.]+)', line)
if len(x) > 0:
print x
  

Здесь регулярное выражение означает, что мы ищем строки, начинающиеся со слова "Details:", за которым следует произвольное количество любых символов ".*", затем фрагмент "rev=" и далее одна или несколько цифр. Мы ищем строки, соответствующие всему выражению, но хотим извлечь из них только числа в концах строк, поэтому мы заключаем фрагмент " [0-9]+" в круглые скобки.

В результате работы программы получаем:

['39772']
['39771']
['39770']
['39769']
...
  

Помните, что фрагмент "[0-9]+" "жадный", он пытается выделить максимально большую подстроку из цифр перед ее извлечением. Это "жадное" поведение объясняет, почему выделяются все пять цифр каждого числа. Расширение происходит в обоих направлениях, пока не встретится либо не-цифра, либо начало или конец строки.

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

From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008

Мы хотим извлечь час из каждой такой строки. Раньше мы делали это с помощью двух вызовов метода split. Сначала строка разделялась на слова, затем извлекалось пятое слово, которое в свою очередь разделялось с помощью двоеточия для извлечения интересовавших нас двух символов.

Хотя это и работало, мы получили в результате довольно ненадежный, хрупкий (brittle) код, предполагающий, что исходные строки правильно отформатированы. Если добавить проверку ошибок (или большой блок try/except), чтобы программа не отказывала при некорректно отформатированных входных данных, код раздулся бы до 10-15 строк и стал бы трудно читаемым.

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

^From .* [0-9][0-9]:

Оно означает, что мы ищем строки, начинающиеся с фрагмента "From " (обратите внимание на пробел!), за которым следует произвольное количество любых символов ".*", затем пробел и дальше две цифры "[0-9][0-9]", после которых стоит символ двоеточия. Это точное определение строк, которые мы ищем.

Для извлечения часа с помощью метода findall() мы заключаем в круглые скобки часть выражения, соответствующую двум цифрам:

^From .* ([0-9][0-9]):

В результате получаем программу:

import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^From .* ([0-9][0-9]):', line)
if len(x) > 0 : print x

На выходе она выдает

['09']
['18']
['16']
['15']
  

23.4. Символ "Escape"

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

Это можно сделать, поставив перед защищаемым символом обратную косую черту "\" (backslash). Например, можно найти обозначения денежных сумм с помощью следующего регулярного выражения:

import re
x = 'We just received $10.00 for cookies.'
y = re.findall('\$[0-9.]+',x)
  

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

Замечание: символы внутри квадратных скобок не считаются специальными. Когда мы пишем "[0-9.]", это действительно означает цифру или точку. Вне квадратных скобок точка означает подстановку произвольного символа. Внутри них точка означает точку.

23.5. Выводы

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

^

Соответствует началу строки.

$

Соответствует концу строки.

.

Соответствует любому символу (символ подстановки, wildcard).

\s

Соответствует пробельному символу (whitespace).

\S

Соответствует непробельному символу (противоположен \s).

*

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

*?

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

+

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

+?

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

[aeiou]

Соответствует одному символу из указанного набора. В данном примере может сопоставляться символам "a", "e", "i", "o", "u" и никаким другим.

[a-z0-9]

Можно указывать диапазон символов с помощью знака минус. В данном примере задается один символ, являющийся строчной буквой или цифрой.

[^A-Za-z]

Если первым символом в обозначении набора является крышка "^", то смысл обозначения меняется на обратный – любой символ, не входящий в указанный набор. В данном примере задается один символ, не являющийся прописной или строчной буквой.

( )

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

\b

Соответствует пустой подстроке в начале или конце слова.

\B

Соответствует пустой подстроке всюду, за исключением начала или конца слова.

\d

Соответствует десятичной цифре; эквивалентно выражению [0-9].

\D

Соответствует любому символу, отличному от десятичной цифры; эквивалентно выражению [^0-9].

23.6. Бонусный раздел для пользователей UNIX

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

Как правило, в системе UNIX имеется программа с консольным интерфейсом под названием grep (Generalized Regular Expression Parser), которая работает примерно так же, как описанный в этой главе метод search(). Таким образом, если вы работаете в системах Macintosh или Linux, вы можете попробовать следующую команду в консольном окне:

$ grep '^From:' mbox-short.txt
From: stephen.marquard@uct.ac.za
From: louis@media.berkeley.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
  

Команда указывает утилите grep напечатать все строки в файле mbox-short.txt, начинающиеся с фрагмента "From:".

Если вы немного поэкспериментируете с утилитой grep и прочитаете документацию к ней, то найдете небольшие различия между регулярными выражениями Питона и регулярными выражениями, используемыми в grep. Например, grep не поддерживает непробельный символ "\S", так что придется использовать чуть более сложное обозначение "[^ ]", означающее попросту любой символ, отличный от пробела.

23.7. Отладка

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

Воспользоваться интерактивной документацией можно с помощью команды help().

>>> help()
Welcome to Python 2.6! This is the online help utility.
If this is your first time using Python, you should definitely check out
the tutorial on the Internet at http://docs.python.org/tutorial/.
Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To quit this help utility and
return to the interpreter, just type "quit".
To get a list of available modules, keywords, or topics, type "modules",
"keywords", or "topics". Each module also comes with a one-line summary
of what it does; to list the modules whose summaries contain a given word
such as "spam", type "modules spam".
help> modules
  

Если вы знаете, какой модуль хотите использовать, можно воспользоваться командой dir() для перечисления его методов:

>>> import re
>>> dir(re)
[.. 'compile', 'copy_reg', 'error', 'escape', 'findall',
'finditer', 'match', 'purge', 'search', 'split', 'sre_compile',
'sre_parse', 'sub', 'subn', 'sys', 'template']
  

Можно запросить короткую информацию по каждому конкретному методу:

>>> help (re.search)
Help on function search in module re:
search(pattern, string, flags=0)
Scan through string looking for a match to the pattern, returning
a match object, or None if no match was found.
>>>
  

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

23.8. Глоссарий

Хрупкий код (brittle code): код, который работает, когда входные данные строго соответствуют заданному формату, но склонен к отказам, когда данные отклоняются от правильного формата. Мы называем такой код "хрупким", поскольку подобные программы легко ломаются.

Жадное сопоставление (greedy matching): сопоставление, при котором подстрока, задаваемая в регулярном выражении с помощью символов "+" и "*", расширяется до максимально возможного предела.

grep: команда, доступная в большинстве UNIX-систем, которая осуществляет поиск в содержимом текстовых файлов, выдавая строки, соответствующие регулярному выражению. Название команды является сокращением от "Generalized Regular Expression Parser".

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

Символ подстановки (wild card): специальный символ, который при сопоставлении соответствует любому символу. В регулярных выражениях в качестве символа подстановки используется точка.

23.9. Упражнения

Упражнение 23.1.

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

$ python grep.py
Enter a regular expression: ^Author
mbox.txt had 1798 lines that matched ^Author
$ python grep.py
Enter a regular expression: ^X-
mbox.txt had 14368 lines that matched ^X-
$ python grep.py
Enter a regular expression: java$
mbox.txt had 4218 lines that matched java$
  

Упражнение 11.2.

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

Enter file: mbox.txt
38549.7949721
Enter file: mbox-short.txt
39756.9259259
  
Вернуться к учебному плану