На этом занятии
поговорим о группировках и сохранении результатов поиска, используя регулярные
выражения. О чем здесь речь? Давайте представим, что нам нужно из текста
lat =
5, lon=7
выделить данные
в формате: ключ=значение. Выражение можно записать так:
import re
text = "lat = 5, lon=7"
match = re.findall(r"\w+\s*=\s*\d+", text)
print(match)
Однако, это же
шаблон будет работать, например, и с такими данными:
а мы бы хотели,
чтобы учитывались только ключи lat и lon. Для этого их
нужно явно указать в нашем правиле. Сделаем это следующим образом:
match = re.findall(r"lat\s*=\s*\d+|lon\s*=\s*\d+", text)
Смотрите, мы
записали два шаблона через символ |, который в регулярных выражениях означает
ИЛИ. То есть, в качестве ключа можно брать lat или lon (одно из двух)
и далее, должно идти число. Запустим программу вот с таким текстом:
text = "lat = 5, lon=7, a=5"
и на выходе
получаем только данные для lat и lon:
['lat = 5',
'lon=7']
Но у этого
выражения есть недостаток: мы дважды записываем одно и то же после имен ключей.
Давайте это оптимизируем. Здесь нам на помощь приходят группирующие скобки:
match = re.findall(r"(?:lat|lon)\s*=\s*\d+", text)
Мы здесь
использовали несохраняющую группировку двух литералов lat или lon. Вот эти два
символа ?: сразу после открывающей круглой скобки как раз и указывает на
несохраняющую группировку. Что это такое я поясню чуть позже. А сейчас, если
запустить программу, то увидим прежний результат:
['lat = 5',
'lon=7']
и, при этом,
наше правило записано без дублирующих элементов. Интерпретировать все это можно
так: сначала проверяется наличие литералов lat или lon, а затем, после
них должен стоять знак равенства и числа.
Теперь, давайте
уберем символы ?: из наших скобок:
match = re.findall(r"(lat|lon)\s*=\s*\d+", text)
В этом случае мы
получаем сохраняющую группировку. То есть, для каждого найденного вхождения
отдельно будет сохраняться соответствующий ключ. Визуально это можно
представить так:
И, если
запустить программу, то в консоли увидим как раз этот второй уровень:
['lat', 'lon']
Чтобы увидеть
оба уровня, поставим сохраняющие скобки вокруг всего выражения:
match
= re.findall(r"((lat|lon)\s*=\s*\d+)", text)
У нас получится
следующий результат:
[('lat
= 5', 'lat'), ('lon=7', 'lon')]
Фактически,
здесь во второй уровень записывается сначала вся найденная фраза, а затем,
включаются внутренние скобки и добавляются ключи. И, смотрите, используя этот
функционал, мы можем отдельно выделить ключ и значения, записав шаблон в таком
виде:
match = re.findall(r"(lat|lon)\s*=\s*(\d+)", text)
На выходе сразу
получим список из кортежей с парами: ключ, значение:
[('lat', '5'),
('lon', '7')]
и это очень
удобно для дальнейшей обработки этих данных. Вот так работают круглые скобки:
они выполняют сразу две функции – группировку и сохранение результатов поиска.
Или, же можем использовать группировку без сохранения, добавляя внутрь скобок
символы
(?:<выражение>)
Давайте теперь
рассмотрим пример использования сохраняющих скобок для выделения значения
атрибута src у тега img:
text = "Картинка <img src='bg.jpg'> в тексте</p>"
Для этого
запишем регулярное выражение в виде:
match = re.findall(r"<img\s+[^>]*src=[\"'](.+?)[\"']", text)
В принципе, вы
тут уже все знаете, я только добавил круглые скобки для тех символов, что
составляют путь к файлу. При запуске увидим следующий результат:
['bg.jpg']
Но как можно
использовать эти сохранения непосредственно внутри регулярного выражения? К ним
можно обратиться с помощью такого синтаксиса:
\i (i – натуральное
число: 1, 2, 3, …)
Например, наш
шаблон сработает и в таких случаях:
<img src="bg. jpg'>
<img src='bg. jpg">
то есть, если
прописать разные типы кавычек. Этот недостаток легко поправить с помощью
обращения к соответствующему сохранению. Перепишем выражение так:
match = re.findall(r"<img\s+[^>]*src=([\"'])(.+?)\1", text)
При запуске
увидим:
[("'",
'bg.jpg')]
Первая кавычка –
это первая сохраняющая скобка, а вторая строка – это вторая скобка. Все
сработало, как и должно быть. Причем, изменив нашу строку с разными кавычками:
text = "<p>Картинка
<img src='bg.jpg\"> в
тексте</p>"
на выходе
получим пустую коллекцию. И, обратите внимание, использовать в символьных
классах [] ссылки на сохранения нельзя, мы их можем прописывать только внутри
самого регулярного выражения.
В ряде случаев
использовать цифры при обращении к сохраненному блоку не очень удобно, поэтому
синтаксис позволяет назначать им имена:
(?P<name>…)
и, затем,
обращаться к ним:
(?P=name)
Перепишем последнее
регулярное выражение с использованием имен, получим:
match = re.findall(r"<img\s+[^>]*src=(?P<quote>[\"'])(.+?)(?P=quote)", text)
Результат будет
прежним. Однако, имена лучше назначать для сложных выражений, в простых они
добавляют только громоздкости и здесь понятнее смотрятся цифры.
Давайте в
качестве практического примера рассмотрим парсинг вот такого xml-файла:
<!DOCTYPE xmlmap>
<xmlmap>
<parametrs>
<name>map</name>
<scale>750000</scale>
<issuedate>20060828</issuedate>
<correctiondate>20060828</correctiondate>
</parametrs>
<object code="43" >
<attribute number="133" value="3000000" />
<attribute number="174" value="20" />
<primitive pointType="2" name="2" >
<point lon="40.8482" lat="52.6274" />
<point lon="40.8559" lat="52.6361" />
…
Нам здесь нужно
у записи point выделить
долготу (lon) и широту (lat). В самом
простом варианте это можно сделать так:
with open("map.xml", "r") as f:
lat = []
lon = []
for text in f:
match = re.findall(r"<point\s+[^>]*?lon=([\"\'])([0-9.,]+)\1\s+[^>]*lat=([\"\'])([0-9.,]+)\1", text)
print(match)
print(lon, lat, sep="\n")
Мы здесь
открываем файл на чтение с использованием менеджера контекста а, затем,
построчно считываем из него информацию. Для каждой строки применяем регулярное
выражение, выделяя атрибуты lon и lat. На выходе
получаем следующий результат:
…
[]
[]
[]
[('"',
'40.8482', '"', '52.6274')]
[('"',
'40.8559', '"', '52.6361')]
[('"',
'40.8614', '"', '52.651')]
…
В тех строчках,
где нет данных атрибутов, имеем пустую коллекцию, а где формат совпадает,
получаем четыре значения из сохраняющих скобок. Здесь нам нужны значения с
индексом 1 – для lon и 3 – для lat. И, записывая
все «в лоб», получаем такую программу:
with open("map.xml", "r") as f:
lat = []
lon = []
for text in f:
match = re.findall(r"<point\s+[^>]*?lon=([\"\'])([0-9.,]+)\1\s+[^>]*lat=([\"\'])([0-9.,]+)\1", text)
if len(match) > 0:
lon.append(match[0][1])
lat.append(match[0][3])
print(lon, lat, sep="\n")
Но, как вы
понимаете, это не лучшая реализация поставленной задачи. Что если регулярное выражение
изменится и индексы станут другими? Придется поправлять весь программный код,
связанный с этим шаблоном! Это не очень удобно. Здесь лучше использовать имена
сохраняющих групп, а затем, обращаться к данным по этим именам. Поэтому
перепишем программу так:
with open("map.xml", "r") as f:
lat = []
lon = []
for text in f:
match = re.search(r"<point\s+[^>]*?lon=([\"\'])(?P<lon>[0-9.,]+)\1\s+[^>]*lat=([\"\'])(?P<lat>[0-9.,]+)\1", text)
if match:
v = match.groupdict()
if "lon" in v and "lat" in v:
lon.append(v["lon"])
lat.append(v["lat"])
print(lon, lat, sep="\n")
Мы здесь
воспользовались другим методом модуля re – search, который
возвращает не просто коллекцию, а объект, из которого можно получить словарь,
содержащий коллекцию сохраненных именованных групп (об этом методе подробнее
речь пойдет позже). Далее, мы проверяем: было ли найдено совпадение и если да,
то дополнительная проверка на наличие ключей lon и lat в полученном
словаре. При истинности обеих проверок, добавляем в наши коллекции
соответствующие данные.
Здесь
использование именованных групп оправдано, т.к. создает универсальность текста
программы, независимо от вида регулярного выражения.