Текст лекции: Щуров И.В., НИУ ВШЭ
Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.
Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.
На предыдущей лекции мы познакомились с библиотекой Beautiful Soup и рассмотрели простейший пример обработки HTML с его помощью. Сейчас мы обсудим более сложные сценарии поиска данных на веб-страницах. Для примера возьмём статью из Википедии о романе М. А. Булгакова Мастер и Маргарита.
Заметим, что в Википедии встречаются ссылки двух типов: внутренние (на другие страницы Википедии) и внешние (на другие сайты), причём они различаются по оформлению — у внешних ссылок есть небольшая стрелочка. Например, мы хотим выбрать все внешние ссылки. Как это сделать?
Для того, чтобы браузер отображал внешние ссылки не так, как внутренние, разработчики Википедии используют так называемые css-классы (конечно, это касается не только Википедии — это вообще основной инструмент современного веба). Теги <a>
, соответствующие внешним ссылкам, имеют специальный атрибут class
, значение которого включает слово external
. Именно по нему можно понять, что речь идёт о внешней ссылке. Это можно было бы увидеть, изучив исходный код страницы, но мы сделаем проще: воспользуемся встроенным инспектором кода в Firefox (в других браузерах есть аналоги — встроенные или в виде расширений).
На скриншоте видно, что в исходном коде в атрибуте class
тега <a>
указана строчка "external text"
, а не просто "external"
— дело в том, что теги могут иметь сразу несколько классов одновременно, и в данном случае external
и text
— это два класса данной ссылки. Но мы будем ориентироваться только на external
.
Итак, мы хотим найти все ссылки с классом external
. Это очень просто.
from bs4 import BeautifulSoup
import requests
url = "https://ru.wikipedia.org/w/index.php?oldid=75475510"
# Используем постоянную ссылку для воспроизводимости результатов
g = requests.get(url)
g.ok
True
page = BeautifulSoup(g.text, "html.parser")
for link in page.findAll("a", class_='external'):
print(link['href'])
//ru.wikipedia.org/w/index.php?title=%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:%D0%96%D1%83%D1%80%D0%BD%D0%B0%D0%BB%D1%8B&type=review&page=%D0%9C%D0%B0%D1%81%D1%82%D0%B5%D1%80_%D0%B8_%D0%9C%D0%B0%D1%80%D0%B3%D0%B0%D1%80%D0%B8%D1%82%D0%B0 http://dombulgakova.ru/bulgakovskaya-biblioteka-2/lidiya-yanovskaya-o-romane-bulgakova-mas/ http://magazines.russ.ru/zvezda/2000/6/suhih.html http://magazines.russ.ru/sib/2013/6/10d.html http://mirknig.mobi/data/2012-12-06/1291088/Chudakova_Vremya_chitat_3_Ne_dlya_vzroslyih._Vremya_chitat_Polka_tretya.1291088.pdf http://www.e-reading.club/chapter.php/39079/24/Zerkalov_-_Etika_Mihaila_Bulgakova.html http://www.litmir.co/br/?b=65955&p=163 http://www.bulgakov.ru/m/morphiy/ http://www.russofile.ru/articles/article_67.php http://feb-web.ru/feb/mayakovsky/texts/ms0/ms9/ms9-183-.htm http://magazines.russ.ru/slovo/2008/58/ko11.html http://magazines.russ.ru/nlo/2005/76/bra.html http://www.rg.ru/2005/12/16/master.html http://magazines.russ.ru/voplit/2009/4/ga8.html http://magazines.russ.ru/znamia/2011/10/bo30.html http://kinoart.ru/archive/2006/01/n1-article4 http://magazines.russ.ru/znamia/2007/1/bu17.html http://izvestia.ru/news/499828 http://www.rg.ru/2011/09/08/mht.html https://musicbrainz.org/work/2fa8ecec-2103-4aa7-97f1-2fbfd1c0607d http://catalogue.bnf.fr/ark:/12148/cb11941979p http://www.idref.fr/027361241 http://viaf.org/viaf/175580487
Как видно из примера выше, достаточно методу findAll()
передать дополнительный именованный параметр class_
— обратите внимание на нижнее подчёркивание, без него получится синтаксическая ошибка, потому что слово class
имеет специальный смысл в Python.
Решим теперь другую задачу: допустим, мы хотим найти все ссылки в разделе «Примечания», где находятся сноски к основному тексту. С помощью инспектора кода мы легко можем заметить, что весь этот раздел находится внутри тега <div>
(этот тег описывает прямоугольные блоки, из которых состоят веб-страницы, и является основным тегом для современной веб-вёрстки), имеющем класс references-small
.
divs = page.findAll('div', class_='references-small')
len(divs)
1
Такой <div>
оказался единственным на странице. Вот и хорошо. Найдём теперь все теги <a>
, являющиеся потомками (возможно, отдалёнными) этого <div>
'а.
div = page.findAll('div', class_='references-small')[0]
for link in div("a")[0:10]:
print(link['href'])
#cite_ref-.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.81.E2.80.941999.E2.80.94.E2.80.94213_1-0 #CITEREF.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.811999 #cite_ref-.D0.9B.D0.BE.D1.81.D0.B5.D0.B2.E2.80.941993.E2.80.94.E2.80.94407_2-0 #cite_ref-.D0.9B.D0.BE.D1.81.D0.B5.D0.B2.E2.80.941993.E2.80.94.E2.80.94407_2-1 #CITEREF.D0.9B.D0.BE.D1.81.D0.B5.D0.B21993 #cite_ref-.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.81.E2.80.941999.E2.80.94.E2.80.94214_3-0 #CITEREF.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.811999 #cite_ref-.D0.A7.D1.83.D0.B4.D0.B0.D0.BA.D0.BE.D0.B2.D0.B0.E2.80.941988.E2.80.94.E2.80.94300_4-0 #CITEREF.D0.A7.D1.83.D0.B4.D0.B0.D0.BA.D0.BE.D0.B2.D0.B01988 #cite_ref-.D0.A7.D1.83.D0.B4.D0.B0.D0.BA.D0.BE.D0.B2.D0.B0.E2.80.941988.E2.80.94.E2.80.94301_5-0
Для экономии места я вывел только первые 10 ссылок. Это внутренние ссылки на другие фрагменты страницы, поэтому они начинаются с символа #
. Легко увидеть, что мы получили то, что требовалось.
Подведём некоторые итоги по поводу поиска информации в HTML-файлах:
Анализируя веб-страницы и извлекая из них информацию мы пытаемся написать программу, которая бы действовала как человек. Это бывает непросто. К счастью, всё чаще разнообразные сайты предлагают информацию, которую может легко обрабатывать не только человек, но и другая программа. Это называется API — application program interface. Обычный интерфейс — это способ взаимодействия человека с программой, а API — одной программы с другой. Например, вашего скрипта на Python с удалённым веб-сервером.
Для хранения веб-страниц, которые читают люди, используется язык HTML. Для хранения произвольных структурированных данных, которыми обмениваются между собой программы, используются другие языки — в частности, язык XML, похожий на HTML. Вернее было бы сказать, что XML это метаязык, то есть способ описания языков. В отличие от HTML, набор тегов в XML-документе может быть произвольным (и определяется разработчиком конкретного диалекта XML). Например, если бы мы хотели описать в виде XML некоторую студенческую группу, это могло бы выглядеть так:
<group>
<number>134</number>
<student>
<firstname>Виталий</firstname>
<lastname>Иванов</lastname>
</student>
<student>
<firstname>Мария</firstname>
<lastname>Петрова</lastname>
</student>
</group>
Для обработки XML-файлов можно использовать тот же пакет Beautiful Soup, который мы уже использовали для работы с HTML. Единственное различие — нужно указать дополнительный параметр feautres="xml"
при вызове функции BeautifulSoup
— чтобы он не искал в документе HTML-теги.
group = """<group>
<number>134</number>
<student>
<firstname>Виталий</firstname>
<lastname>Иванов</lastname>
</student>
<student>
<firstname>Мария</firstname>
<lastname>Петрова</lastname>
</student>
</group>"""
obj = BeautifulSoup(group, features="xml")
print(obj.prettify())
<?xml version="1.0" encoding="utf-8"?> <group> <number> 134 </number> <student> <firstname> Виталий </firstname> <lastname> Иванов </lastname> </student> <student> <firstname> Мария </firstname> <lastname> Петрова </lastname> </student> </group>
Вот так мы можем найти в нашем XML-документе номер группы:
obj.group.number.string
'134'
Это значит «в объекте obj
найти тег group
в нём найти тег number
и выдать в виде строки то, что в нём содержится.
А вот так можно перечислить всех студентов:
for student in obj.group.findAll('student'):
print(student.lastname.string, student.firstname.string)
Иванов Виталий Петрова Мария
Допустим, нам потребовалось получить список всех статей из некоторой категории в Википедии. Мы могли бы открыть эту категорию в браузере и дальше действовать теми методами, которые обсуждались выше. Однако, на наше счастье разработчики Википедии сделали удобное API. Чтобы научиться с ним работать, придётся познакомиться с документацией (так будет с любым API), но это кажется сложным только в первый раз. Ну хорошо, в первые 10 раз. Или 20. Потом будет проще.
Итак, приступим. Взаимодействие с сервером при помощи API происходит с помощью отправки специальным образом сформированных запросов и получения ответа в одном из машинночитаемых форматов. Нас будет интересовать формат XML, хотя бывают и другие (позже мы познакомимся с JSONN). А вот такой запрос мы можем отправить:
Строка https://en.wikipedia.org/w/api.php
(до знака вопроса) — это точка входа в API. Всё, что идёт после знака вопроса — это, собственно, запрос. Он представляет собой что-то вроде словаря и состоит из пар «ключ=значение», разделяемых амперсандом &
. Некоторые символы приходится кодировать специальным образом.
Например, в адресе выше сказано, что мы хотим сделать запрос (action=query
), перечислить элементы категории list=categorymembers
, в качестве категории, которая нас интересует, указана Category:Physics
(cmtitle=Category:Physics
) и указаны некоторые другие параметры. Если кликнуть по этой ссылке, откроется примерно такая штука:
<?xml version="1.0"?>
<api batchcomplete="">
<continue cmcontinue="2015-05-30 19:37:50|1653925" continue="-||" />
<query>
<categorymembers>
<cm pageid="24293838" ns="0" title="Wigner rotation" />
<cm pageid="48583145" ns="0" title="Northwest Nuclear Consortium" />
<cm pageid="48407923" ns="0" title="Hume Feldman" />
<cm pageid="48249441" ns="0" title="Phase Stretch Transform" />
<cm pageid="47723069" ns="0" title="Epicatalysis" />
<cm pageid="2237966" ns="14" title="Category:Surface science" />
<cm pageid="2143601" ns="14" title="Category:Interaction" />
<cm pageid="10844347" ns="14" title="Category:Physical systems" />
<cm pageid="18726608" ns="14" title="Category:Physical quantities" />
<cm pageid="22688097" ns="0" title="Branches of physics" />
</categorymembers>
</query>
</api>
Мы видим здесь разные теги, и видим, что нас интересуют теги <cm>
, находящиеся внутри тега <categorymembers>
.
Давайте сделаем соответствующий запрос с помощью Python. Для этого нам понадобится уже знакомый модуль requests
.
url = "https://en.wikipedia.org/w/api.php"
params = {
'action':'query',
'list':'categorymembers',
'cmtitle': 'Category:Physics',
'format': 'xml'
}
g = requests.get(url, params=params)
Как видно, список параметров мы передаем в виде обычного словаря. Посмотрим, что получилось.
g.ok
True
Всё хорошо. Теперь используем Beautiful Soup для обработки этого XML.
data = BeautifulSoup(g.text, features='xml')
print(data.prettify())
<?xml version="1.0" encoding="utf-8"?> <api batchcomplete=""> <continue cmcontinue="page|4d4f4445524e20504859534943530a4d4f4445524e2050485953494353|844186" continue="-||"/> <query> <categorymembers> <cm ns="0" pageid="22939" title="Physics"/> <cm ns="0" pageid="22688097" title="Branches of physics"/> <cm ns="0" pageid="3445246" title="Glossary of classical physics"/> <cm ns="0" pageid="24489" title="Outline of physics"/> <cm ns="100" pageid="1653925" title="Portal:Physics"/> <cm ns="0" pageid="151066" title="Classical physics"/> <cm ns="0" pageid="47723069" title="Epicatalysis"/> <cm ns="0" pageid="685311" title="Experimental physics"/> <cm ns="0" pageid="48407923" title="Hume Feldman"/> <cm ns="0" pageid="23581364" title="Microphysics"/> </categorymembers> </query> </api>
Найдём все вхождения тега <cm>
и выведем их атрибут title
:
for cm in data.api.query.categorymembers("cm"):
print(cm['title'])
Physics Branches of physics Glossary of classical physics Outline of physics Portal:Physics Classical physics Epicatalysis Experimental physics Hume Feldman Microphysics
Можно было упростить поиск <cm>
, не указывая «полный путь» к ним:
for cm in data("cm"):
print(cm['title'])
Physics Branches of physics Glossary of classical physics Outline of physics Portal:Physics Classical physics Epicatalysis Experimental physics Hume Feldman Microphysics
По умолчанию сервер вернул нам список из 10 элементов. Если мы хотим больше, нужно воспользоваться элементом continue
— это своего рода гиперссылка на следующие 10 элементов.
data.find("continue")['cmcontinue']
'page|4d4f4445524e20504859534943530a4d4f4445524e2050485953494353|844186'
Мне пришлось использовать метод find()
вместо того, чтобы просто написать data.continue
, потому что continue
в Python имеет специальный смысл.
Теперь добавим cmcontinue
в наш запрос и выполним его ещё раз:
params['cmcontinue'] = data.api("continue")[0]['cmcontinue']
g = requests.get(url, params=params)
data = BeautifulSoup(g.text, features='xml')
for cm in data.api.query.categorymembers("cm"):
print(cm['title'])
Modern physics Northwest Nuclear Consortium Phase Stretch Transform Statistical mechanics Surface science Wigner rotation Category:Concepts in physics Category:Physicists Category:Applied and interdisciplinary physics Category:Atomic, molecular, and optical physics
Мы получили следующие 10 элементов из категории. Продолжая таким образом, можно выкачать её даже целиком (правда, для этого потребуется много времени).
Аналогичным образом реализована работа с разнообразными другими API, имеющимися на разных сайтах. Где-то API является полностью открытым (как в Википедии), где-то вам потребуется зарегистрироваться и получить application id и какой-нибудь ключ для доступа к API, где-то попросят даже заплатить (например, автоматический поиск в Google стоит что-то вроде 5 долларов за 100 запросов). Есть API, которые позволяют только читать информацию, а бывают и такие, которые позволяют её править. Например, можно написать скрипт, который будет автоматически сохранять какую-то информацию в Google Spreadsheets. Всякий раз при использовании API вам придётся изучить его документацию, но это в любом случае проще, чем обрабатывать HTML-код. Иногда удаётся упростить доступ к API, используя специальные библиотеки.