В предишната статия разгледахме основите на уеб скрапинга с Python и aiohttp за асинхронно извличане на данни от уеб страници. Днес ще се гмурнем по-дълбоко и ще разгледаме как да изградим по-мащабируеми и ефективни краулъри с помощта на Scrapy – мощна и гъвкава Python рамка за уеб скрапинг.

Какво е Scrapy?

Scrapy е open-source framework за извличане на структурирани данни от уебсайтове. Той предоставя пълен набор от инструменти за разработване и изпълнение на краулъри (web spiders), които могат систематично да обхождат уебсайтове и да извличат необходимата информация. Някои от основните предимства на Scrapy са:

  1. Вградена поддръжка за асинхронни заявки и конкурентно извличане на данни;
  2. Гъвкава архитектура, базирана на spider класове и item pipeline;
  3. Вградена поддръжка за селектори на данни като CSS и XPath;
  4. Вградени middlewares за често срещани задачи като обработка на cookies, сесии, HTTP аутентикация и др.;
  5. Удобен интерактивен конзолен инструмент (Scrapy shell) за тестване и отстраняване на грешки;
  6. Лесна интеграция с външни инструменти и бази данни.

Архитектура на 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 дефинира три ключови атрибута:

  1. name: Идентифицира уникално този spider. Използва се за стартиране на краулъра от командния ред;
  2. start_urls: Списък с URL адреси, от които да започне краулърът;
  3. 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).

Ето някои полезни ресурси за допълнително четене:

Заключение

В тази статия научихме основите на уеб скрапинга с Python и Scrapy. Видяхме как да изградим краулър за извличане на структурирани данни от уебсайтове, как да навигираме през страници и да следваме връзки, как да извличаме данни с помощта на CSS и XPath селектори и как да обработваме извлечените данни с помощта на Items и Item Pipelines.

Scrapy е изключително мощен и гъвкав инструмент, който превръща сложните задачи за уеб скрапинг в лесна и приятна работа. С богатия набор от вградени функции и възможности за разширяване, Scrapy е идеален избор както за малки еднократни проекти, така и за мащабни краулъри на ниво продукция.

Въпреки това, важно е винаги да помним етичните и правните съображения при скрапинг на уебсайтове. Уверете се, че спазвате условията за ползване, зачитате файловете robots.txt и не претоварвате сървърите с прекалено агресивни заявки. Добра практика е също така да се идентифицирате с подходящ user agent и да предоставяте начини за контакт в случай на проблеми.

С правилния подход и инструментариум, уеб скрапингът отваря безкрайни възможности за събиране на ценни данни и извличане на прозрения от интернет. Надяваме се, че тази статия ви е дала солидна основа, от която да започнете вашето приключение със Scrapy и Python за уеб скрапинг.

Категории:

Уеб скрапинг,

Последно обновяване: май 3, 2024