В этом материале кратко расскажу о библиотеке scrapy (https://scrapy.org/). Библиотека имеет очень много возможностей, я разберу только основные, которых, впрочем, будет достаточно для базового парсинга данных.
Для примера будем парсить небольшой объем данных. А поскольку скоро лето, то и данные будут летними - каталог всех зарегистрированных пляжей России.
Эти данные находятся в открытом доступе по адресу http://www.classification-tourism.ru/index.php/displayBeach/index
После установки scrapy с помощью pip становится доступна утилита командной строки scrapy
. Вообще взаимодействие со scrapy происходит из командой строки, из ноутбука запускать классы-парсеры неудобно, поэтому будем работать, как работали с vw.
Перед тем, как начинать парсинг, необходимо создать scrapy проект. Это делается командой scrapy startproject <имя-проекта>
. scrapy создаст каталог <имя-проекта> и в нем расположит заготовки для необходимых файлов. В частности, там будут файл настроек settings.py, директория с обработчиками html-страниц (они в терминах scrapy называются спайдерами), файл дополнительной обработки спаршенных данных pipelines.py
Этот ноутбук фактически находится в директории scrapy проекта scrapy_tourism.
!ls scrapy_tourism/*.py
scrapy_tourism/__init__.py scrapy_tourism/settings.py scrapy_tourism/pipelines.py
!ls scrapy_tourism/spiders/*.py
scrapy_tourism/spiders/beach_spider_0.py scrapy_tourism/spiders/beach_spider_1.py scrapy_tourism/spiders/beach_spider_2.py scrapy_tourism/spiders/__init__.py
Для начала спарсим названия пляжей с одной страницы http://www.classification-tourism.ru/index.php/displayBeach/index. Создадим файл с название beach_spider_0.py
в директории spiders/
и опишем там спайдер. Спайдеру необходимы имя name
; урл, с которго начинать парсинг start_urls
; и метод обработки скачанного html документа parse(self, response)
. Метод parse
должен возвращать с помощью yield готовые спаршенные объекты-словари, в нашем случае словари из одного элемента с ключом title.
!cat scrapy_tourism/spiders/beach_spider_0.py
import scrapy class TourismBeachSpider0(scrapy.Spider): name = "beach_0" start_urls = [ 'http://www.classification-tourism.ru/index.php/displayBeach/index', ] def parse(self, response): for obj in response.css('a.field.object-title'): yield { 'title': obj.css('::text').extract_first() }
Парсинг документа происходит с помощью css-селекторов, про них подробней можно почитать, например, в документации scrapy, по сути это способ обращаться к объектам в html-документе через их названия и классы.
В данном случае мы находим все объекты тэга <a>
с классом field object-title
и достаем из них текстовое содержимое.
Чтобы понять, из каких объектов доставать необходимые данные, нужно посмотреть исходный код страницы.
Для запуска парсинга нужно использовать утилиту scrapy:
# !scrapy crawl beach_0 --loglevel ERROR -o result_0.csv --output-format csv
Здесь beach_0
- это имя запускаемого спайдера (TourismBeachSpider0.name
), -o result_0.csv
- имя выходного файла, --output-format csv
- формат выходного файла, --loglevel ERROR
- уровень логирования (здесь повыше, чтобы не спамить логами).
import pandas as pd
result_beach_0 = pd.read_csv('result_0_ref.csv')
print(result_beach_0.shape)
result_beach_0
(10, 1)
title | |
---|---|
0 | Пляж базы отдыха "Искра" ПАО "Межрегиональная ... |
1 | Пляж ФГБУ "Дом отдыха "Туапсе" Управления дела... |
2 | Пляж детского оздоровительного лагеря «Альбатр... |
3 | Лечебный пляж закрытого акционерного общества ... |
4 | Пляж «Бархатные сезоны» непубличного акционерн... |
5 | Пляж Открытого акционерного общества «Санатори... |
6 | Пляж пансионата «Бургас» АО «Пансионат «Бургас» |
7 | Пляж «Ривьера Санрайз» (Riviera Sunrise Resort... |
8 | пляж "Оздоровительного комплекса "Орбита" АО "... |
9 | Пляж общества с ограниченной ответственностью ... |
Теперь скачаем название не только с первой страницы списка, а со всех.
Для этого нам потребуется переходить на новые страницы и парсить их тем же методом parse
.
Если в parse
вернуть не dict
, а объект scrapy.Request
, то именно это и произойдет - указанный урл скачается и отправится на обработку.
!cat scrapy_tourism/spiders/beach_spider_1.py
import scrapy class TourismBeachSpider1(scrapy.Spider): name = "beach_1" start_urls = [ 'http://www.classification-tourism.ru/index.php/displayBeach/index', ] def parse(self, response): for obj in response.css('a.field.object-title'): yield { 'title': obj.css('::text').extract_first() } next_page_href = response.css('li.next a::attr("href")').extract_first() if next_page_href is not None: next_page_url = response.urljoin(next_page_href) yield scrapy.Request(next_page_url, self.parse)
Здесь урлы следующих страниц достаются из объекта атрибута href объекта li.next.
Второй аргумент конструктора scrapy.Request
- это метод, которым будет парситься скачанный документ,
в нашем случае тот же parse
.
# !scrapy crawl beach_1 --loglevel ERROR -o result_1.csv --output-format csv
result_beach_1 = pd.read_csv('result_1_ref.csv')
print(result_beach_1.shape)
result_beach_1.head()
(77, 1)
title | |
---|---|
0 | Пляж базы отдыха "Искра" ПАО "Межрегиональная ... |
1 | Пляж ФГБУ "Дом отдыха "Туапсе" Управления дела... |
2 | Пляж детского оздоровительного лагеря «Альбатр... |
3 | Лечебный пляж закрытого акционерного общества ... |
4 | Пляж «Бархатные сезоны» непубличного акционерн... |
Мы парсили только названия пляжей, теперь давайте спарсим дополнительную информацию: например, адрес и категорию. Для этого будем переходить на страницу пляжа (например) и доставать оттуда необходимые данные. Конкретно, эти данные можно было бы спарсить и со страницы-списка, но для целей тьюториала представим, что их там нет.
!cat scrapy_tourism/spiders/beach_spider_2.py
import scrapy class TourismBeachSpider2(scrapy.Spider): name = "beach_2" start_urls = [ 'http://www.classification-tourism.ru/index.php/displayBeach/index', ] def parse_item(self, response): fields = {} for obj in response.css('div.detail-field'): field_name = obj.css('span.detail-label::text').extract_first() field_value = obj.css('span.detail-value::text').extract_first() fields[field_name] = field_value yield { 'reg_id': fields['Регистрационный номер в Федеральном перечне:'], 'full_name': fields['Полное наименование классифицированного объекта:'], 'name': fields['Cокращенное наименование классифицированного объекта:'], 'category': fields['Присвоенная категория:'], 'address': fields['Адрес:'], } def parse(self, response): for obj in response.css('a.field.object-title'): item_href = obj.css('::attr("href")').extract_first() yield scrapy.Request(response.urljoin(item_href), self.parse_item) next_page_href = response.css('li.next a::attr("href")').extract_first() if next_page_href is not None: next_page_url = response.urljoin(next_page_href) yield scrapy.Request(next_page_url, self.parse)
Здесь мы достаем ссылки на страницы пляжей через атрибут href
объектов a.field.object-title
, а сами страницы парсим
с помощью уже другого метода parse_item
, который передается как колбэк в scrapy.Request
.
# !scrapy crawl beach_2 --loglevel ERROR -o result_2.csv --output-format csv
result_beach_2 = pd.read_csv('result_2_ref.csv')
print(result_beach_2.shape)
result_beach_2.head()
(77, 5)
reg_id | full_name | name | category | address | |
---|---|---|---|---|---|
0 | 330000087 | Пляж общества с ограниченной ответственностью ... | Пляж ООО Санаторий «Мечта» | желтый флаг (3 категория) | 353456, Краснодарский край, г. Анапа, Пионерск... |
1 | 330000089 | пляж "Оздоровительного комплекса "Орбита" АО "... | пляж "Оздоровительного комплекса "Орбита" | зеленый флаг (2 категория) | 352840,Туапсинский район, с.Ольгинка, оздорови... |
2 | 330000091 | Пляж «Ривьера Санрайз» (Riviera Sunrise Resort... | Пляж «Ривьера Санрайз» (Riviera Sunrise Resort... | синий флаг (1 категория) | 298500, Республика Крым, г. Алушта, ул. Ленина, 2 |
3 | 330000095 | Пляж пансионата «Бургас» АО «Пансионат «Бургас» | Пляж пансионата «Бургас» | зеленый флаг (2 категория) | 354364, Краснодарский край, г. Сочи, ул. Ленин... |
4 | 330000096 | Пляж Открытого акционерного общества «Санатори... | Пляж ОАО «Санаторий «Южное взморье» | синий флаг (1 категория) | 354340, Краснодарский край, г. Сочи, ул. Калин... |
Мы спарсили каталог пляжей с адресами. Допустим, мы хотим адреса геокодировать, то есть преобразовывать их в координаты. Для этого хорошо подойдет механизм пайплайнов из scrapy - каждый спаршенный объект дополнительно обрабатывается произвольным классом, в нашем случае геокодировщиком.
!cat scrapy_tourism/pipelines.py
import urllib.request import requests class GeocoderPipeline(object): def process_item(self, item, spider): address = item['address'] print(address) geocode_url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode={0}'.format( urllib.request.quote(address)) response = requests.get(geocode_url) lat, lon = None, None if response.status_code == 200: data = response.json() geocoder_objects = data['response']['GeoObjectCollection']['featureMember'] if geocoder_objects: coordinates = geocoder_objects[0]['GeoObject']['Point']['pos'].split() lat, lon = float(coordinates[1]), float(coordinates[0]) item['lat'] = lat item['lon'] = lon return item
Здесь мы помощью геокодировщика Яндекса получаем из адреса координаты и сохраняем их в спаршенный объект
Для того, чтобы пайплайн заработал, его необходимо добавить в settings.py
:
ITEM_PIPELINES
'scrapy_tourism.pipelines.GeocoderPipeline': 300,
}```
# !scrapy crawl beach_2 --loglevel ERROR -o result_3.csv --output-format csv
result_beach_3 = pd.read_csv('result_3_ref.csv')
print(result_beach_3.shape)
result_beach_3.head()
(77, 7)
reg_id | full_name | name | category | address | lat | lon | |
---|---|---|---|---|---|---|---|
0 | 330000087 | Пляж общества с ограниченной ответственностью ... | Пляж ООО Санаторий «Мечта» | желтый флаг (3 категория) | 353456, Краснодарский край, г. Анапа, Пионерск... | 44.934010 | 37.312253 |
1 | 330000089 | пляж "Оздоровительного комплекса "Орбита" АО "... | пляж "Оздоровительного комплекса "Орбита" | зеленый флаг (2 категория) | 352840,Туапсинский район, с.Ольгинка, оздорови... | 44.201336 | 38.889165 |
2 | 330000091 | Пляж «Ривьера Санрайз» (Riviera Sunrise Resort... | Пляж «Ривьера Санрайз» (Riviera Sunrise Resort... | синий флаг (1 категория) | 298500, Республика Крым, г. Алушта, ул. Ленина, 2 | 44.668763 | 34.412258 |
3 | 330000095 | Пляж пансионата «Бургас» АО «Пансионат «Бургас» | Пляж пансионата «Бургас» | зеленый флаг (2 категория) | 354364, Краснодарский край, г. Сочи, ул. Ленин... | 43.489340 | 39.888028 |
4 | 330000096 | Пляж Открытого акционерного общества «Санатори... | Пляж ОАО «Санаторий «Южное взморье» | синий флаг (1 категория) | 354340, Краснодарский край, г. Сочи, ул. Калин... | 43.430094 | 39.914600 |
Отобразим полученные данные на карте. Для этого нам потребуется библиотека folium. Сделаем красиво: будем ставить маркеры цвета, соответствующего категории пляжа.
beach_df = result_beach_3.copy()
beach_df.category.value_counts()
синий флаг (1 категория) 37 желтый флаг (3 категория) 25 зеленый флаг (2 категория) 15 Name: category, dtype: int64
COLOR_DICT = {
'синий флаг (1 категория)': 'blue',
'зеленый флаг (2 категория)': 'green',
'желтый флаг (3 категория)': 'beige',
}
beach_df['color'] = beach_df['category'].apply(lambda c: COLOR_DICT[c])
import folium
SOCHI = [43.585525, 39.723062]
m = folium.Map(location=SOCHI, tiles='Stamen Terrain', zoom_start=11)
for i in range(len(beach_df)):
folium.Marker(beach_df.loc[i, ['lat', 'lon']].values,
popup=beach_df.loc[i, 'name'],
icon=folium.Icon(color=beach_df.loc[i, 'color'], icon='')) \
.add_to(m)
m
Мы рассмотрели пример, как с помощью scrapy можно спарсить данные с сайта. Надеюсь, тьюториал оказался полезным. Всем хорошего лета!