{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Программирование на языке Python для сбора и анализа данных\n", "\n", "*Текст лекции: Будылин Р.Я., Щуров И.В., НИУ ВШЭ*\n", "\n", "Данный 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/).\n", "\n", "Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на [странице курса](http://math-info.hse.ru/s15/m)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## JSON и API. Управление браузером в RoboBrowser и Selenium " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Работа с API с помощью JSON" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В [прошлый раз](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 это позволяет.\n", "\n", "Напомним, что нашей задачей является получение списка всех статей из некоторой категории в Википедии. Вот так мы это делали в прошлый раз:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import requests\n", "from bs4 import BeautifulSoup\n", "\n", "url = \"https://en.wikipedia.org/w/api.php\"\n", "params = {\n", " 'action':'query',\n", " 'list':'categorymembers',\n", " 'cmtitle': 'Category:Physics',\n", " 'format': 'xml'\n", "}\n", "\n", "g = requests.get(url, params=params)\n", "g.ok" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как и в прошлый раз, мы взяли эти параметры из [документации](https://www.mediawiki.org/wiki/API:Categorymembers): `'action': 'query'` значит, что мы отправляем запрос, чтобы получить содержимое Википедии. Параметр `list` отвечает на вопрос список чего мы бы хотели получить. В данном случае это `categorymembers` — список элементов какой-то категории, `cmtitle` — это название категории, список элементов которой мы хотим получить. `'format'` — это формат ответа, который в прошлый раз был `xml`." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ "data = BeautifulSoup(g.text, features='xml')" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Physics\n", "Branches of physics\n", "Glossary of classical physics\n", "Outline of physics\n", "Portal:Physics\n", "Classical physics\n", "Epicatalysis\n", "Experimental physics\n", "Hume Feldman\n", "Microphysics\n" ] } ], "source": [ "for cm in data.api.query.categorymembers(\"cm\"):\n", " print(cm['title'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Попробуем теперь использовать JSON. Отличия в способе вызова минимальны: в качестве `format` указываем `json`:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "url = \"https://en.wikipedia.org/w/api.php\"\n", "params = {\n", " 'action':'query',\n", " 'list':'categorymembers',\n", " 'cmtitle': 'Category:Physics',\n", " 'format': 'json'\n", "}\n", "\n", "g = requests.get(url, params=params)\n", "g.ok" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Смотрим, что нам выдали по запросу. Это и есть JSON" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'{\"batchcomplete\":\"\",\"continue\":{\"cmcontinue\":\"page|4d4f4445524e20504859534943530a4d4f4445524e2050485953494353|844186\",\"continue\":\"-||\"},\"query\":{\"categorymembers\":[{\"pageid\":22939,\"ns\":0,\"title\":\"Physics\"},{\"pageid\":22688097,\"ns\":0,\"title\":\"Branches of physics\"},{\"pageid\":3445246,\"ns\":0,\"title\":\"Glossary of classical physics\"},{\"pageid\":24489,\"ns\":0,\"title\":\"Outline of physics\"},{\"pageid\":1653925,\"ns\":100,\"title\":\"Portal:Physics\"},{\"pageid\":151066,\"ns\":0,\"title\":\"Classical physics\"},{\"pageid\":47723069,\"ns\":0,\"title\":\"Epicatalysis\"},{\"pageid\":685311,\"ns\":0,\"title\":\"Experimental physics\"},{\"pageid\":48407923,\"ns\":0,\"title\":\"Hume Feldman\"},{\"pageid\":23581364,\"ns\":0,\"title\":\"Microphysics\"}]}}'" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "r.text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Он очень похож на описание объекта в Python и смысл квадратных и фигурных скобок такой же. Правда, есть и отличия: например, в Python одинарные и двойные кавычки ничем не отличаются, а в JSON можно использовать только двойные. Мы видим, что полученный нами JSON представляет собой словарь, значения которого — строки или числа, а также списки или словари, значения которых в свою очередь также могут быть строками, числами, списками, словарями и т.д. То есть получается такая довольно сложная структура данных. \n", "\n", "В данный момент тот факт, что перед нами сложная структура данных, видим только мы — с точки зрения Python, `r.text` это просто такая строка. Однако в модуле `requests` есть метод, позволяющий сразу выдать питоновский объект (словарь или список), если результат запроса возвращён в формате JSON. Так что нам не придётся использовать никакие дополнительные библиотеки." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": true }, "outputs": [], "source": [ "q = r.json()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что q это словарь" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "{'batchcomplete': '',\n", " 'continue': {'cmcontinue': 'page|4d4f4445524e20504859534943530a4d4f4445524e2050485953494353|844186',\n", " 'continue': '-||'},\n", " 'query': {'categorymembers': [{'ns': 0, 'pageid': 22939, 'title': 'Physics'},\n", " {'ns': 0, 'pageid': 22688097, 'title': 'Branches of physics'},\n", " {'ns': 0, 'pageid': 3445246, 'title': 'Glossary of classical physics'},\n", " {'ns': 0, 'pageid': 24489, 'title': 'Outline of physics'},\n", " {'ns': 100, 'pageid': 1653925, 'title': 'Portal:Physics'},\n", " {'ns': 0, 'pageid': 151066, 'title': 'Classical physics'},\n", " {'ns': 0, 'pageid': 47723069, 'title': 'Epicatalysis'},\n", " {'ns': 0, 'pageid': 685311, 'title': 'Experimental physics'},\n", " {'ns': 0, 'pageid': 48407923, 'title': 'Hume Feldman'},\n", " {'ns': 0, 'pageid': 23581364, 'title': 'Microphysics'}]}}" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "q" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "dict" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(q)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Содержательная информация хранится по ключу `'query'`. А уже внутри есть ключ `'categorymembers'`, значением которого является список всех категорий. Каждая категория отображается в виде словаря, записями которого являются разные параметры категории (например, `'title'` соответствует названию, а `pageid` — внутреннему идентификатору в системе)." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "list" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(q['query']['categorymembers'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это список всех членов категории. Мы можем посмотреть на них с помощью цикла" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Physics\n", "Branches of physics\n", "Glossary of classical physics\n", "Outline of physics\n", "Portal:Physics\n", "Classical physics\n", "Epicatalysis\n", "Experimental physics\n", "Hume Feldman\n", "Microphysics\n" ] } ], "source": [ "for cm in q['query']['categorymembers']:\n", " print(cm['title'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Преимущества JSON в том, что мы получаем готовый объект Python и нет необходимости использовать какие-то дополнительные библиотеки для того, чтобы с ним работать. Недостатком является то же самое: зачастую поиск информации в XML-файле может проводиться более эффективно, чем в JSON. Продемонстрируем это на уже рассмотренном примере. Чтобы получить список всех тегов ``, в которых хранилась информация об элементах категории в XML, мы использовали полный «путь»: \n", "```python\n", "for cm in data.api.query.categorymembers(\"cm\"):\n", " print(cm['title'])\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Однако, это можно бы сделать (в данном случае) гораздо короче. Если посмотреть на XML, то можно заметить, что в нём нет других тегов ``, кроме тех, которые нам нужны. С другой стороны, *Beautiful Soup* ищет все теги с данным именем, а не только те, которые являются потомками первого уровня для данного тега. Таким образом, код выше можно было бы переписать более коротко:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Physics\n", "Branches of physics\n", "Glossary of classical physics\n", "Outline of physics\n", "Portal:Physics\n", "Classical physics\n", "Epicatalysis\n", "Experimental physics\n", "Hume Feldman\n", "Microphysics\n" ] } ], "source": [ "for cm in data(\"cm\"):\n", " print(cm['title'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Конечно `data(\"cm\")` выглядит короче, чем `q['query']['categorymembers']`. В JSON мы не можем использовать подобные методы. Так что у обоих форматов есть свои плюсы и минусы." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Эмуляция действий с браузером" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Иногда нам нужно не просто скачать какую-нибудь информацию с сайта, а сделать что-то более сложное: например, залогиниться по своим аккаунтом, перейти на какую-то страницу, найти на ней ссылку, перейти по этой ссылке и скачать какую-то информацию. Продемонстрируем два инструмента для решения этой задачи: `robobrowser` и `selenium`.\n", "\n", "Рассмотрим эту задачу на примере работы с сервисом `informatics.mccme.ru`, который мы использовали для сдачи задач в начале нашего курса. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### RoboBrowser" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Пакет `robobrowser` позволяет работать с неким виртуальным браузером, который позволяет ходить по страничкам и получать их содержимое. На самом деле, этот браузер полностью эмулируется Python: фактически `robobrowser` представляет собой надстройку над `requests` и `BeautifulSoup`, позволяющую несколько упростить типичные операции типа «найти ссылку и пройти по ней»." ] }, { "cell_type": "code", "execution_count": 69, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from robobrowser import RoboBrowser" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если вдруг Python ругается, что нет каких-то модулей, то сделайте `pip install имя_модуля` в консоли." ] }, { "cell_type": "code", "execution_count": 70, "metadata": { "collapsed": true }, "outputs": [], "source": [ "q = RoboBrowser()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы создали виртуальный браузер." ] }, { "cell_type": "code", "execution_count": 71, "metadata": { "collapsed": true }, "outputs": [], "source": [ "ref = 'http://informatics.mccme.ru'\n", "q.open(ref)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "И сказали ему открыть ссылку. Мы можем посмотреть на html содержимое страницы командой ниже" ] }, { "cell_type": "code", "execution_count": 72, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Дистанционная подготовка\n", "// Заметим, что `browser.page_source` — это не тот HTML-код, который был передан сервером, а тот, который мы построили на стороне клиента, в том числе, с помощью JavaScript. То есть это именно то, что нам нужно." ] }, { "cell_type": "code", "execution_count": 102, "metadata": { "collapsed": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.5/site-packages/bs4/__init__.py:166: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system (\"lxml\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n", "\n", "To get rid of this warning, change this:\n", "\n", " BeautifulSoup([your markup])\n", "\n", "to this:\n", "\n", " BeautifulSoup([your markup], \"lxml\")\n", "\n", " markup_type=markup_type))\n" ] } ], "source": [ "from bs4 import BeautifulSoup\n", "bs = BeautifulSoup(browser.page_source)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С помощью просмотра кода элемента в браузере мы можем узнать, что интересующая нас информация находится в теге `` внутри тега `
` с `id='Searchresult'`. Извлечем её из `bs`. При этом результат `bs('div', id = 'Searchresult')` — это список (даже если результат только один). Поэтому нам надо взять первый элемент этого списка. Потом внутри `div` мы точно так же ищем `table`." ] }, { "cell_type": "code", "execution_count": 114, "metadata": { "collapsed": false }, "outputs": [], "source": [ "div = bs('div', id='Searchresult')[0]\n", "# Можно было бы также использовать div = bs.find('div', id='Searchresult')\n", "\n", "table = div('table')[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Напечатаем ячейки в первых строках этой таблицы" ] }, { "cell_type": "code", "execution_count": 107, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "---- Next cell ----\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "---- Next cell ----\n" ] } ], "source": [ "for row in table('tr')[:2]:\n", " # я печатаю только первые две строки\n", " for cell in row('td'):\n", " print(cell)\n", " print(\"---- Next cell ----\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выглядит страшновато, но вообще-то видно, что вся интересующая нас информация как раз и находится в ячейках этой таблицы. Если нас интересует какая-то конкретная колонка, например дата и время отправки посылки, то её значения можно получить вот так:" ] }, { "cell_type": "code", "execution_count": 109, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Дата\n", "2016-01-08 23:32:13\n", "2016-01-08 23:32:01\n", "2016-01-08 23:31:48\n", "2016-01-08 23:31:25\n", "2015-10-06 02:39:28\n", "2015-10-06 02:37:48\n", "2015-10-06 02:36:33\n", "2015-09-29 14:33:09\n", "2015-09-22 14:39:18\n", "2015-09-08 14:18:28\n" ] } ], "source": [ "for row in table('tr'):\n", " cells = row('td')\n", " print(cells[3].string)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если мы хотим выписать все элементы, то нам надо будет перейти на следующую страницу листинга. В браузере мы видим стрелочу `>`, ведущую к следующей странице результатов. Найдем элемент соответствующий этой стрелке." ] }, { "cell_type": "code", "execution_count": 110, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a = browser.find_element_by_link_text('>')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "К счастью, на странице это единственный элемент с таким текстом. Чтобы кликнуть по нему, сделаем следующее" ] }, { "cell_type": "code", "execution_count": 111, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a.click()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что загрузилась следующая страница, её можно обработать таким же образом, что и раньше.\n", "\n", "Это можно повторять в цикле, и таким образом обработать все записи. Нужно только учитывать то, что Python не будет ждать загрузки страницы в браузере, прежде, чем выполнять следующие команды, поэтому, делая `browser.page_source`, мы рискуем загрузить старую страницу. Чтобы решить эту проблему, сделаем в Python искусственную паузу." ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import time\n", "time.sleep(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Эта команда сделает паузу на любое время в секундах (здесь на 1 секунду)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отметим, что в `Selenium` есть команд «назад»…" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": false }, "outputs": [], "source": [ "browser.back()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "…команда «вперёд»…" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "collapsed": true }, "outputs": [], "source": [ "browser.forward()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "…и «обновить»:" ] }, { "cell_type": "code", "execution_count": 115, "metadata": { "collapsed": false }, "outputs": [], "source": [ "browser.refresh()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В общем, это полноценный браузер на дистанционном управлении. Теперь вы можете автоматизировать всё на свете!" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.0" } }, "nbformat": 4, "nbformat_minor": 0 }
IDУчастникЗадачаДатаЯзыкСтатусПройдено тестовБаллыПодробнее1758-36031Илья Щуров3451. Корень степени 10.2016-01-08 23:32:13Python 3.3
Частичное решение
  • ---
  • OK
  • Перетестировать
  • Зачтено/Принято
  • Ошибка оформления кода
  • Проигнорировано
  • Ошибка компиляции
  • Дисквалифицировано
  • Частичное решение
  • Ожидает проверки
  • Ошибка во время выполнения программы
  • Превышено максимальное время работы
  • Неправильный формат вывода
  • Неправильный ответ
  • Ошибка проверки,обратитесь к администраторам
  • Превышение лимита памяти
  • Security error
  • Тестирование...
  • Компилирование...
\n", "
00Подробнее