#!/usr/bin/env python # coding: utf-8 # # Программирование на языке Python для сбора и анализа данных # # *Текст лекции: Щуров И.В., НИУ ВШЭ* # # Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии [Creative Commons Attribution-Share Alike 4.0](http://creativecommons.org/licenses/by-sa/4.0/). При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на [страницу курса](http://math-info.hse.ru/s15/m). Фрагменты кода, включенные в этот notebook, публикуются как [общественное достояние](http://creativecommons.org/publicdomain/zero/1.0/). # # Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на [странице курса](http://math-info.hse.ru/s15/m). # ## Лекция №9: Поиск данных на HTML-страницах и работа с API с помощью XML # ### Поиск данных на HTML-странице # На предыдущей лекции мы познакомились с библиотекой *Beautiful Soup* и рассмотрели простейший пример обработки HTML с его помощью. Сейчас мы обсудим более сложные сценарии поиска данных на веб-страницах. Для примера возьмём статью из Википедии о романе М. А. Булгакова [Мастер и Маргарита](https://ru.wikipedia.org/wiki/%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). # # Заметим, что в Википедии встречаются ссылки двух типов: внутренние (на другие страницы Википедии) и внешние (на другие сайты), причём они различаются по оформлению — у внешних ссылок есть небольшая стрелочка. Например, мы хотим выбрать все внешние ссылки. Как это сделать? # # Для того, чтобы браузер отображал внешние ссылки не так, как внутренние, разработчики Википедии используют так называемые css-классы (конечно, это касается не только Википедии — это вообще основной инструмент современного веба). Теги ``, соответствующие внешним ссылкам, имеют специальный атрибут `class`, значение которого включает слово `external`. Именно по нему можно понять, что речь идёт о внешней ссылке. Это можно было бы увидеть, изучив исходный код страницы, но мы сделаем проще: воспользуемся встроенным инспектором кода в Firefox (в других браузерах есть аналоги — встроенные или в виде расширений). # ![Тег `` с классом `external`](http://math-info.hse.ru/f/2015-16/all-py/m_and_m_1.png) # На скриншоте видно, что в исходном коде в атрибуте `class` тега `` указана строчка `"external text"`, а не просто `"external"` — дело в том, что теги могут иметь сразу несколько классов одновременно, и в данном случае `external` и `text` — это два класса данной ссылки. Но мы будем ориентироваться только на `external`. # # Итак, мы хотим найти все ссылки с классом `external`. Это очень просто. # In[1]: from bs4 import BeautifulSoup import requests # In[8]: url = "https://ru.wikipedia.org/w/index.php?oldid=75475510" # Используем постоянную ссылку для воспроизводимости результатов # In[3]: g = requests.get(url) # In[4]: g.ok # In[6]: page = BeautifulSoup(g.text, "html.parser") # In[7]: for link in page.findAll("a", class_='external'): print(link['href']) # Как видно из примера выше, достаточно методу `findAll()` передать дополнительный именованный параметр `class_` — обратите внимание на нижнее подчёркивание, без него получится синтаксическая ошибка, потому что слово `class` имеет специальный смысл в Python. # # ### Классы и поиск по дереву # Решим теперь другую задачу: допустим, мы хотим найти все ссылки в разделе «Примечания», где находятся сноски к основному тексту. С помощью инспектора кода мы легко можем заметить, что весь этот раздел находится внутри тега `
` (этот тег описывает прямоугольные блоки, из которых состоят веб-страницы, и является основным тегом для современной веб-вёрстки), имеющем класс `references-small`. # In[13]: divs = page.findAll('div', class_='references-small') # In[14]: len(divs) # Такой `
` оказался единственным на странице. Вот и хорошо. Найдём теперь все теги ``, являющиеся потомками (возможно, отдалёнными) этого `
`'а. # In[15]: div = page.findAll('div', class_='references-small')[0] for link in div("a")[0:10]: print(link['href']) # Для экономии места я вывел только первые 10 ссылок. Это внутренние ссылки на другие фрагменты страницы, поэтому они начинаются с символа `#`. Легко увидеть, что мы получили то, что требовалось. # # ### Некоторые итоги # Подведём некоторые итоги по поводу поиска информации в HTML-файлах: # # - Это всегда творческий процесс: все сайты разные и нет единого рецепта, как извлекать из них нужную информацию. # - В первую очередь нужно посмотреть в исходник интересующей вас странички. Проще всего это делать с помощью инструментария веб-разработчика типа Firebug или встроенного инспектора кода в Firefox или аналогичных инструментов для других браузеров. # - В HTML-дереве можно ориентироваться по названиям тегов, их классам, id'ам и другим свойствам. # - Можно искать нужный элемент итеративно — сначала найти «большой» тег, который включает наш элемент, потом найти в нём элемент поменьше и т.д. # ### API и XML # Анализируя веб-страницы и извлекая из них информацию мы пытаемся написать программу, которая бы действовала как человек. Это бывает непросто. К счастью, всё чаще разнообразные сайты предлагают информацию, которую может легко обрабатывать не только человек, но и другая программа. Это называется API — application program interface. Обычный интерфейс — это способ взаимодействия человека с программой, а API — одной программы с другой. Например, вашего скрипта на Python с удалённым веб-сервером. # # Для хранения веб-страниц, которые читают люди, используется язык HTML. Для хранения произвольных структурированных данных, которыми обмениваются между собой программы, используются другие языки — в частности, язык XML, похожий на HTML. Вернее было бы сказать, что XML это *метаязык*, то есть способ описания языков. В отличие от HTML, набор тегов в XML-документе может быть произвольным (и определяется разработчиком конкретного диалекта XML). Например, если бы мы хотели описать в виде XML некоторую студенческую группу, это могло бы выглядеть так: # ```xml # # 134 # # Виталий # Иванов # # # Мария # Петрова # # # ``` # Для обработки XML-файлов можно использовать тот же пакет *Beautiful Soup*, который мы уже использовали для работы с HTML. Единственное различие — нужно указать дополнительный параметр `feautres="xml"` при вызове функции `BeautifulSoup` — чтобы он не искал в документе HTML-теги. # In[17]: group = """ 134 Виталий Иванов Мария Петрова """ # In[20]: obj = BeautifulSoup(group, features="xml") print(obj.prettify()) # Вот так мы можем найти в нашем XML-документе номер группы: # In[21]: obj.group.number.string # Это значит «в объекте `obj` найти тег `group` в нём найти тег `number` и выдать в виде строки то, что в нём содержится. # # А вот так можно перечислить всех студентов: # In[30]: for student in obj.group.findAll('student'): print(student.lastname.string, student.firstname.string) # ### Получаем список статей из категории в Википедии # Допустим, нам потребовалось получить список всех статей из некоторой категории в Википедии. Мы могли бы открыть эту категорию в браузере и дальше действовать теми методами, которые обсуждались выше. Однако, на наше счастье разработчики Википедии сделали удобное API. Чтобы научиться с ним работать, придётся познакомиться с [документацией](https://www.mediawiki.org/wiki/API:Main_page) (так будет с любым API), но это кажется сложным только в первый раз. Ну хорошо, в первые 10 раз. Или 20. Потом будет проще. # # Итак, приступим. Взаимодействие с сервером при помощи API происходит с помощью отправки специальным образом сформированных запросов и получения ответа в одном из машинночитаемых форматов. Нас будет интересовать формат XML, хотя бывают и другие (позже мы познакомимся с JSONN). А вот такой запрос мы можем отправить: # https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:Physics&cmsort=timestamp&cmdir=desc&format=xmlfm # Строка `https://en.wikipedia.org/w/api.php` (до знака вопроса) — это *точка входа* в API. Всё, что идёт после знака вопроса — это, собственно, запрос. Он представляет собой что-то вроде словаря и состоит из пар «ключ=значение», разделяемых амперсандом `&`. Некоторые символы приходится кодировать специальным образом. # # Например, в адресе выше сказано, что мы хотим сделать запрос (`action=query`), перечислить элементы категории `list=categorymembers`, в качестве категории, которая нас интересует, указана `Category:Physics` (`cmtitle=Category:Physics`) и указаны некоторые другие параметры. Если кликнуть по этой ссылке, откроется примерно такая штука: # ```xml # # # # # # # # # # # # # # # # # # # ``` # Мы видим здесь разные теги, и видим, что нас интересуют теги ``, находящиеся внутри тега ``. # # Давайте сделаем соответствующий запрос с помощью Python. Для этого нам понадобится уже знакомый модуль `requests`. # In[23]: url = "https://en.wikipedia.org/w/api.php" params = { 'action':'query', 'list':'categorymembers', 'cmtitle': 'Category:Physics', 'format': 'xml' } g = requests.get(url, params=params) # Как видно, список параметров мы передаем в виде обычного словаря. Посмотрим, что получилось. # In[24]: g.ok # Всё хорошо. Теперь используем *Beautiful Soup* для обработки этого XML. # In[25]: data = BeautifulSoup(g.text, features='xml') # In[26]: print(data.prettify()) # Найдём все вхождения тега `` и выведем их атрибут `title`: # In[27]: for cm in data.api.query.categorymembers("cm"): print(cm['title']) # Можно было упростить поиск ``, не указывая «полный путь» к ним: # In[28]: for cm in data("cm"): print(cm['title']) # По умолчанию сервер вернул нам список из 10 элементов. Если мы хотим больше, нужно воспользоваться элементом `continue` — это своего рода гиперссылка на следующие 10 элементов. # In[29]: data.find("continue")['cmcontinue'] # Мне пришлось использовать метод `find()` вместо того, чтобы просто написать `data.continue`, потому что `continue` в Python имеет специальный смысл. # # Теперь добавим `cmcontinue` в наш запрос и выполним его ещё раз: # In[30]: params['cmcontinue'] = data.api("continue")[0]['cmcontinue'] # In[31]: g = requests.get(url, params=params) data = BeautifulSoup(g.text, features='xml') for cm in data.api.query.categorymembers("cm"): print(cm['title']) # Мы получили следующие 10 элементов из категории. Продолжая таким образом, можно выкачать её даже целиком (правда, для этого потребуется много времени). # # Аналогичным образом реализована работа с разнообразными другими API, имеющимися на разных сайтах. Где-то API является полностью открытым (как в Википедии), где-то вам потребуется зарегистрироваться и получить application id и какой-нибудь ключ для доступа к API, где-то попросят даже заплатить (например, автоматический поиск в Google стоит что-то вроде 5 долларов за 100 запросов). Есть API, которые позволяют только читать информацию, а бывают и такие, которые позволяют её править. Например, можно написать скрипт, который будет автоматически сохранять какую-то информацию в Google Spreadsheets. Всякий раз при использовании API вам придётся изучить его документацию, но это в любом случае проще, чем обрабатывать HTML-код. Иногда удаётся упростить доступ к API, используя специальные библиотеки.