Парсинг сайтов с помощью фреймворка Scrapy: различия между версиями

Материал из GIS-Lab
Перейти к навигации Перейти к поиску
мНет описания правки
 
(не показано 29 промежуточных версий 2 участников)
Строка 1: Строка 1:
{{Статья|Черновик}}
{{Статья|Опубликована|scrapy}}


== Введение ==
== Введение ==


Синтаксический анализ (парсинг) сайтов хоть и не имеет прямого отношения к пространственным данным, но владение основами которого полезно любому, работающему с ними. В свете роста числа онлайн-ресурсов, публикующих открытые данные, ценность умения извлекать из них необходимую информацию многократно повышается. Приведём небольшой пример. Допустим, нам необходимо составить набор тематических карт, отражающих результаты Выборов Президента Российской Федерации 2012. Исходные данные можно получить на сайте [http://www.vybory.izbirkom.ru/region/region/izbirkom?action=show&root=1000020&tvd=100100031793849&vrn=100100031793505&region=0&global=true&sub_region=0&prver=0&pronetvd=null&vibid=100100031793849&type=226 ЦИК России]. Однако, согласитесь, что непосредственно использовать данные, предоставляемые в таком виде, очень сложно. Плюс это усложняется тем, что данные по разным регионам расположены на разных страницах. Гораздо удобнее было бы, чтобы вся эта информация была представлена, например, в виде одного или нескольких структурированных CSV или XML файлов (в идеале, конечно было бы иметь еще и некоторый API, позволяющий выполнять запросы к таким ресурсам), однако зачастую формирование подобных файлов отдаётся на откуп конечному пользователю (почему так происходит - это вопрос отдельный). Именно проблеме создания таких вот аггрегированных наборов данных и посвещена данная статья. В связи с [http://ru.wikipedia.org/wiki/%D0%A4%D0%B5%D0%B4%D0%B5%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD_%D0%BE%D1%82_28_%D0%B4%D0%B5%D0%BA%D0%B0%D0%B1%D1%80%D1%8F_2012_%D0%B3%D0%BE%D0%B4%D0%B0_%E2%84%96_272-%D0%A4%D0%97 недавними событиями] в качестве целевого сайта, который мы будем парсить выбран сайт [http://detskiedomiki.ru/ ДетскиеДомики.ру], а именно его раздел [http://detskiedomiki.ru/guide/child/ Детские учреждения]. Предполагается, что информация, расположенная на этом сайте станет в ближайшее время очень востребованной.
Синтаксический анализ (парсинг) сайтов хоть и не имеет прямого отношения к пространственным данным, но владение основами которого полезно любому, работающему с ними. В свете роста числа онлайн-ресурсов, публикующих открытые данные, ценность умения извлекать из них необходимую информацию многократно повышается. Приведём небольшой пример. Допустим, нам необходимо составить набор тематических карт, отражающих результаты Выборов Президента Российской Федерации 2012. Исходные данные можно получить на сайте [http://www.vybory.izbirkom.ru/region/region/izbirkom?action=show&root=1000020&tvd=100100031793849&vrn=100100031793505&region=0&global=true&sub_region=0&prver=0&pronetvd=null&vibid=100100031793849&type=226 ЦИК России]. Однако, согласитесь, что непосредственно использовать данные, предоставляемые в таком виде, очень сложно. Плюс это усложняется тем, что данные по разным регионам расположены на разных страницах. Гораздо удобнее было бы, чтобы вся эта информация была представлена, например, в виде одного или нескольких структурированных CSV или XML файлов (в идеале, конечно было бы иметь еще и некоторый API, позволяющий выполнять запросы к таким ресурсам), однако зачастую формирование подобных файлов отдаётся на откуп конечному пользователю (почему так происходит - это вопрос отдельный). Именно проблеме создания таких вот аггрегированных наборов данных и посвещена данная статья. В связи с [http://ru.wikipedia.org/wiki/%D0%A4%D0%B5%D0%B4%D0%B5%D1%80%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD_%D0%BE%D1%82_28_%D0%B4%D0%B5%D0%BA%D0%B0%D0%B1%D1%80%D1%8F_2012_%D0%B3%D0%BE%D0%B4%D0%B0_%E2%84%96_272-%D0%A4%D0%97 недавними событиями] в качестве целевого сайта, который мы будем парсить, выбран сайт [http://detskiedomiki.ru/ ДетскиеДомики.ру], а именно его раздел [http://detskiedomiki.ru/guide/child/ Детские учреждения]. Предполагается, что информация, расположенная на этом сайте будет в ближайшее время очень востребованной.


В качестве инструмента, которым будет выполняться парсинг выбран [http://scrapy.org/ Scrapy] - очень гибкий фреймворк, написанный на Python, позволяющего решать широкий спектр задач. Информации о Scrapy на русском языке не так много, [http://habrahabr.ru/post/115710/ например], но этот пробел компенсируется отличной [http://doc.scrapy.org/ документацией].
В качестве инструмента, которым будет выполняться парсинг, выбран [http://scrapy.org/ Scrapy] - очень гибкий фреймворк, написанный на Python и позволяющий решать широкий спектр задач. Информации о Scrapy на русском языке не так много, [http://habrahabr.ru/post/115710/ например] и [http://www.minisotm.ru/blog/programming/36.html ещё], но этот пробел компенсируется отличной [http://doc.scrapy.org/ документацией].


В данной статье мы не будем подробно заострять внимание на всех технических возможностях Scrapy, а просто рассмотрим поэтапно как была решена определённая задача. Если кто-то захочет использовать данный материал как отправную точку для решения собственной задачи, но не найдёт здесь ответов на свой вопрос - спрашивайте в форуме, постараемся помочь.
В данной статье мы не будем подробно заострять внимание на всех технических возможностях Scrapy, а просто рассмотрим поэтапно, как была решена определённая задача. Если кто-то захочет использовать данный материал как отправную точку для решения собственной задачи, но не найдёт здесь ответов на свой вопрос - спрашивайте в форуме, постараемся помочь.


== Установка Scrapy ==
== Установка Scrapy ==
 
===Unix===
В *nix-системах установка Scrapy - тривиальная задача, чего не скажешь о [http://doc.scrapy.org/en/0.16/intro/install.html#windows Windows] (чем не повод наконец-то отказаться от неё или по крайней мере посмотреть вокруг), поэтому мы рассмотрим первый вариант:
В *nix-системах установка Scrapy - тривиальная задача:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Строка 21: Строка 21:
pip install Scrapy
pip install Scrapy
</syntaxhighlight>
</syntaxhighlight>
===Windows===
В Windows всё [http://doc.scrapy.org/en/0.16/intro/install.html#windows сложнее]:
#Перейдите на страницу [http://slproweb.com/products/Win32OpenSSL.html Win32 OpenSSL]
#Скачайте и установите Visual C++ 2008 redistributables для вашей версии Windows и архитектуры
#Скачайте OpenSSL для вашей версии Windows и архитектуры (вам нужна обычная версия - не light)
#Добавьте путь c:\openssl-win32\bin (или тот куда вы установили OpenSSL) к переменной PATH
#Установите twisted отдельно (например для [http://twistedmatrix.com/Releases/Twisted/12.3/Twisted-12.3.0.win32-py2.7.exe 2.7])
#Установите lxml отдельно (например для [https://pypi.python.org/packages/2.7/l/lxml/lxml-2.2.8.win32-py2.7.exe#md5=deb95d53dbd3734ecfb4f69850758427 2.7])
#если easy_install не установлен, установите сначала [https://pypi.python.org/pypi/setuptools#files его самого], он ставится в C:\Python27\Scripts\ и запускается из cmd, а не из Python
далее установите:
<pre>easy_install pip
pip install Scrapy
pip install zope.interface
pip install w3lib</pre>


== Создание проекта ==
== Создание проекта ==


После того, как Scrapy установлен, необходимо создать каталог проекта. Для этого, находясь в каталоге ''~/projects/scrapy'', необходимо выполнить команду:
После того, как Scrapy будет установлен, нужно создать каталог проекта. Для этого, находясь в каталоге ''~/projects/scrapy'', выполните команду:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Строка 47: Строка 64:
* ''scrapy.cfg'' - настройки проекта;
* ''scrapy.cfg'' - настройки проекта;
* ''orphanage/'' - Python модуль проекта;
* ''orphanage/'' - Python модуль проекта;
* ''orphanage/items.py'' - классы, которые перечисляют поля собираемых данных;
* ''orphanage/items.py'' - классы, описывающие модель собираемых данных;
* ''orphanage/pipelines.py'' - используется в основном для описания каких-нибудь кастомных форматов сохранения результатов парсинга;
* ''orphanage/pipelines.py'' - используется в основном для описания пользовательских форматов сохранения результатов парсинга;
* ''orphanage/settings.py'' - пользовательские настройки [http://ru.wikipedia.org/wiki/%CF%EE%E8%F1%EA%EE%E2%FB%E9_%F0%EE%E1%EE%F2 паука];
* ''orphanage/settings.py'' - пользовательские настройки [http://ru.wikipedia.org/wiki/%CF%EE%E8%F1%EA%EE%E2%FB%E9_%F0%EE%E1%EE%F2 паука];
* ''orphanage/spiders/'' - директория, в которой хранятся файлы с классами пауков. Каждого паука принято писать в отдельном файле..
* ''orphanage/spiders/'' - директория, в которой хранятся файлы с классами пауков. Каждого паука принято писать в отдельном файле.


== Описание модели данных ==
== Описание модели данных ==


Модель представляет собой отдельный класс, содержащий перечень атрибутивных полей собираемых данных. Прежде чем описывать модель, необходимо определиться во-первых с объектом парсинга, а во-вторых с набором его атрибутов, которые мы хотим извлечь из целевого ресурса. Объектом парсинга в нашем случае будут являться детские дома, а набором атрибутов - их характеристики (в случае если у каждого объекта перечень доступных атрибутов различный, то итоговым набором будет являться объединение множеств атрибутов всех объектов). Находим [http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65 страницу], содержащую наиболее полный перечень атрибутов (в данном случае факт полнотоы был определён путём сопоставления представленных атрибутов и [http://detskiedomiki.ru/UserFiles/Anketa%20detskogo%20uchrezhdeniya(2).doc анкеты детского учреждения]) и выписываем их: "Рег. номер", "Регион", "Район", "Тип учреждения", "Название", "Почтовый адрес" и т.д. (всего 34 атрибута). После того, как мы определились с перечнем атрибутов, отразим их в специальном классе. Для этого открываем файл ''items.py'' и описываем класс (название - произвольное):
Модель представляет собой отдельный класс, содержащий перечень атрибутивных полей собираемых данных. Прежде чем описывать модель, необходимо определиться во-первых с объектом парсинга, а во-вторых с набором его атрибутов, которые мы хотим извлечь из целевого ресурса. Объектом парсинга в нашем случае будут являться детские дома, а набором атрибутов - их характеристики (в случае если у каждого объекта перечень доступных атрибутов различный, то итоговым набором будет являться объединение множеств атрибутов всех объектов). Находим [http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65 страницу], содержащую наиболее полный перечень атрибутов (в данном случае факт полноты был определён путём сопоставления представленных атрибутов и [http://detskiedomiki.ru/UserFiles/Anketa%20detskogo%20uchrezhdeniya(2).doc анкеты детского учреждения]) и выписываем их: "Рег. номер", "Регион", "Район", "Тип учреждения", "Название", "Почтовый адрес" и т.д. (всего 34 атрибута). После того, как мы определились с перечнем атрибутов, отразим их в специальном классе. Для этого открываем файл ''items.py'' и описываем класс (название - произвольное):


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Строка 99: Строка 116:
</syntaxhighlight>
</syntaxhighlight>


Как можно увидеть представленный класс содержит записи вида ''имя атрибута = Field()'', где в качестве ''имя атрибута'' рекомендуется использовать английский вариант названия соответствующего атрибута. Кроме того, в класс был добавлен ещё один атрибут ''url'', который подразумевает хранение для объекта URL той страницы, из которой были извлечены данные.
Как можно увидеть представленный класс содержит записи вида ''имя атрибута = Field()'', где в качестве ''имя атрибута'' рекомендуется использовать английский вариант названия соответствующего атрибута. Кроме того, в класс был добавлен ещё один атрибут ''url'', который предназначен для хранения URL той страницы, из которой были извлечены данные.


== Создание паука ==
== Создание паука ==


[http://ru.wikipedia.org/wiki/%CF%EE%E8%F1%EA%EE%E2%FB%E9_%F0%EE%E1%EE%F2 Паук] - это основная часть системы. Паук в нашем случае будет представлять собой отдельный класс, описывающий способ обхода ресурса и собирающий необходимую информацию в соответствии с описанной на предыдущем этапе моделью.
[http://ru.wikipedia.org/wiki/%CF%EE%E8%F1%EA%EE%E2%FB%E9_%F0%EE%E1%EE%F2 Паук] - это основная часть нашей системы, представляющий собой отдельный класс, описывающий способ обхода ресурса и собирающий необходимую информацию в соответствии с описанной на предыдущем этапе моделью.


Переходим в директорию ''orphanage/spiders/'' и создаём файл с описанием паука (''detskiedomiki.py'' - название произвольное). Внутри файла описываем класс (имя класса - произвольное):
Переходим в директорию ''orphanage/spiders/'' и создаём файл с описанием паука ''detskiedomiki.py'' (название произвольное). Внутри файла описываем класс (имя класса - произвольное):


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Строка 127: Строка 144:


     def parse_item(self, response):
     def parse_item(self, response):
         pass
         ...
</syntaxhighlight>
</syntaxhighlight>


Строка 133: Строка 150:
* ''allowed_domains'' - список доменов по которым может ходить паук;
* ''allowed_domains'' - список доменов по которым может ходить паук;
* ''start_urls'' - список URL, с которых начинается обход ресурса;
* ''start_urls'' - список URL, с которых начинается обход ресурса;
* ''rules'' - список правил обхода ресурса. В данном случае данный список содержит 2 правила - первое будет срабатывать при попадании паука на страницы, URL которых будут содержать ''act=home_reg'' или ''act=home_zone''. Под срабатыванием в данном случае подразумевается переход по ссылкам, извлеченным из этих страниц (за что отвечает ''follow=True''). Второе правило будет срабатывать при попадании паука на страницы, URL которых будут содержать ''act=home_more'' (именно на этих страницах содержится информация, которую мы хотим извлечь), например http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65. В данном случае у правила не указан аргумент ''follow'', что означает, что при попадании паука на данную страницу ссылки из неё не извлекаются, вместо этого содержимое страницы передаётся на вход функции, указанной в аргументе ''callback'' - назовём её в нашем случае ''parse_item''.
* ''rules'' - список правил обхода ресурса. В данном случае данный список содержит 2 правила - первое будет срабатывать при попадании паука на страницы, URL которых содержит ''act=home_reg'' или ''act=home_zone''. Под срабатыванием в данном случае подразумевается переход по ссылкам, извлеченным из этих страниц (за что отвечает аргумент ''follow=True''). Второе правило будет срабатывать при попадании паука на страницы, URL которых содержит ''act=home_more'' (именно на этих страницах содержится информация, которую мы хотим извлечь), например http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65. В данном случае у правила не указан аргумент ''follow'', что означает, что при попадании паука на данную страницу ссылки из неё не извлекаются, вместо этого содержимое страницы передаётся на вход функции, указанной в аргументе ''callback'' - назовём её в нашем случае ''parse_item''.
 
Прежде чем переходить к рассмотрению функции ''parse_item'', сделаем небольшое техническое отступление. Существет несколько библиотек, предназначенных для извлечения данных из HTML-документов. Наиболее распространённые это [http://www.crummy.com/software/BeautifulSoup/ BeautifulSoup] (популярная, но при этом обладающая одним недостатком - очень медленная) и [http://codespeak.net/lxml/ lxml]. В Scrapy используется собственный механизм извлечения данных (основанный так же как и lxml на [http://xmlsoft.org/ libxml2]) из HTML-документов - селекторы (''selectors''). Фактически, селекторы - это отдельные классы, при создании экземпляров которых на вход передаётся объект класса [https://scrapy.readthedocs.org/en/latest/topics/request-response.html#scrapy.http.Response Response] (представляющий собой ответ сервера). В Scrapy доступно 2 [https://scrapy.readthedocs.org/en/latest/topics/selectors.html селектора] - HtmlXPathSelector и XmlXPathSelector, предназначенных для парсинга HTML и XML документов соответственно.
 
Для того, чтобы понять, как работают селекторы, перейдите в директорию ''~/projects/scrapy/orphanage'' и выполните команду:
 
<syntaxhighlight lang="bash">
scrapy shell "http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65"
</syntaxhighlight>
 
В результате выполнения данной команды будет осуществлен запрос к указанной странице, после чего вы попадете в интерактивную консоль, в которой уже будет ряд созданных Scrapy объектов, в том числе и объект ''hxs'' класса ''HtmlXPathSelector''. Данную консоль очень удобно использовать для составления [http://ru.wikipedia.org/wiki/XPath XPath] выражений - основного инструмента доступа к элементам HTML-документа, используемого в Scrapy. Например, мы хотим извлечь значение атрибута ''Рег. номер'' из нашей страницы, для этого в консоли вызываем метод [https://scrapy.readthedocs.org/en/latest/topics/selectors.html#scrapy.selector.XPathSelector select] объекта ''hxs'' и в качестве аргумента используем соответствующую XPath-строку:
 
<syntaxhighlight lang="bash">
In [10]: hxs.select("//td[text()='%s']/following-sibling::td/text()" % "Рег. номер:".decode('utf-8'))
Out[10]: [<HtmlXPathSelector xpath=u"//td[text()='\u0420\u0435\u0433. \u043d\u043e\u043c\u0435\u0440:']/following-sibling::td/text()" data=u'01-04-15-01'>]
</syntaxhighlight>
 
В результате получим массив, состоящий из списка объектов класса ''HtmlXPathSelector'', то есть метод ''select'' объекта класса
''HtmlXPathSelector'' возвращает список объектов класса ''HtmlXPathSelector'' (что очень удобно в случае парсинга вложенных данных). Для извлечения непосредственно данных необходимо применить метод [https://scrapy.readthedocs.org/en/latest/topics/selectors.html#scrapy.selector.XPathSelector extract]:


Переходим к описанию функции ''parse_item'':
<syntaxhighlight lang="bash">
In [11]: hxs.select("//td[text()='%s']/following-sibling::td/text()" % "Рег. номер:".decode('utf-8')).extract()
Out[11]: [u'01-04-15-01']
</syntaxhighlight>
 
В результате мы получим список значений тех элементов, которые удовлетворяют XPath-выражению.
 
А теперь рассмотрим нашу функцию ''parse_item'':


<syntaxhighlight lang='python'>
<syntaxhighlight lang='python'>
class OrphanLoader(XPathItemLoader):
    default_output_processor = TakeFirst()
def parse_item(self, response):
    hxs = HtmlXPathSelector(response)
    l = OrphanLoader(OrphanageItem(), hxs)
    #
    l.add_xpath('id', "//td[text()='%s']/following-sibling::td/text()" % u"Рег. номер:")
    ...
    l.add_value('url', response.url)
    return l.load_item()
</syntaxhighlight>
Загрузчики ([https://scrapy.readthedocs.org/en/latest/topics/loaders.html Item Loaders]) - специальные классы, облегчающие заполнение объекта класса модели данных (в нашем случае ''OrphanageItem''). В данном случае мы расширяем базовый класс загрузчика [https://scrapy.readthedocs.org/en/latest/topics/loaders.html#scrapy.contrib.loader.XPathItemLoader XPathItemLoader] путём создания нового класса ''OrphanLoader'' и устанавливаем свойство default_output_processor в значение [https://scrapy.readthedocs.org/en/latest/topics/loaders.html#scrapy.contrib.loader.processor.TakeFirst TakeFirst], это сделано из следующих соображений. При извлечении данных из HTML-документа результат возвращается в виде массива значений (даже если этот массив состоит из одного элемента как в нашем случае), использование процессора ''TakeFirst'' позволяет из полученного массива значений извлекать первый элемент.
Следующей строкой мы создаём объект класса ''OrphanLoader'':
<syntaxhighlight lang="python">
l = OrphanLoader(OrphanageItem(), hxs)
</syntaxhighlight>
Первый аргумент - объект класса модели данных ''OrphanageItem'', второй класса ''HtmlXPathSelector''. Следующая (и последующие) строка вида:
<syntaxhighlight lang="python">
l.add_xpath('id', "//td[text()='%s']/following-sibling::td/text()" % u"Рег. номер:")
</syntaxhighlight>
говорит о том, что атрибут ''id'' объекта класса нашей модели данных будет извлекаться в соответствии с переданным выражением XPath.
<syntaxhighlight lang="python">
l.add_value('url', response.url)
</syntaxhighlight>
Метод ''add_value'' позволяет задать значение указанного (''url'') атрибута вручную. Следующая строка:
<syntaxhighlight lang="python">
return l.load_item()
</syntaxhighlight>
заполняет атрибуты объекта ''OrphanageItem'' в соответствии с настройками загрузчика.
Итоговый вариант файла ''orphanage/spiders/detskiedomiki.py'':
<syntaxhighlight lang="python">
# -*- encoding: utf-8 -*-
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.contrib.loader.processor import TakeFirst
from scrapy.contrib.loader import XPathItemLoader
from scrapy.selector import HtmlXPathSelector
from orphanage.items import OrphanageItem
class OrphanLoader(XPathItemLoader):
    default_output_processor = TakeFirst()
class OrphanSpider(CrawlSpider):
    name = "detskiedomiki"
    allowed_domains = ["www.detskiedomiki.ru"]
    start_urls = ["http://www.detskiedomiki.ru/guide/child/"]
    rules = (
        Rule(SgmlLinkExtractor(allow=('act=home_reg', 'act=home_zone')), follow=True),
        Rule(SgmlLinkExtractor(allow=('act=home_more')), callback='parse_item'),
    )
     def parse_item(self, response):
     def parse_item(self, response):
         hxs = HtmlXPathSelector(response)
         hxs = HtmlXPathSelector(response)
Строка 147: Строка 258:
         l.add_xpath('district', "//td[text()='%s']/following-sibling::td/text()" % u"Район:")
         l.add_xpath('district', "//td[text()='%s']/following-sibling::td/text()" % u"Район:")
         l.add_xpath('type', "//td[text()='%s']/following-sibling::td/text()" % u"Тип учреждения:")
         l.add_xpath('type', "//td[text()='%s']/following-sibling::td/text()" % u"Тип учреждения:")
         l.add_xpath('name', "//td[text()='%s']/following-sibling::td/text()" % u"Название:")
         l.add_xpath('name', "//td[text()='%s']/following-sibling::td/strong/text()" % u"Название:")
         l.add_xpath('post', "//td[text()='%s']/following-sibling::td/text()" % u"Почтовый адрес:")
         l.add_xpath('post', "//td[text()='%s']/following-sibling::td/text()" % u"Почтовый адрес:")
         l.add_xpath('phone', "//td[text()='%s']/following-sibling::td/text()" % u"Телефоны:")
         l.add_xpath('phone', "//td[text()='%s']/following-sibling::td/text()" % u"Телефоны:")
Строка 181: Строка 292:
         l.add_xpath('toys', "//td[text()='%s']/following-sibling::td/text()" % u"Игрушки и игры")
         l.add_xpath('toys', "//td[text()='%s']/following-sibling::td/text()" % u"Игрушки и игры")


 
        #
         l.add_xpath('patronage', "//td[text()='%s']/following-sibling::td/text()" % u"Шефство, помощь:")
         l.add_xpath('patronage', "//td[text()='%s']/following-sibling::td/text()" % u"Шефство, помощь:")
         l.add_xpath('needs', "//td[text()='%s']/following-sibling::td/text()" % u"Потребности учреждения:")
         l.add_xpath('needs', "//td[text()='%s']/following-sibling::td/text()" % u"Потребности учреждения:")
         l.add_xpath('volunteers', "//td[text()='%s']/following-sibling::td/text()" % u"Привлечение добровольцев:")
         l.add_xpath('volunteers', "//td[text()='%s']/following-sibling::td/text()" % u"Привлечение добровольцев:")


        l.add_value('federal_district', get_federal_district(hxs.select("//td[text()='%s']/following-sibling::td/text()" % u"Регион:").extract()[0]))
         l.add_value('url', response.url)
         l.add_value('url', response.url)


         return l.load_item()
         return l.load_item()
</syntaxhighlight>
</syntaxhighlight>


== Формирование выходных данных ==
== Формирование выходных данных ==
Для запуска нашей программы необходимо, находясь в каталоге ''~/projects/scrapy/orphanage'' выполнить команду вида:
<syntaxhighlight lang="bash">
scrapy crawl [имя паука]
</syntaxhighlight>
В нашем случае:
<syntaxhighlight lang="bash">
scrapy crawl detskiedomiki
</syntaxhighlight>
При таком запуске результаты парсинга будут обрабатываться методами, описанными в специальных классах, расположенных в файле ''pipelines.py''. То есть мы можем написать свой класс, который будет принимать на вход объекты класса ''OrphanageItem'' и обрабатывать их нужным образом. Однако в большинстве случаев оказывается достаточным того [https://scrapy.readthedocs.org/en/latest/topics/feed-exports.html функционала], который предоставляет Scrapy по выгрузке данных в JSON, XML или CSV. Так, чтобы результаты парсинга в нашем случае сохранились в файле формата CSV, необходимо выполнить команду:
<syntaxhighlight lang="bash">
scrapy crawl detskiedomiki -o scarped_data_utf8.csv -t csv
</syntaxhighlight>


== Результат ==
== Результат ==
Результаты парсинга доступны по адресу: http://gis-lab.info/share/DR/scarped_data_utf8.csv.7z


== Ссылки ==
== Ссылки ==
# [http://habrahabr.ru/post/115710/ Собираем данные с помощью Scrapy]
# [http://habrahabr.ru/post/115710/ Собираем данные с помощью Scrapy]
# [http://stackoverflow.com/a/9195158/813758 Scrapy text encoding]
# [http://stackoverflow.com/a/9195158/813758 Scrapy text encoding]

Текущая версия от 13:08, 13 марта 2013

Эта страница опубликована в основном списке статей сайта
по адресу http://gis-lab.info/qa/scrapy.html


Введение

Синтаксический анализ (парсинг) сайтов хоть и не имеет прямого отношения к пространственным данным, но владение основами которого полезно любому, работающему с ними. В свете роста числа онлайн-ресурсов, публикующих открытые данные, ценность умения извлекать из них необходимую информацию многократно повышается. Приведём небольшой пример. Допустим, нам необходимо составить набор тематических карт, отражающих результаты Выборов Президента Российской Федерации 2012. Исходные данные можно получить на сайте ЦИК России. Однако, согласитесь, что непосредственно использовать данные, предоставляемые в таком виде, очень сложно. Плюс это усложняется тем, что данные по разным регионам расположены на разных страницах. Гораздо удобнее было бы, чтобы вся эта информация была представлена, например, в виде одного или нескольких структурированных CSV или XML файлов (в идеале, конечно было бы иметь еще и некоторый API, позволяющий выполнять запросы к таким ресурсам), однако зачастую формирование подобных файлов отдаётся на откуп конечному пользователю (почему так происходит - это вопрос отдельный). Именно проблеме создания таких вот аггрегированных наборов данных и посвещена данная статья. В связи с недавними событиями в качестве целевого сайта, который мы будем парсить, выбран сайт ДетскиеДомики.ру, а именно его раздел Детские учреждения. Предполагается, что информация, расположенная на этом сайте будет в ближайшее время очень востребованной.

В качестве инструмента, которым будет выполняться парсинг, выбран Scrapy - очень гибкий фреймворк, написанный на Python и позволяющий решать широкий спектр задач. Информации о Scrapy на русском языке не так много, например и ещё, но этот пробел компенсируется отличной документацией.

В данной статье мы не будем подробно заострять внимание на всех технических возможностях Scrapy, а просто рассмотрим поэтапно, как была решена определённая задача. Если кто-то захочет использовать данный материал как отправную точку для решения собственной задачи, но не найдёт здесь ответов на свой вопрос - спрашивайте в форуме, постараемся помочь.

Установка Scrapy

Unix

В *nix-системах установка Scrapy - тривиальная задача:

cd ~
mkdir scrapy
cd scrapy
virtualenv --no-site-packages env
source ./env/bin/activate
pip install Scrapy

Windows

В Windows всё сложнее:

  1. Перейдите на страницу Win32 OpenSSL
  2. Скачайте и установите Visual C++ 2008 redistributables для вашей версии Windows и архитектуры
  3. Скачайте OpenSSL для вашей версии Windows и архитектуры (вам нужна обычная версия - не light)
  4. Добавьте путь c:\openssl-win32\bin (или тот куда вы установили OpenSSL) к переменной PATH
  5. Установите twisted отдельно (например для 2.7)
  6. Установите lxml отдельно (например для 2.7)
  7. если easy_install не установлен, установите сначала его самого, он ставится в C:\Python27\Scripts\ и запускается из cmd, а не из Python

далее установите:

easy_install pip
pip install Scrapy
pip install zope.interface
pip install w3lib

Создание проекта

После того, как Scrapy будет установлен, нужно создать каталог проекта. Для этого, находясь в каталоге ~/projects/scrapy, выполните команду:

scrapy startproject orphanage

В результате чего будет создана директория orphanage (соответствует имени проекта), имеющая следующую структуру:

orphanage/
    scrapy.cfg
    orphanage/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...
  • scrapy.cfg - настройки проекта;
  • orphanage/ - Python модуль проекта;
  • orphanage/items.py - классы, описывающие модель собираемых данных;
  • orphanage/pipelines.py - используется в основном для описания пользовательских форматов сохранения результатов парсинга;
  • orphanage/settings.py - пользовательские настройки паука;
  • orphanage/spiders/ - директория, в которой хранятся файлы с классами пауков. Каждого паука принято писать в отдельном файле.

Описание модели данных

Модель представляет собой отдельный класс, содержащий перечень атрибутивных полей собираемых данных. Прежде чем описывать модель, необходимо определиться во-первых с объектом парсинга, а во-вторых с набором его атрибутов, которые мы хотим извлечь из целевого ресурса. Объектом парсинга в нашем случае будут являться детские дома, а набором атрибутов - их характеристики (в случае если у каждого объекта перечень доступных атрибутов различный, то итоговым набором будет являться объединение множеств атрибутов всех объектов). Находим страницу, содержащую наиболее полный перечень атрибутов (в данном случае факт полноты был определён путём сопоставления представленных атрибутов и анкеты детского учреждения) и выписываем их: "Рег. номер", "Регион", "Район", "Тип учреждения", "Название", "Почтовый адрес" и т.д. (всего 34 атрибута). После того, как мы определились с перечнем атрибутов, отразим их в специальном классе. Для этого открываем файл items.py и описываем класс (название - произвольное):

from scrapy.item import Item, Field

class OrphanageItem(Item):
    # define the fields for your item here like:
    # name = Field()
    id               = Field()
    region           = Field()
    district         = Field()
    type             = Field()
    name             = Field()
    post             = Field()
    phone            = Field()
    director         = Field()
    bank             = Field()
    parent           = Field()
    foundation       = Field()
    activities       = Field()
    history          = Field()
    staff            = Field()
    publications     = Field()
    children         = Field()
    age              = Field()
    orphans          = Field()
    deviated         = Field()
    principle        = Field()
    education        = Field()
    treatment        = Field()
    holidays         = Field()
    communication    = Field()
    buildings        = Field()
    vehicles         = Field()
    farming          = Field()
    working_cabinet  = Field()
    library          = Field()
    computers        = Field()
    toys             = Field()
    patronage        = Field()
    needs            = Field()
    volunteers       = Field()
    url              = Field()

Как можно увидеть представленный класс содержит записи вида имя атрибута = Field(), где в качестве имя атрибута рекомендуется использовать английский вариант названия соответствующего атрибута. Кроме того, в класс был добавлен ещё один атрибут url, который предназначен для хранения URL той страницы, из которой были извлечены данные.

Создание паука

Паук - это основная часть нашей системы, представляющий собой отдельный класс, описывающий способ обхода ресурса и собирающий необходимую информацию в соответствии с описанной на предыдущем этапе моделью.

Переходим в директорию orphanage/spiders/ и создаём файл с описанием паука detskiedomiki.py (название произвольное). Внутри файла описываем класс (имя класса - произвольное):

from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.contrib.loader.processor import TakeFirst
from scrapy.contrib.loader import XPathItemLoader
from scrapy.selector import HtmlXPathSelector
from orphanage.items import OrphanageItem


class OrphanSpider(CrawlSpider):
    name = "detskiedomiki"
    allowed_domains = ["www.detskiedomiki.ru"]
    start_urls = ["http://www.detskiedomiki.ru/guide/child/"]

    rules = (
        Rule(SgmlLinkExtractor(allow=('act=home_reg', 'act=home_zone')), follow=True),
        Rule(SgmlLinkExtractor(allow=('act=home_more')), callback='parse_item'),
    )

    def parse_item(self, response):
        ...
  • name - уникальный идентификатор паука, именно он будет использоваться в качестве аргумента при запуске процесса парсинга;
  • allowed_domains - список доменов по которым может ходить паук;
  • start_urls - список URL, с которых начинается обход ресурса;
  • rules - список правил обхода ресурса. В данном случае данный список содержит 2 правила - первое будет срабатывать при попадании паука на страницы, URL которых содержит act=home_reg или act=home_zone. Под срабатыванием в данном случае подразумевается переход по ссылкам, извлеченным из этих страниц (за что отвечает аргумент follow=True). Второе правило будет срабатывать при попадании паука на страницы, URL которых содержит act=home_more (именно на этих страницах содержится информация, которую мы хотим извлечь), например http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65. В данном случае у правила не указан аргумент follow, что означает, что при попадании паука на данную страницу ссылки из неё не извлекаются, вместо этого содержимое страницы передаётся на вход функции, указанной в аргументе callback - назовём её в нашем случае parse_item.

Прежде чем переходить к рассмотрению функции parse_item, сделаем небольшое техническое отступление. Существет несколько библиотек, предназначенных для извлечения данных из HTML-документов. Наиболее распространённые это BeautifulSoup (популярная, но при этом обладающая одним недостатком - очень медленная) и lxml. В Scrapy используется собственный механизм извлечения данных (основанный так же как и lxml на libxml2) из HTML-документов - селекторы (selectors). Фактически, селекторы - это отдельные классы, при создании экземпляров которых на вход передаётся объект класса Response (представляющий собой ответ сервера). В Scrapy доступно 2 селектора - HtmlXPathSelector и XmlXPathSelector, предназначенных для парсинга HTML и XML документов соответственно.

Для того, чтобы понять, как работают селекторы, перейдите в директорию ~/projects/scrapy/orphanage и выполните команду:

scrapy shell "http://detskiedomiki.ru/?act=home_more&id=6278&z_id=3&part_id=65"

В результате выполнения данной команды будет осуществлен запрос к указанной странице, после чего вы попадете в интерактивную консоль, в которой уже будет ряд созданных Scrapy объектов, в том числе и объект hxs класса HtmlXPathSelector. Данную консоль очень удобно использовать для составления XPath выражений - основного инструмента доступа к элементам HTML-документа, используемого в Scrapy. Например, мы хотим извлечь значение атрибута Рег. номер из нашей страницы, для этого в консоли вызываем метод select объекта hxs и в качестве аргумента используем соответствующую XPath-строку:

In [10]: hxs.select("//td[text()='%s']/following-sibling::td/text()" % "Рег. номер:".decode('utf-8'))
Out[10]: [<HtmlXPathSelector xpath=u"//td[text()='\u0420\u0435\u0433. \u043d\u043e\u043c\u0435\u0440:']/following-sibling::td/text()" data=u'01-04-15-01'>]

В результате получим массив, состоящий из списка объектов класса HtmlXPathSelector, то есть метод select объекта класса HtmlXPathSelector возвращает список объектов класса HtmlXPathSelector (что очень удобно в случае парсинга вложенных данных). Для извлечения непосредственно данных необходимо применить метод extract:

In [11]: hxs.select("//td[text()='%s']/following-sibling::td/text()" % "Рег. номер:".decode('utf-8')).extract()
Out[11]: [u'01-04-15-01']

В результате мы получим список значений тех элементов, которые удовлетворяют XPath-выражению.

А теперь рассмотрим нашу функцию parse_item:

class OrphanLoader(XPathItemLoader):
    default_output_processor = TakeFirst()

def parse_item(self, response):
    hxs = HtmlXPathSelector(response)
    l = OrphanLoader(OrphanageItem(), hxs)

    #
    l.add_xpath('id', "//td[text()='%s']/following-sibling::td/text()" % u"Рег. номер:")
    ...

    l.add_value('url', response.url)

    return l.load_item()

Загрузчики (Item Loaders) - специальные классы, облегчающие заполнение объекта класса модели данных (в нашем случае OrphanageItem). В данном случае мы расширяем базовый класс загрузчика XPathItemLoader путём создания нового класса OrphanLoader и устанавливаем свойство default_output_processor в значение TakeFirst, это сделано из следующих соображений. При извлечении данных из HTML-документа результат возвращается в виде массива значений (даже если этот массив состоит из одного элемента как в нашем случае), использование процессора TakeFirst позволяет из полученного массива значений извлекать первый элемент.

Следующей строкой мы создаём объект класса OrphanLoader:

l = OrphanLoader(OrphanageItem(), hxs)

Первый аргумент - объект класса модели данных OrphanageItem, второй класса HtmlXPathSelector. Следующая (и последующие) строка вида:

l.add_xpath('id', "//td[text()='%s']/following-sibling::td/text()" % u"Рег. номер:")

говорит о том, что атрибут id объекта класса нашей модели данных будет извлекаться в соответствии с переданным выражением XPath.

l.add_value('url', response.url)

Метод add_value позволяет задать значение указанного (url) атрибута вручную. Следующая строка:

return l.load_item()

заполняет атрибуты объекта OrphanageItem в соответствии с настройками загрузчика.

Итоговый вариант файла orphanage/spiders/detskiedomiki.py:

# -*- encoding: utf-8 -*-

from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.contrib.loader.processor import TakeFirst
from scrapy.contrib.loader import XPathItemLoader
from scrapy.selector import HtmlXPathSelector
from orphanage.items import OrphanageItem

class OrphanLoader(XPathItemLoader):
    default_output_processor = TakeFirst()

class OrphanSpider(CrawlSpider):
    name = "detskiedomiki"
    allowed_domains = ["www.detskiedomiki.ru"]
    start_urls = ["http://www.detskiedomiki.ru/guide/child/"]

    rules = (
        Rule(SgmlLinkExtractor(allow=('act=home_reg', 'act=home_zone')), follow=True),
        Rule(SgmlLinkExtractor(allow=('act=home_more')), callback='parse_item'),
    )

    def parse_item(self, response):
        hxs = HtmlXPathSelector(response)
        l = OrphanLoader(OrphanageItem(), hxs)

        #
        l.add_xpath('id', "//td[text()='%s']/following-sibling::td/text()" % u"Рег. номер:")
        l.add_xpath('region', "//td[text()='%s']/following-sibling::td/text()" % u"Регион:")
        l.add_xpath('district', "//td[text()='%s']/following-sibling::td/text()" % u"Район:")
        l.add_xpath('type', "//td[text()='%s']/following-sibling::td/text()" % u"Тип учреждения:")
        l.add_xpath('name', "//td[text()='%s']/following-sibling::td/strong/text()" % u"Название:")
        l.add_xpath('post', "//td[text()='%s']/following-sibling::td/text()" % u"Почтовый адрес:")
        l.add_xpath('phone', "//td[text()='%s']/following-sibling::td/text()" % u"Телефоны:")
        l.add_xpath('director', "//td[text()='%s']/following-sibling::td/text()" % u"Руководство:")
        l.add_xpath('bank', "//td[text()='%s']/following-sibling::td/text()" % u"Банковские реквизиты:")
        l.add_xpath('parent', "//td[text()='%s']/following-sibling::td/text()" % u"Вышестоящая организация:")

        # 
        l.add_xpath('foundation', "//td[text()='%s']/following-sibling::td/text()" % u"Дата основания:")
        l.add_xpath('activities', "//td[text()='%s']/following-sibling::td/text()" % u"Направления деятельности:")
        l.add_xpath('history', "//td[text()='%s']/following-sibling::td/text()" % u"История:")
        l.add_xpath('staff', "//td[text()='%s']/following-sibling::td/text()" % u"Персонал:")
        l.add_xpath('publications', "//td[text()='%s']/following-sibling::td/text()" % u"Публикации в СМИ:")

        #
        l.add_xpath('children', "//td[text()='%s']/following-sibling::td/text()" % u"Количество детей в учреждении:")
        l.add_xpath('age', "//td[text()='%s']/following-sibling::td/text()" % u"Возраст детей:")
        l.add_xpath('orphans', "//td[text()='%s']/following-sibling::td/text()" % u"Количество детей-сирот:")
        l.add_xpath('deviated', "//td[text()='%s']/following-sibling::td/text()" % u"Количество детей с отклонениями в развитии:")
        l.add_xpath('principle', "//td[text()='%s']/following-sibling::td/text()" % u"Принцип формирования группы:")
        l.add_xpath('education', "//td[text()='%s']/following-sibling::td/text()" % u"Обучение детей:")
        l.add_xpath('treatment', "//td[text()='%s']/following-sibling::td/text()" % u"Лечение детей:")
        l.add_xpath('holidays', "//td[text()='%s']/following-sibling::td/text()" % u"Летний отдых:")
        l.add_xpath('communication', "//td[text()='%s']/following-sibling::td/text()" % u"Общение детей:")

        #
        l.add_xpath('buildings', "//td[text()='%s']/following-sibling::td/text()" % u"Здания:")
        l.add_xpath('vehicles', "//td[text()='%s']/following-sibling::td/text()" % u"Автотранспорт:")
        l.add_xpath('farming', "//td[text()='%s']/following-sibling::td/text()" % u"Подсобное хозяйство:")
        l.add_xpath('working_cabinet', "//td[text()='%s']/following-sibling::td/text()" % u"Кабинеты труда:")
        l.add_xpath('library', "//td[text()='%s']/following-sibling::td/text()" % u"Библиотека:")
        l.add_xpath('computers', "//td[text()='%s']/following-sibling::td/text()" % u"Компьютеры:")
        l.add_xpath('toys', "//td[text()='%s']/following-sibling::td/text()" % u"Игрушки и игры")

        #
        l.add_xpath('patronage', "//td[text()='%s']/following-sibling::td/text()" % u"Шефство, помощь:")
        l.add_xpath('needs', "//td[text()='%s']/following-sibling::td/text()" % u"Потребности учреждения:")
        l.add_xpath('volunteers', "//td[text()='%s']/following-sibling::td/text()" % u"Привлечение добровольцев:")

        l.add_value('url', response.url)

        return l.load_item()


Формирование выходных данных

Для запуска нашей программы необходимо, находясь в каталоге ~/projects/scrapy/orphanage выполнить команду вида:

scrapy crawl [имя паука]

В нашем случае:

scrapy crawl detskiedomiki

При таком запуске результаты парсинга будут обрабатываться методами, описанными в специальных классах, расположенных в файле pipelines.py. То есть мы можем написать свой класс, который будет принимать на вход объекты класса OrphanageItem и обрабатывать их нужным образом. Однако в большинстве случаев оказывается достаточным того функционала, который предоставляет Scrapy по выгрузке данных в JSON, XML или CSV. Так, чтобы результаты парсинга в нашем случае сохранились в файле формата CSV, необходимо выполнить команду:

scrapy crawl detskiedomiki -o scarped_data_utf8.csv -t csv

Результат

Результаты парсинга доступны по адресу: http://gis-lab.info/share/DR/scarped_data_utf8.csv.7z

Ссылки

  1. Собираем данные с помощью Scrapy
  2. Scrapy text encoding