В предишната статия разгледахме основите на уеб скрапинга с Python и aiohttp за асинхронно извличане на данни от уеб страници. Днес ще се гмурнем по-дълбоко и ще разгледаме как да изградим по-мащабируеми и ефективни краулъри с помощта на Scrapy – мощна и гъвкава Python рамка за уеб скрапинг.
Какво е Scrapy?
Scrapy е open-source framework за извличане на структурирани данни от уебсайтове. Той предоставя пълен набор от инструменти за разработване и изпълнение на краулъри (web spiders), които могат систематично да обхождат уебсайтове и да извличат необходимата информация. Някои от основните предимства на Scrapy са:
- Вградена поддръжка за асинхронни заявки и конкурентно извличане на данни;
- Гъвкава архитектура, базирана на spider класове и item pipeline;
- Вградена поддръжка за селектори на данни като CSS и XPath;
- Вградени middlewares за често срещани задачи като обработка на cookies, сесии, HTTP аутентикация и др.;
- Удобен интерактивен конзолен инструмент (Scrapy shell) за тестване и отстраняване на грешки;
- Лесна интеграция с външни инструменти и бази данни.
Архитектура на Scrapy
Преди да преминем към писане на код, нека бързо разгледаме основните компоненти в архитектурата на Scrapy:
- Spiders: Класове, които дефинират как да се обходи даден сайт и как да се извлекат данните от него. Всеки spider отговаря за обхождането на определен домейн или набор от URL адреси.
- Items: Контейнери за извлечените данни, дефинирани като класове на Python. Те осигуряват последователна структура на извлечените данни и могат да се използват за валидиране.
- Item Pipeline: Компоненти, които обработват извлечените данни (Items), след като са били извлечени от spider. Pipeline-ите могат да се използват за почистване, валидиране, дублиране и съхраняване на данни.
- Downloaders: Компоненти, отговорни за изтеглянето на уеб страници и подаването им на spiders за обработка.
- Schedulers: Компоненти, които решават кои заявки за изтегляне да се обработят след това.
Тези компоненти работят заедно, за да образуват тръбопровод за обработка на данни, започвайки от заявка за сваляне, преминавайки през извличане на данни от spider и завършвайки с обработка на елемент в конвейера.
Инсталиране и настройка на Scrapy проект
Нека започнем с инсталирането на Scrapy чрез pip:
# Install scrapy
pip install scrapy
След това създаваме нов Scrapy проект със scrapy CLI:
# Use Scrapy CLI to start new project
scrapy startproject myspider
Това ще генерира следната структура на проекта:
myspider/
scrapy.cfg # Config file
myspider/ # Python package
__init__.py
items.py # Elements
middlewares.py # Middlewares
pipelines.py # Definition of pipelines
settings.py # Different configurations
spiders/ # Dir for spiders
__init__.py
След това можем да дефинираме нов spider клас като подклас на scrapy.Spider
в модула myspider/spiders/
:
import scrapy
class MySpider(scrapy.Spider):
name = 'myspider'
start_urls = ['https://pro-soft.bg']
def parse(self, response):
# Get the response
title = response.css('h1::text').get()
yield {'title': title}
Този прост spider дефинира три ключови атрибута:
name
: Идентифицира уникално този spider. Използва се за стартиране на краулъра от командния ред;start_urls
: Списък с URL адреси, от които да започне краулърът;parse
: Метод, който се извиква за обработка на всеки изтеглен отговор.
В метода parse
, извличаме заглавието на страницата (<h1>
tag), използвайки CSS селектор, и го връщаме като речник (който автоматично се превръща в Item).
За да стартираме нашия spider, изпълняваме следната команда в основната директория на проекта:
# Start the spider
scrapy crawl myspider
Това ще изпрати заявка към всеки от start_urls
, ще предаде всеки отговор на метода parse
и ще изведе извлечените данни в конзолата.
Обхождане на сайтове и следване на връзки
Нашият базов пример извлича данни само от единична страница. В повечето случаи обаче, бихме искали краулърът да следва връзки и да обхожда няколко (или всички) страници от уеб сайта.
За да направим това, можем да използваме метода response.follow()
за генериране на нови заявки от извлечените URL адреси:
import scrapy
class MySpider(scrapy.Spider):
name = 'myspider'
start_urls = ['http://example.com']
def parse(self, response):
for href in response.css('a::attr(href)'):
yield response.follow(href, self.parse_item)
def parse_item(self, response):
yield {
'title': response.css('h1::text').get(),
'content': response.css('p::text').getall(),
'url': response.url
}
Тук извличаме всички href
атрибути от елементите <a>
и извикваме response.follow
за всеки един от тях. Това генерира нови заявки към извлечените URL адреси, като предава отговорите към метода parse_item
. Параметърът self.parse_item
указва обратния метод (callback
), който да се използва за обработка на генерираните заявки.
В parse_item
извличаме заглавието, съдържанието и URL адреса на всяка посетена страница и ги връщаме като речник.
С това имаме рекурсивен краулър, който започва от start_urls
, следва всички открити връзки и извлича данни от посетените страници.
Извличане на данни със CSS и XPath селектори
Scrapy поддържа два мощни метода за извличане на данни от HTML и XML отговори – CSS селектори (чрез response.css()
) и XPath изрази (чрез response.xpath()
).
CSS селекторите са удобен начин за избиране на елементи чрез подбор по HTML тагове, класове, идентификатори и атрибути. Например:
'h1::text'
: Избира текстовото съдържание на всички елементи<h1>
.'div.item'
: Избира всички елементи<div>
с класitem
.'a[href^="http"]::attr(href)'
: Избира стойностите на атрибутаhref
на всички елементи<a>
, чиитоhref
започва сhttp
.
От друга страна, XPath е по-изразителен език за заявки, който позволява избор на елементи въз основа на тяхната позиция в дървото на документа, условия и много други критерии. Например:
'//h1/text()'
: Избира текстовото съдържание на всички елементи<h1>
.'//div[@class="item"]'
: Избира всички елементи<div>
с атрибутclass="item"
.'//a[starts-with(@href, "http")]/@href'
: Избира стойностите на атрибутаhref
на всички елементи<a>
, чиитоhref
започва сhttp
.
Scrapy също така предоставя множество помощни методи за извличане на данни от избраните елементи, като get()
, getall()
, re()
и re_first()
.
Тук е даден по-сложен пример за извличане на данни чрез комбинация от CSS и XPath селектори:
def parse_item(self, response):
yield {
'title': response.css('h1::text').get(),
'description': response.xpath('//div[@id="description"]/text()').get(),
'price': response.css('span.price::text').re_first(r'£(\d+\.\d+)'),
'images': response.css('img.product-image::attr(src)').getall(),
'categories': response.xpath('//a[@class="category-link"]/text()').getall()
}
Кратко обяснение:
'title'
: Извлича първия (и единствен) елемент<h1>
и връща неговото текстово съдържание.'description'
: Извлича текстовото съдържание на първия елемент<div>
сid="description"
.'price'
: Извлича текстовото съдържание на първия елемент<span>
с класprice
, прилага регулярен израз за извличане на цената и връща първото съвпадение (цената).'images'
: Извлича стойностите на атрибутаsrc
на всички елементи<img>
с класproduct-image
.'categories'
: Извлича текстовото съдържание на всички елементи<a>
с класcategory-link
.
След като сме извлекли необходимите данни, следващата стъпка е да ги структурираме, валидираме и съхраним за по-нататъшна обработка. Scrapy предлага два основни начина за това – Items и Item Pipeline.
Items са прости Python класове, които дефинират полетата за съхранение на извлечените данни. Те осигуряват последователна структура и позволяват валидиране на типовете и стойностите. Дефинираме ги в модула items.py
:
import scrapy
class ProductItem(scrapy.Item):
title = scrapy.Field()
description = scrapy.Field()
price = scrapy.Field()
images = scrapy.Field()
categories = scrapy.Field()
След това можем да използваме този клас Item в нашия spider за структуриране на извлечените данни:
from myspider.items import ProductItem
def parse_item(self, response):
item = ProductItem()
item['title'] = response.css('h1::text').get()
item['description'] = response.xpath('//div[@id="description"]/text()').get()
item['price'] = response.css('span.price::text').re_first(r'£(\d+\.\d+)')
item['images'] = response.css('img.product-image::attr(src)').getall()
item['categories'] = response.xpath('//a[@class="category-link"]/text()').getall()
yield item
Елементите, върнати от спайдър, се изпращат последователно през тръби (pipelines) за допълнителна обработка (дефинирани в pipelines.py
). Pipeline-ите се използват за задачи като почистване на данни, валидиране, филтриране на дубликати и съхранение в бази данни. Например:
import json
class JsonWriterPipeline:
def open_spider(self, spider):
self.file = open('items.jl', 'w')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
line = json.dumps(dict(item)) + "\n"
self.file.write(line)
return item
Този pipeline просто записва всички извлечени елементи като JSON Lines във файл items.jl
.
За да активираме тръбопровода, трябва да го добавим към ITEM_PIPELINES
настройката в settings.py
:
ITEM_PIPELINES = {
'myspider.pipelines.JsonWriterPipeline': 300,
}
Номерът (300) указва реда на изпълнение на pipeline-ите (по-ниските стойности се изпълняват първо).
Сега, когато стартираме спайдър, извлечените елементи ще преминават през нашия тръбопровод и ще бъдат записани във файл.
Допълнителни теми и ресурси
Това, което покрихме дотук, е само повърхността на възможностите на Scrapy. Други важни теми и функции, които да проучите, включват:
- Обработване на формуляри и изпращане на POST заявки;
- Работа със сесии и аутентикация (вградени в Request обекти);
- Имитиране на потребителски агенти и мидълуер за ротиране на IP;
- Обработка на изображения (ImagesPipeline);
- Интеграция с бази данни и експортиране на различни формати;
- Разгръщане и управление на стабилни краулъри (Scrapyd, AutoThrottle).
Ето някои полезни ресурси за допълнително четене:
- Официална документация на Scrapy: https://docs.scrapy.org/
- Scrapy Tutorial: https://docs.scrapy.org/en/latest/intro/tutorial.html
- Примери за проекти със Scrapy: https://github.com/geekan/scrapy-examples
- Scrapy Cloud (платформа за хостинг на краулъри): https://www.zyte.com/scrapy-cloud/
Заключение
В тази статия научихме основите на уеб скрапинга с Python и Scrapy. Видяхме как да изградим краулър за извличане на структурирани данни от уебсайтове, как да навигираме през страници и да следваме връзки, как да извличаме данни с помощта на CSS и XPath селектори и как да обработваме извлечените данни с помощта на Items и Item Pipelines.
Scrapy е изключително мощен и гъвкав инструмент, който превръща сложните задачи за уеб скрапинг в лесна и приятна работа. С богатия набор от вградени функции и възможности за разширяване, Scrapy е идеален избор както за малки еднократни проекти, така и за мащабни краулъри на ниво продукция.
Въпреки това, важно е винаги да помним етичните и правните съображения при скрапинг на уебсайтове. Уверете се, че спазвате условията за ползване, зачитате файловете robots.txt и не претоварвате сървърите с прекалено агресивни заявки. Добра практика е също така да се идентифицирате с подходящ user agent и да предоставяте начини за контакт в случай на проблеми.
С правилния подход и инструментариум, уеб скрапингът отваря безкрайни възможности за събиране на ценни данни и извличане на прозрения от интернет. Надяваме се, че тази статия ви е дала солидна основа, от която да започнете вашето приключение със Scrapy и Python за уеб скрапинг.