{ "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": [ "## Лекция №9: Поиск данных на HTML-страницах и работа с API с помощью XML" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Поиск данных на HTML-странице\n", "На предыдущей лекции мы познакомились с библиотекой *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).\n", "\n", "Заметим, что в Википедии встречаются ссылки двух типов: внутренние (на другие страницы Википедии) и внешние (на другие сайты), причём они различаются по оформлению — у внешних ссылок есть небольшая стрелочка. Например, мы хотим выбрать все внешние ссылки. Как это сделать?\n", "\n", "Для того, чтобы браузер отображал внешние ссылки не так, как внутренние, разработчики Википедии используют так называемые css-классы (конечно, это касается не только Википедии — это вообще основной инструмент современного веба). Теги ``, соответствующие внешним ссылкам, имеют специальный атрибут `class`, значение которого включает слово `external`. Именно по нему можно понять, что речь идёт о внешней ссылке. Это можно было бы увидеть, изучив исходный код страницы, но мы сделаем проще: воспользуемся встроенным инспектором кода в Firefox (в других браузерах есть аналоги — встроенные или в виде расширений)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![Тег `` с классом `external`](http://math-info.hse.ru/f/2015-16/all-py/m_and_m_1.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На скриншоте видно, что в исходном коде в атрибуте `class` тега `` указана строчка `\"external text\"`, а не просто `\"external\"` — дело в том, что теги могут иметь сразу несколько классов одновременно, и в данном случае `external` и `text` — это два класса данной ссылки. Но мы будем ориентироваться только на `external`.\n", "\n", "Итак, мы хотим найти все ссылки с классом `external`. Это очень просто." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from bs4 import BeautifulSoup\n", "import requests" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "url = \"https://ru.wikipedia.org/w/index.php?oldid=75475510\"\n", "# Используем постоянную ссылку для воспроизводимости результатов" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "g = requests.get(url)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g.ok" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [], "source": [ "page = BeautifulSoup(g.text, \"html.parser\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "//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\n", "http://dombulgakova.ru/bulgakovskaya-biblioteka-2/lidiya-yanovskaya-o-romane-bulgakova-mas/\n", "http://magazines.russ.ru/zvezda/2000/6/suhih.html\n", "http://magazines.russ.ru/sib/2013/6/10d.html\n", "http://mirknig.mobi/data/2012-12-06/1291088/Chudakova_Vremya_chitat_3_Ne_dlya_vzroslyih._Vremya_chitat_Polka_tretya.1291088.pdf\n", "http://www.e-reading.club/chapter.php/39079/24/Zerkalov_-_Etika_Mihaila_Bulgakova.html\n", "http://www.litmir.co/br/?b=65955&p=163\n", "http://www.bulgakov.ru/m/morphiy/\n", "http://www.russofile.ru/articles/article_67.php\n", "http://feb-web.ru/feb/mayakovsky/texts/ms0/ms9/ms9-183-.htm\n", "http://magazines.russ.ru/slovo/2008/58/ko11.html\n", "http://magazines.russ.ru/nlo/2005/76/bra.html\n", "http://www.rg.ru/2005/12/16/master.html\n", "http://magazines.russ.ru/voplit/2009/4/ga8.html\n", "http://magazines.russ.ru/znamia/2011/10/bo30.html\n", "http://kinoart.ru/archive/2006/01/n1-article4\n", "http://magazines.russ.ru/znamia/2007/1/bu17.html\n", "http://izvestia.ru/news/499828\n", "http://www.rg.ru/2011/09/08/mht.html\n", "https://musicbrainz.org/work/2fa8ecec-2103-4aa7-97f1-2fbfd1c0607d\n", "http://catalogue.bnf.fr/ark:/12148/cb11941979p\n", "http://www.idref.fr/027361241\n", "http://viaf.org/viaf/175580487\n" ] } ], "source": [ "for link in page.findAll(\"a\", class_='external'):\n", " print(link['href'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно из примера выше, достаточно методу `findAll()` передать дополнительный именованный параметр `class_` — обратите внимание на нижнее подчёркивание, без него получится синтаксическая ошибка, потому что слово `class` имеет специальный смысл в Python.\n", "\n", "### Классы и поиск по дереву\n", "Решим теперь другую задачу: допустим, мы хотим найти все ссылки в разделе «Примечания», где находятся сноски к основному тексту. С помощью инспектора кода мы легко можем заметить, что весь этот раздел находится внутри тега `
` (этот тег описывает прямоугольные блоки, из которых состоят веб-страницы, и является основным тегом для современной веб-вёрстки), имеющем класс `references-small`." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [], "source": [ "divs = page.findAll('div', class_='references-small')" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(divs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Такой `
` оказался единственным на странице. Вот и хорошо. Найдём теперь все теги ``, являющиеся потомками (возможно, отдалёнными) этого `
`'а." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "#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\n", "#CITEREF.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.811999\n", "#cite_ref-.D0.9B.D0.BE.D1.81.D0.B5.D0.B2.E2.80.941993.E2.80.94.E2.80.94407_2-0\n", "#cite_ref-.D0.9B.D0.BE.D1.81.D0.B5.D0.B2.E2.80.941993.E2.80.94.E2.80.94407_2-1\n", "#CITEREF.D0.9B.D0.BE.D1.81.D0.B5.D0.B21993\n", "#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\n", "#CITEREF.D0.9B.D0.B5.D1.81.D1.81.D0.BA.D0.B8.D1.811999\n", "#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\n", "#CITEREF.D0.A7.D1.83.D0.B4.D0.B0.D0.BA.D0.BE.D0.B2.D0.B01988\n", "#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\n" ] } ], "source": [ "div = page.findAll('div', class_='references-small')[0]\n", "for link in div(\"a\")[0:10]:\n", " print(link['href'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для экономии места я вывел только первые 10 ссылок. Это внутренние ссылки на другие фрагменты страницы, поэтому они начинаются с символа `#`. Легко увидеть, что мы получили то, что требовалось.\n", "\n", "### Некоторые итоги\n", "Подведём некоторые итоги по поводу поиска информации в HTML-файлах:\n", "\n", "- Это всегда творческий процесс: все сайты разные и нет единого рецепта, как извлекать из них нужную информацию.\n", "- В первую очередь нужно посмотреть в исходник интересующей вас странички. Проще всего это делать с помощью инструментария веб-разработчика типа Firebug или встроенного инспектора кода в Firefox или аналогичных инструментов для других браузеров.\n", "- В HTML-дереве можно ориентироваться по названиям тегов, их классам, id'ам и другим свойствам.\n", "- Можно искать нужный элемент итеративно — сначала найти «большой» тег, который включает наш элемент, потом найти в нём элемент поменьше и т.д." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### API и XML\n", "Анализируя веб-страницы и извлекая из них информацию мы пытаемся написать программу, которая бы действовала как человек. Это бывает непросто. К счастью, всё чаще разнообразные сайты предлагают информацию, которую может легко обрабатывать не только человек, но и другая программа. Это называется API — application program interface. Обычный интерфейс — это способ взаимодействия человека с программой, а API — одной программы с другой. Например, вашего скрипта на Python с удалённым веб-сервером.\n", "\n", "Для хранения веб-страниц, которые читают люди, используется язык HTML. Для хранения произвольных структурированных данных, которыми обмениваются между собой программы, используются другие языки — в частности, язык XML, похожий на HTML. Вернее было бы сказать, что XML это *метаязык*, то есть способ описания языков. В отличие от HTML, набор тегов в XML-документе может быть произвольным (и определяется разработчиком конкретного диалекта XML). Например, если бы мы хотели описать в виде XML некоторую студенческую группу, это могло бы выглядеть так:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```xml\n", "\n", " 134\n", " \n", " Виталий\n", " Иванов\n", " \n", " \n", " Мария\n", " Петрова\n", " \n", "\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для обработки XML-файлов можно использовать тот же пакет *Beautiful Soup*, который мы уже использовали для работы с HTML. Единственное различие — нужно указать дополнительный параметр `feautres=\"xml\"` при вызове функции `BeautifulSoup` — чтобы он не искал в документе HTML-теги." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "group = \"\"\"\n", "134\n", "\n", "Виталий\n", "Иванов\n", "\n", "\n", "Мария\n", "Петрова\n", "\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", " \n", " 134\n", " \n", " \n", " \n", " Виталий\n", " \n", " \n", " Иванов\n", " \n", " \n", " \n", " \n", " Мария\n", " \n", " \n", " Петрова\n", " \n", " \n", "\n" ] } ], "source": [ "obj = BeautifulSoup(group, features=\"xml\")\n", "print(obj.prettify())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вот так мы можем найти в нашем XML-документе номер группы:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'134'" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "obj.group.number.string" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это значит «в объекте `obj` найти тег `group` в нём найти тег `number` и выдать в виде строки то, что в нём содержится.\n", "\n", "А вот так можно перечислить всех студентов:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Иванов Виталий\n", "Петрова Мария\n" ] } ], "source": [ "for student in obj.group.findAll('student'):\n", " print(student.lastname.string, student.firstname.string)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Получаем список статей из категории в Википедии" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Допустим, нам потребовалось получить список всех статей из некоторой категории в Википедии. Мы могли бы открыть эту категорию в браузере и дальше действовать теми методами, которые обсуждались выше. Однако, на наше счастье разработчики Википедии сделали удобное API. Чтобы научиться с ним работать, придётся познакомиться с [документацией](https://www.mediawiki.org/wiki/API:Main_page) (так будет с любым API), но это кажется сложным только в первый раз. Ну хорошо, в первые 10 раз. Или 20. Потом будет проще.\n", "\n", "Итак, приступим. Взаимодействие с сервером при помощи API происходит с помощью отправки специальным образом сформированных запросов и получения ответа в одном из машинночитаемых форматов. Нас будет интересовать формат XML, хотя бывают и другие (позже мы познакомимся с JSONN). А вот такой запрос мы можем отправить:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:Physics&cmsort=timestamp&cmdir=desc&format=xmlfm" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Строка `https://en.wikipedia.org/w/api.php` (до знака вопроса) — это *точка входа* в API. Всё, что идёт после знака вопроса — это, собственно, запрос. Он представляет собой что-то вроде словаря и состоит из пар «ключ=значение», разделяемых амперсандом `&`. Некоторые символы приходится кодировать специальным образом.\n", "\n", "Например, в адресе выше сказано, что мы хотим сделать запрос (`action=query`), перечислить элементы категории `list=categorymembers`, в качестве категории, которая нас интересует, указана `Category:Physics` (`cmtitle=Category:Physics`) и указаны некоторые другие параметры. Если кликнуть по этой ссылке, откроется примерно такая штука:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```xml\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы видим здесь разные теги, и видим, что нас интересуют теги ``, находящиеся внутри тега ``.\n", "\n", "Давайте сделаем соответствующий запрос с помощью Python. Для этого нам понадобится уже знакомый модуль `requests`." ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": true }, "outputs": [], "source": [ "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)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, список параметров мы передаем в виде обычного словаря. Посмотрим, что получилось." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g.ok" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Всё хорошо. Теперь используем *Beautiful Soup* для обработки этого XML." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [], "source": [ "data = BeautifulSoup(g.text, features='xml')" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "print(data.prettify())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Найдём все вхождения тега `` и выведем их атрибут `title`:" ] }, { "cell_type": "code", "execution_count": 27, "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": [ "Можно было упростить поиск ``, не указывая «полный путь» к ним:" ] }, { "cell_type": "code", "execution_count": 28, "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": [ "По умолчанию сервер вернул нам список из 10 элементов. Если мы хотим больше, нужно воспользоваться элементом `continue` — это своего рода гиперссылка на следующие 10 элементов." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'page|4d4f4445524e20504859534943530a4d4f4445524e2050485953494353|844186'" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data.find(\"continue\")['cmcontinue']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мне пришлось использовать метод `find()` вместо того, чтобы просто написать `data.continue`, потому что `continue` в Python имеет специальный смысл.\n", "\n", "Теперь добавим `cmcontinue` в наш запрос и выполним его ещё раз:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": true }, "outputs": [], "source": [ "params['cmcontinue'] = data.api(\"continue\")[0]['cmcontinue']" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Modern physics\n", "Northwest Nuclear Consortium\n", "Phase Stretch Transform\n", "Statistical mechanics\n", "Surface science\n", "Wigner rotation\n", "Category:Concepts in physics\n", "Category:Physicists\n", "Category:Applied and interdisciplinary physics\n", "Category:Atomic, molecular, and optical physics\n" ] } ], "source": [ "g = requests.get(url, params=params)\n", "data = BeautifulSoup(g.text, features='xml')\n", "for cm in data.api.query.categorymembers(\"cm\"):\n", " print(cm['title'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы получили следующие 10 элементов из категории. Продолжая таким образом, можно выкачать её даже целиком (правда, для этого потребуется много времени).\n", "\n", "Аналогичным образом реализована работа с разнообразными другими API, имеющимися на разных сайтах. Где-то API является полностью открытым (как в Википедии), где-то вам потребуется зарегистрироваться и получить application id и какой-нибудь ключ для доступа к API, где-то попросят даже заплатить (например, автоматический поиск в Google стоит что-то вроде 5 долларов за 100 запросов). Есть API, которые позволяют только читать информацию, а бывают и такие, которые позволяют её править. Например, можно написать скрипт, который будет автоматически сохранять какую-то информацию в Google Spreadsheets. Всякий раз при использовании API вам придётся изучить его документацию, но это в любом случае проще, чем обрабатывать HTML-код. Иногда удаётся упростить доступ к API, используя специальные библиотеки." ] } ], "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 }