#!/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). # ## JSON и API. Управление браузером в RoboBrowser и Selenium # ### Работа с API с помощью JSON # В [прошлый раз](http://nbviewer.ipython.org/github/ischurov/pythonhse/blob/master/Lecture%209.ipynb#%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D0%B5%D0%BC-%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA-%D1%81%D1%82%D0%B0%D1%82%D0%B5%D0%B9-%D0%B8%D0%B7-%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D0%B8-%D0%B2-%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D0%B8) мы обсуждали работу с API. При этом для получения информации от API использовался формат XML. Помимо XML существует другой распространённый формат хранения и передачи структурированной информации, называющийся JSON. JSON расшифровывается как JavaScript Object Notation и изначально возник как подмножество языка JavaScript (пусть вас не вводит в заблуждение название, этот язык ничего не имеет общего с Java), используемое для описания объектов, но впоследствии стал использоваться и в других языках программирования, включая Python. Различные API могут поддерживать либо XML, либо JSON, либо и то, и другое, так что нам полезно научиться работать с обоими типами данных. Поэтому мы рассмотрим пример чтения данных из Википедии как в прошлый раз, но будем использовать формат JSON — на наше счастье, API MediaWiki это позволяет. # # Напомним, что нашей задачей является получение списка всех статей из некоторой категории в Википедии. Вот так мы это делали в прошлый раз: # In[20]: import requests from bs4 import BeautifulSoup 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 # Как и в прошлый раз, мы взяли эти параметры из [документации](https://www.mediawiki.org/wiki/API:Categorymembers): `'action': 'query'` значит, что мы отправляем запрос, чтобы получить содержимое Википедии. Параметр `list` отвечает на вопрос список чего мы бы хотели получить. В данном случае это `categorymembers` — список элементов какой-то категории, `cmtitle` — это название категории, список элементов которой мы хотим получить. `'format'` — это формат ответа, который в прошлый раз был `xml`. # In[21]: data = BeautifulSoup(g.text, features='xml') # In[22]: for cm in data.api.query.categorymembers("cm"): print(cm['title']) # Попробуем теперь использовать JSON. Отличия в способе вызова минимальны: в качестве `format` указываем `json`: # In[24]: url = "https://en.wikipedia.org/w/api.php" params = { 'action':'query', 'list':'categorymembers', 'cmtitle': 'Category:Physics', 'format': 'json' } g = requests.get(url, params=params) g.ok # Смотрим, что нам выдали по запросу. Это и есть JSON # In[25]: r.text # Он очень похож на описание объекта в Python и смысл квадратных и фигурных скобок такой же. Правда, есть и отличия: например, в Python одинарные и двойные кавычки ничем не отличаются, а в JSON можно использовать только двойные. Мы видим, что полученный нами JSON представляет собой словарь, значения которого — строки или числа, а также списки или словари, значения которых в свою очередь также могут быть строками, числами, списками, словарями и т.д. То есть получается такая довольно сложная структура данных. # # В данный момент тот факт, что перед нами сложная структура данных, видим только мы — с точки зрения Python, `r.text` это просто такая строка. Однако в модуле `requests` есть метод, позволяющий сразу выдать питоновский объект (словарь или список), если результат запроса возвращён в формате JSON. Так что нам не придётся использовать никакие дополнительные библиотеки. # In[26]: q = r.json() # Видим, что q это словарь # In[27]: q # In[28]: type(q) # Содержательная информация хранится по ключу `'query'`. А уже внутри есть ключ `'categorymembers'`, значением которого является список всех категорий. Каждая категория отображается в виде словаря, записями которого являются разные параметры категории (например, `'title'` соответствует названию, а `pageid` — внутреннему идентификатору в системе). # In[29]: type(q['query']['categorymembers']) # Это список всех членов категории. Мы можем посмотреть на них с помощью цикла # In[30]: for cm in q['query']['categorymembers']: print(cm['title']) # Преимущества JSON в том, что мы получаем готовый объект Python и нет необходимости использовать какие-то дополнительные библиотеки для того, чтобы с ним работать. Недостатком является то же самое: зачастую поиск информации в XML-файле может проводиться более эффективно, чем в JSON. Продемонстрируем это на уже рассмотренном примере. Чтобы получить список всех тегов ``, в которых хранилась информация об элементах категории в XML, мы использовали полный «путь»: # ```python # for cm in data.api.query.categorymembers("cm"): # print(cm['title']) # ``` # Однако, это можно бы сделать (в данном случае) гораздо короче. Если посмотреть на XML, то можно заметить, что в нём нет других тегов ``, кроме тех, которые нам нужны. С другой стороны, *Beautiful Soup* ищет все теги с данным именем, а не только те, которые являются потомками первого уровня для данного тега. Таким образом, код выше можно было бы переписать более коротко: # In[33]: for cm in data("cm"): print(cm['title']) # Конечно `data("cm")` выглядит короче, чем `q['query']['categorymembers']`. В JSON мы не можем использовать подобные методы. Так что у обоих форматов есть свои плюсы и минусы. # ### Эмуляция действий с браузером # Иногда нам нужно не просто скачать какую-нибудь информацию с сайта, а сделать что-то более сложное: например, залогиниться по своим аккаунтом, перейти на какую-то страницу, найти на ней ссылку, перейти по этой ссылке и скачать какую-то информацию. Продемонстрируем два инструмента для решения этой задачи: `robobrowser` и `selenium`. # # Рассмотрим эту задачу на примере работы с сервисом `informatics.mccme.ru`, который мы использовали для сдачи задач в начале нашего курса. # #### RoboBrowser # Пакет `robobrowser` позволяет работать с неким виртуальным браузером, который позволяет ходить по страничкам и получать их содержимое. На самом деле, этот браузер полностью эмулируется Python: фактически `robobrowser` представляет собой надстройку над `requests` и `BeautifulSoup`, позволяющую несколько упростить типичные операции типа «найти ссылку и пройти по ней». # In[69]: from robobrowser import RoboBrowser # Если вдруг Python ругается, что нет каких-то модулей, то сделайте `pip install имя_модуля` в консоли. # In[70]: q = RoboBrowser() # Мы создали виртуальный браузер. # In[71]: ref = 'http://informatics.mccme.ru' q.open(ref) # И сказали ему открыть ссылку. Мы можем посмотреть на html содержимое страницы командой ниже # In[72]: # мне пришлось немного поколодовать, чтобы вывод получился не слишком длинным, # но можно было написать просто # print(q.parsed.text) for l in q.parsed.text.splitlines()[0:50]: # выведем первые несколько строк if l.strip(): # пропустим пустые строки print(l) # Не пугайтесь красного *warning* выше — просто *Beautiful Soup* предупреждает, что мы (а точнее разработчики RoboBrowser) не указали ему, какой парсер использовать, и он использовал самый лучший из доступных (с его точки зрения). # # Найдём ту форму, которая соответствует вводу пароля. В браузере с помощью просмотра кода элемента, мы можем посмотреть кусок HTML, соответствующий форме ввода логина и пароля и узнать у неё есть атрибут `id = 'login'` (атрибут `id` похож на атрибут `class`, но отличается уникальностью: существует ровно один элемент на странице с данным `id`). # ![Форма ввода логина и пароля](http://math-info.hse.ru/f/2015-16/all-py/informatics-login.png) # Извлечем эту форму в *RoboBrowser*. # In[75]: form = q.get_form(id='login') # Нам естественно понадобятся логин и пароль от informatics. Чтобы не сохранять их в исходнике программы, я введу их с клавиатуры. # In[ ]: login = input() password = input() # Элемент `form` ведёт себя как словарь и вы можете передать ему ваши логин и пароль вот так: # In[76]: form['username'] = login form['password'] = password # Теперь посылаем заполненную форму браузеру. # In[77]: q.submit_form(form) # Проверяем, что мы залогинились и наша фамилия или имя есть на странице # In[78]: name = "Щуров" if name in q.response.text: print("Okay, you are logged in") # Итак, мы залогинились и продемонстрировали, как совершать простейшие действия с помощью *RoboBrowser*. Дальше можно искать ссылки и переходить по ним, заполнять формы и т.д. В общем, RoboBrowser довольно удобен для простых задач, связанных с обращением к сайтам. Однако для дальнейшего нам потребуется инструмент помощнее… # # ### Selenium # #### Давным-давно, в одной далёкой-далёкой галактике… # # Когда-то давно трава была зеленой, деревья высокими, а Веб состоял из статических HTML-страниц. Его можно было только читать — ну и выкладывать новые HTML-страницы на сервер, если вы знали, как это делается. Потом появились разные интерактивные страницы типа форумов и первых блогов. Работали они примерно так: вы заходили на сайт, ваш браузер скачивал соответствующую страницу. Там можно было кликнуть по какой-то ссылке или заполнить какую-то форму (например, написать комментарий к посту). В ответ сервер генерировал новую HTML-страницу, браузер её снова загружал и т.д. При этом страница перезагружалась целиком, даже если там изменился всего один символ. Это было дико долго и неэффективно. # # Потом появились новые технологии, которые позволили веб-странице обновляться «кусочками». Для этого в них стали встраивать помимо HTML-кода также код на языке JavaScript. В отличие от HTML, являющегося лишь языком разметки текста, язык JavaScript является полноценным императивным языком программирования (по своим возможностям он похож на Python) и с его помощью можно делать много разных вещей. В частности, в ответ на действие пользователя (например, клик по ссылке или кнопке) отправить какую-то информацию серверу, получить ответ и поменять в соответствии с этим ответом страничку, которая отображается в данный момент, не перезагружая её целиком. Благодаря этому, например, отправив комментарий в социальной сети мы тут же видим, как оно появилось, не перезагружая всю ленту целиком. # # Но есть и тёмная сторона Силы. Современные веб-страницы бывает очень сложно обрабатывать как раз из-за того, что они генерируются динамически на стороне клиента (то есть пользователя). В частности, используемый нами RoboBrowser не умеет запускать JavaScript. А информация о посылках на informatics как раз именно им и генерируется — об этом свидетельствует тот факт, что после открытия соответствующей страницы её центральная часть отображается не сразу — сначала там крутится индикатор (в этот момент как раз JavaScript запрашивает информацию у сервера). # # #### Selenium: дистанционное управление для браузера # # Однако, не следует отчаиваться: нам поможет другой пакет, называемый *Selenium*. Он не запускает JavaScript сам, зато он умеет управлять браузерами, в том числе тем который уже установлен у вас. # # Допустим, что нам надо скачать результаты наших посылок (на лекции скачивали результаты участников факультатива, но я не уверен, что они доступны для студентов). Здесь я буду скачивать результаты своих посылок, эти задачи аналогичны. # In[88]: from selenium import webdriver # Откроем браузер с помощью *Selenium*. Для этого нужно чтобы у вас был установлен данный браузер. В моем случае это Firefox # In[89]: browser = webdriver.Firefox() # Видим, что открылось окошко браузера. Перейдем на informatics # In[95]: ref = 'http://informatics.mccme.ru' browser.get(ref) # Найдем форму входа на сайт # In[96]: form = browser.find_element_by_id('login') # Логика здесь примерно такая же, как в `RoboBrowser` (а у него она заимствована из *Beautiful Soup*), хотя названия методов различаются. # # Найдем у этой формы элементы, отвечающие логину и паролю, и введём в них наши данные. Имейте в виду, что informatics может узнать вас и поле username может быть уже заполненным. Тогда нужно вводить только пароль. Следует отметить, что *Selenium* вводит данные в форму, эмулируя нажатия на кнопки, поэтому если в форме что-то уже записано, то дополнительные символы припишутся к уже существующим. Для безопасности мы на всякий случай очистим поле, прежде, чем что-то туда писать. # In[97]: un = form.find_element_by_name('username') un.clear() # на случай, если это поле уже заполнено, очистим его un.send_keys(login) pw = form.find_element_by_name('password') pw.send_keys(password) # А теперь пошлем данные браузеру командой ниже. # In[98]: form.submit() # Опять проверим, что теперь страница персонифицирована и в ней есть наше имя. Здесь `browser.page_source` — это HTML-код текущей страницы. # In[99]: if name in browser.page_source: print("Okay, you are logged in!") # Заметим, что мы можем управлять браузером, не только с помощью Python, но и вручную. Зайдите например, в «Мои посылки» вручную. Теперь из текущей страницы нужно извлечь информацию о посылках. Можно было бы использовать встроенные возможности Selenium по поиску HTML-элементов, но мы для простоты воспользуемся *Beautiful Soup*, передав ему `browser.page_source`. # # > Заметим, что `browser.page_source` — это не тот HTML-код, который был передан сервером, а тот, который мы построили на стороне клиента, в том числе, с помощью JavaScript. То есть это именно то, что нам нужно. # In[102]: from bs4 import BeautifulSoup bs = BeautifulSoup(browser.page_source) # С помощью просмотра кода элемента в браузере мы можем узнать, что интересующая нас информация находится в теге `` внутри тега `
` с `id='Searchresult'`. Извлечем её из `bs`. При этом результат `bs('div', id = 'Searchresult')` — это список (даже если результат только один). Поэтому нам надо взять первый элемент этого списка. Потом внутри `div` мы точно так же ищем `table`. # In[114]: div = bs('div', id='Searchresult')[0] # Можно было бы также использовать div = bs.find('div', id='Searchresult') table = div('table')[0] # Напечатаем ячейки в первых строках этой таблицы # In[107]: for row in table('tr')[:2]: # я печатаю только первые две строки for cell in row('td'): print(cell) print("---- Next cell ----") # Выглядит страшновато, но вообще-то видно, что вся интересующая нас информация как раз и находится в ячейках этой таблицы. Если нас интересует какая-то конкретная колонка, например дата и время отправки посылки, то её значения можно получить вот так: # In[109]: for row in table('tr'): cells = row('td') print(cells[3].string) # Если мы хотим выписать все элементы, то нам надо будет перейти на следующую страницу листинга. В браузере мы видим стрелочу `>`, ведущую к следующей странице результатов. Найдем элемент соответствующий этой стрелке. # In[110]: a = browser.find_element_by_link_text('>') # К счастью, на странице это единственный элемент с таким текстом. Чтобы кликнуть по нему, сделаем следующее # In[111]: a.click() # Видим, что загрузилась следующая страница, её можно обработать таким же образом, что и раньше. # # Это можно повторять в цикле, и таким образом обработать все записи. Нужно только учитывать то, что Python не будет ждать загрузки страницы в браузере, прежде, чем выполнять следующие команды, поэтому, делая `browser.page_source`, мы рискуем загрузить старую страницу. Чтобы решить эту проблему, сделаем в Python искусственную паузу. # In[34]: import time time.sleep(1) # Эта команда сделает паузу на любое время в секундах (здесь на 1 секунду). # Отметим, что в `Selenium` есть команд «назад»… # In[37]: browser.back() # …команда «вперёд»… # In[38]: browser.forward() # …и «обновить»: # In[115]: browser.refresh() # В общем, это полноценный браузер на дистанционном управлении. Теперь вы можете автоматизировать всё на свете!