web scraping

Web scraping w Scrapy

Web Scraping w Scrapy

Scrapy to najpopularniejsze narzędzie do web scrapingu i crawlowania stron internetowych. Na czym polega fenomen Scrapy i jak z niego korzystać?

Czym jest scrapy?

Scrapy to największy i najpopularniejszy framework służący do scrapowania danych.

Dzięki wbudowanym mechanizmom pozwala w szybki sposób stworzyć wydajne i skalowalne scrapery-crawlery.

Scrapy vs requests i BeautifulSoup

Na moim blogu możesz przeczytać o web scrapingu w Pythonie z wykorzystaniem bibliotek requests oraz BeautifulSoup. Scrapy jest typowym frameworkiem, który możesz zastosować jako alternatywę bibliotek.

Jeżeli chcesz poznać różnice między biblioteką a frameworkiem zapraszam Cię do przeczytania wpisu framework vs biblioteka.

Biblioteka Requests pozwala na wysyłanie żądań HTTP do stron internetowych i pobieranie ich treści, a BeautifulSoup dostarcza zestaw narzędzi do przetwarzania pobranego kodu HTML. Scrapy łączy te i wiele innych funkcji.

Struktura projektu Scrapy

Aby utworzyć bazowy projekt Scrapy użyj polecenia:

scrapy startproject blog_scraper

Utworzony projekt posiada strukturę:

blog_scraper/
    blog_scraper/
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            [...]

Architektura Scrapy

Architektura Scrapy
Źródło: https://docs.scrapy.org/en/latest/topics/architecture.html

Analizując diagram architektury Scrapyego widzimy, że jej głównym elementem jest kontoler zwany silnikiem (Engine), które steruje czterema komponentami wykonawczymi:

  • Spiders
  • ItemPipelines
  • Downloader
  • Scheduler

Cały proces scrapowania rozpoczyna się od przekazania obiektu Request do komponentu Spiders. Następnie obiekt żądania przekazywany jest do Schedulera (Planera), aby zapisać go w kolejce.

Silnik przekazuje obiekt żądania do modułu Downloader, który pobiera kod strony i zamienia go w obiekt Response. Obiekt odpowiedzi jest dalej przekazywany do modułu Spider, a następnie ItemPipelines.

Na koniec sinlik wysyła zapytanie do Schedulera, aby pobrać następny link do zescrapowania. Jeżeli jest następny link to proces się powtarza.

Scrapy Spider

Najważniejszą częścią projektu Scrapy są klasy Spider. Są to podstawowe klasy, które definiują scrapera-crawlera.

W klasie Spider definiujemy sposób zachowania „pająka” przechodzącego po stronie internetowej.

Nazwa klasy spider

Każda klasa Spider identyfikowana jest za pomocą nazwy określonej w zmiennej name.

Dobrą praktyką jest nazywanie spidera nazwą strony internetowej, z której pobiera dane. W taki sposób nazywamy również plik.

Chcąc stworzyć scrapera na bloga, który właśnie czytasz plik nazwiemy go kamil_kwapisz.py.

Startowe adresy URL

Klasa Spider pozwala definiować startowe adresy URL, od których domyślnie nasz crawler zacznie przechodzić strony internetowe. Definiuje się je jako lista w zmiennej start_urls .

start_urls = ['https://kamil.kwapisz.pl']

Metoda start_requests

Metodę start_requests możemy potraktować jako alternatywę zmiennej start_urls. Wewnątrz metody powinniśmy zwrócić obiekt scrapy.Request, który prowadzi do początkowej strony.

class Blog(scrapy.Spider):
    name = 'blog'

    def start_requests(self):
        return scrapy.Request("https://kamil.kwapisz.pl", meta={'date': "12.10.2020"}, callback=self.parse_article)

Wskazując początek crawlowania w ten sposób mamy jednak dodatkowe możliwości. Możemy przekazać w ten sposób metadane czy wskazać callback.

Callback – funkcja zwrotna

Callback to funkcja, która ma zostać wywołana na zakończenie wywoływanej funkcji.

W powyższym przykładzie możemy rozumieć to tak, że callback (tj. funkcja parse) zostanie wywowałana, gdy zakończy się wykonywanie żądania (requesta), tzn. otrzymamy odpowiedź serwera.

Funkcja parse

W przypadku scrapy funkcją callback często będzie funkcja parse. Wewnątrz tej funkcji definiujemy sposób ekstrakcji danych ze strony internetowej.

Scrapy Items

W przypadku Scrapy pobierane dane nazywamy przedmiotami (Items). Itemem nazwiemy więc każdy pobrany rekord.

Pobierając artykuły z bloga Item określał będzie jakie dane z artykułu mają zostać pobrane:

from scrapy.item import Item, Field


class ArticleItem(Item):
    title = Field()
    date = Field()
    author = Field()
    tags = Field()
    category = Field()
    text = Field()
    length = Field()

Tak zdefiniowany Item wykorzystamy potem do ekstrakcji danych.

Item pipeline

Crawlery napisane w scrapy po wejściu na stronę powinny zwrócić item z pobranymi danymi.

W tym momencie w grę wchodzi tzw. Item pipeline. Jest to etap, w trakcie którego mamy dostęp do pobranego itemu (pojedynczego rekordu).

ItemPipeline jest miejscem, w którym możemy dokonać walidacji danych, ich czyszczenia, czy nawet zapisu do bazy danych.

Napiszmy ItemPipeline, który zliczy znaki w tekście artykułu i zapisze ich liczbę:

# pipelines.py
from scrapy.exceptions import DropItem


class CountLengthItemPipeline:

    def open_spider(self, spider: scrapy.Spider):
        pass

    def close_spider(self, spider: scrapy.Spider):
        pass

    def process_item(self, item, spider: scrapy.Spider):
        article_text = article.get('text', '')
        text_length = len(article_text)
        if text_length > 0:
            item['length'] = text_length
        else:
            raise DropItem(f"Brak tekstu w artykule {item['title']}")

Klasy ItemPipeline musimy również zdefiniować w pliku settings.py:

ITEM_PIPELINES = {
    'blog_scraper.pipelines.CountLengthItemPipeline': 300,
}

Klasom przypisujemy wartości w zakresie 100-1000, które definiują kolejność wykonywania. Im mniejsza liczba tym wcześniej klasa zostanie uruchomiona.

Definiując więcej niż jedną klasę pipeline musimy zadbać o ich poprawną kolejność, gdyż zmiany dokonane na wcześniejszym etapie będą dostępne w późniejszych.

Middleware – Oprogramowanie pośredniczące

Ważnym elementem w Scrapy są tzw Middlewares (po polsku pośredniki).

Aby lepiej zrozumieć czym jest middleware warto jeszcze raz spojrzeć na wykres prezentujący architekturę Scrapyego:

Architektura Scrapy z Middleware
Źródło: https://docs.scrapy.org/en/latest/topics/architecture.html

Komunikacja silnika z modułem Spiders oraz Downloader odbywa się za pomocą oprogramowania pośredniczącego Middleware.

Oprogramowanie middleware symbolizowane jest przez kilka elementów, gdyż możemy tworzyć wiele klas pośredników, które utworzą cały proces obsługi obiektó Request i Response.

Kolejność wykonywania Middleware definiujemy w pliku settings.py:

DOWNLOADER_MIDDLEWARES = {
   'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 100,
    'blog_scraper.middlewares.DomainValidationMiddleware': 200,
    'rotating_proxies.middlewares.RotatingProxyMiddleware': 610,
    'rotating_proxies.middlewares.BanDetectionMiddleware': 620,
}

Działą tutaj identyczna zasada definiowania jak w przypadku ItemPipelines.

DownloaderMiddleware

DownloaderMiddleware możemy wykorzystać do:

  • anulowania wykonywania niektórych żądań
  • zmiany żądania przed wysłaniem:
    • ustawienie proxy
    • autoryzacja proxy
    • zmiana nagłówków HTTP
  • przetworzenia odpowiedzi serwera
    • wykrycie czy wystąpiła captcha
  • przetworzenia błędu
    • zapis logów z błędami

Tworząc DownloaderMiddleware powinniśmy skorzystać ze wzoru klasy:

class CustomMiddleware:

    def __init__(self):
        pass

    @classmethod
    def from_crawler(cls, crawler: scrapy.crawler.Crawler):
        return cls()

    def process_request(self, request: scrapy.Request, spider: scrapy.Spider):
        # przetwarzenie żądania, np. dodanie proxy
        pass

    def process_response(self, request: scrapy.Request, response: scrapy.http.Response, spider: scrapy.Spider):
        # przetwarzanie odpowiedzi
        pass

    def process_exception(self, request: scrapy.Request, exception: Exception, spider: scrapy.Spider):
        # przetwarzanie danych w razie wystąpienia wyjątku
        pass

Należy pamiętać, że oprogramowanie pośredniczące działają dwustronnie, więc przez nie przechodzi zarówno żądanie (obiekt Request) jak i odpowiedź na to żądanie (obiekt Response).

SpiderMiddleware

Do komunikacji pomiędzy silnikiem a modułem Spiders wykorzystywane są tzw. SpiderMiddleware.

SpiderMiddleware wykorzystujemy do:

  • przetwarzania wyjątków dotyczących modułu
  • weryfikacja treści odpowiedzi
  • przetwarzania metody start_requests

Wzór klasy SpiderMiddleware:

class CustomSpiderMiddleware:

    def __init__(self):
        pass

    @classmethod
    def from_crawler(cls, crawler: scrapy.crawler.Crawler):
        return cls()

    def process_spider_input(self, response: scrapy.http.Response, spider: scrapy.Spider):
        pass

    def process_spider_output(self, response: scrapy.http.Response, result: Tuple[scrapy.Request, scrapy.item.Item], spider: scrapy.Spider):
        pass

    def process_spider_exception(self, response: scrapy.http.Response, exception: Exception, spider: scrapy.Spider):
        pass

    def process_start_requests(self, start_requests, spider: scrapy.Spider):
        pass

Ekstrakcja danych

Tworząc klasę Spider najważniejszym etapem jest stworzenie kodu odpowiedzialnego za ekstrakcję danych.

Aby wskazać, które dane ze strony Scrapy ma pobrać możemy użyć:

  • Selektorów XPath
  • Selektorów CSS
  • biblioteki BeautfiulSoup

XPath

Selektory XPath są bardzo często wykorzystywane w Web Scrapingu ze względu na ich szerokie możliwości.

Za pomocą XPath możemy nie tylko wskazać odpowiedni element strony, lecz także:

  • pobrać tekst elementu
  • pobrać odpowiedni parametr elementu
  • pobrać element zawierający frazę kluczową – contains
  • pobrać elementy, których zaczynają/kończą się wskazaną frazą – startswith i ends-with
  • pobierać sąsiadujące elementy
  • pobierać element rodzic lub dziecko
  • pobierać elementy, których atrybuty spełniają warunek matematyczny

Składnia selektorów XPath z początku może wydawać się skomplikowana, jednak w miarę pracy z narzędziem zaczniesz doceniać tę mechanikę 🙂

Zobaczmy jak wyglądania ekstrakcja danych przy użyciu XPath w praktyce na przykładzie wpisu o najczęstszych błędach w nauce programowania.

from blog_scraper.items import ArticleItem

class Blog(scrapy.Spider):
    name = 'blog'

    def start_requests(self):
        return scrapy.Request(
            "https://kamil.kwapisz.pl",
            meta={'date': "12.10.2020"},
            callback=self.parse_article
        )

    def parse_article(self, response: scrapy.http.Response):
        article = ArticleItem()
        article['title'] = response.xpath('.//h1[@class="entry-title"]//text()').get()  # '7 najczęstszych błędów podczas nauki programowania'
        article['date'] = response.xpath('.//span[@class="posted-on"]//text()').get()  # '9 lipca 2020'
        article['author'] = response.xpath('.//a[@class="url fn n"]//text()').get()   # "Kamil Kwapisz"
        article['tags'] = response.xpath('.//meta[@property="article:tag"]//@content').getall()  # ['błędy', 'nauka programowania', 'nauka python', 'python']
        # [...]
        article_text = response.xpath('.//p//text()').getall()
        article['text'] = ''.join(article_text)

Metoda get() służy do pobrania jednego elementu, który może zostać wskazany przez selektor. W przypadku wielu wyników, metoda get pobierze tylko pierwszy element.

Selektory CSS

Selektory CSS są łatwiejszą alternatywą dla początkujących, szczególnie tych znających ten temat już z tworzenia arkuszy stylu CSS.

Same w sobie selektory CSS mają nieco mniej możliwości niż XPath, jednak w przypadku Scrapy zostały one rozszerzone o dodatkową składnie, która m.in. umożliwia pobranie konkretnego atrybutu elementu.

Metoda parse_article którą możesz zobaczyć powyżej w kontekście XPath przerobiona na selektory CSS wyglądałaby tak:

def parse_article(self, response: scrapy.http.Response):
        article = ArticleItem()
        article['title'] = response.css('h1.entry-title::text').get()  
        article['date'] = response.css('span.posten-on::text').get()
        article['author'] = response.css('a.url.fn.n::text').get()   # Uwaga na kropki!
        article['tags'] = response.css('meta[property="article:tag"]').xpath('@content').getall() 
        article_text = response.css('p::text').getall()
        article['text'] = ''.join(article_text)

Funkcja pobierania tekstu z elementu (::text) jest składniowym ulepszeniem zapewnianym przez Scrapy. Wykorzystanie tej metody w zwykłych selektorach CSS poza Scrapy nie zadziała.

Jak widać aby pobrać tylko parametr content musieliśmy użyć dodatkowo XPath, gdyż nawet selektory CSS rozszerzone przez Scrapy nie posiadają takiej funkcji wbudowanej w składnię.

Możliwość łączenia obu typów selektorów w taki sposób jest ważną zaletą Scrapy.

Wyrażenia regularne

Zarówno korzystając z selektorów CSS jak i XPath w Scrapy możemy wykorzystać wyrażenia regularne, aby jeszcze lepiej pobierać elementy ze strony.

>>> response.xpath('.//h3').re(r'Powiązane\s*(.*)')
['artykuły</h3>']

BeautifulSoup

Jeżeli żadna z powyższych opcji nam nie odpowiada, możemy użyć biblioteki BeautifulSoup. Nie jest ona wbudowana w Scrapy, jednak możemy ją zaimportować i wykorzystywać.

O bibliotece BeautifulSoup pisałem już we wpisie Web scraping w Pythonie, zachęcam do przeczytania 🙂

Zalety Scrapy

Szybkość i skalowalność

Scrapy pozwala znacząco wyskalować i przyspieszyć nasze crawlery za pomocą wbudowanych mechanizmów asynchroniczności.

Stworzenie scrapera wykonującego kilkadziesiąt żądań HTTP jednocześnie ograniczna się do zmiany jednego parametru w ustawieniach.

# settings.py

CONCURRENT_REQUESTS = 32

Należy jednak pamiętać, że tak dużo żądań wysłanych do jednej strony często spowoduje zablokowanie naszego adresu IP przez systemy ochronne witryny.

Szybkość tworzenia scraperów

Dzięki tak ogromnej łatwości skalowania scraperów w górę (na jednej maszynie) oraz gotowym komponentom możemy w bardzo krótkim czasie stworzyć sprawnego scrapera.

Użycie Scrapy to najszybsza droga do stworzenia działającego rozwiązania na scrapowanie dobrze ustrukturyzowanych danych, w których położenie poszczególnych informacji nie zmienia się na różnych stronach, a strony, z których pobieramy dane łatwo jest odnaleźć, np. poprzez wyniki wyszukiwania.

Zapis danych

Dzięki systemowi klas Item oraz klas wczytujących dane ItemLoader zapis danych staje się bardzo prosty.

Scrapy posiada wbudowane serializatory zapisujące dane w popularnych formatach: JSON, XML, CSV.

Za pomocą ItemPipeline jesteśmy w stanie w prosty sposób dobudować do całego procesu zapis do bazy danych.

Rozszerzalność

Dzięki wsparciu społeczności do Scrapy zostało utworzonych wiele bibliotek, rozszerzeń czy nawet usług chmurowych, które usprawniają i ułatwiają scraping.

Co więcej, dodawanie nowych rozszerzeń i ich używanie bardzo często ograniczna się do instalacji za pomocą instalatora (np. pip) oraz do włączenia jednej z klasy Middleware.

Kompatybilność z zewnętrznymi serwisami

W internecie możesz znaleźć wiele płatnych i bezpłatnych usług dotyczących scrapowania. Są to między innymi proxy, API do renderowania JS, czy narzędzia chmurowe do skalowania scrapingu.

Większość z takich narzędzi została stworzona z myślą o Scrapy, dzięki czemu integracja przebiega bardzo sprawnie i najczęściej jest dobrze opisana.

Wady Scrapy

Scrapy niestety nie jest rozwiązaniem idealnym. Jest to framework, więc jego używanie niesie ze sobą wszelkie wady tego typu rozwiązań. W tym miejscu zachęcam do lektury o różnicach framework vs biblioteka.

Ograniczenia frameworka

Korzystając ze Scrapyego (i właściwie z każdego frameworka) musimy wpisywać się w wyznaczone przez framework „ramy” kodu. Oznacza to pewne ograniczenia w dowolności tworzenia kodu.

Kodując w scrapy jesteśmy ograniczeni do edycji procesu przetwarzania danych (pipeline). Możemy dodawać kolejne części procesu, np. tworzyć nowy ItemPipeline, który przetworzy pobrane dane, jednak trudno będzie obejść ten proces i obsłużyć dane inaczej.

Powiedzmy, że pobieramy oferty ze sklepów. Dla lepszej optymalizacji chcemy zapisać je w bazie danych grupowo, a nie pojedynczo. W takim wypadku możemy stworzyć ItemPipeline, który będzie „odkładał” itemy w pamięci i sprawdzał ile ich odłożyliśmy, żeby zaktualizować bazę danych grupowo.

Problem pojawia się w momencie kiedy chcemy dodatkowo obsłużyć pogrupowane dane, np. wyszukać najniższą i najwyższą cenę, obliczyć medianę. Najprostszym rozwiązaniem byłoby dodanie tej matematycznej logiki do ItemPipeline, który odpowiada za zapis do bazy danych, jednak to obniży jakość kodu i utrudni jego dalszy rozwój w przyszłości.

Logi

O Scrapym można powiedzieć że jest bardzo „gadatliwy” (ang. verbose). Podczas pracy produkuję naprawdę ogromną liczbę logów.

Scrapy w logach umieszcza każdy pobrany item, co nawet przy słownikach o kilku kluczach zajmuje naprawdę sporo miejsca.

Ponadto w logach ląduje wszystkie informacje o uruchomionych rozszerzeniach, middleware, pipeline. Korzystając z dodatkowych rozszerzeń (np. proxy-rotation) dokładamy sobie kolejne logi.

Scrapy oczywiście obsługuje standardowe poziomy logów, jednak może to nie być wystarczające. Ustawiając poziom logów na WARNING:

> scrapy crawl spider_name -L WARNING

pozbędziemy się co prawda logów z pobranymi itemami, jednak wtedy tych logów może być za mało.

Najlepszym rozwiązaniem przy produkcyjnym wykorzystaniu Scrapy jest dodanie zewnętrznego narzędzia do analizy logów (np. Kibana).

Ciche błędy

Problemem występującym przy skalowaniu scrapera napisanego w Scrapy są „ciche błędy”.

Mam tu na myśli sytuację, w której podczas parsowania strony Scrapy napotkał błąd w kodzie, który nie został wykryty przed wykonaniem kodu. Taki błąd pozostanie niezgłoszony, a parsowanie strony zostanie po prostu przerwane.

Tego typu problemy bardzo utrudniają debugowanie aplikacji.

Brak statusu crawlowania

Scrapy nie posiada również wbudowanego systemu pozwalającego podejrzeć status procesu crawlowania.

Przez brak przechowywania stanu trudno jest określić jak długo jeszcze potrwa cały proces.

Brak skalowalności wszerz

Scrapy świetnie skaluje się na jednej maszynie, jednak jest problematyczny w przypadku skalowania wszerz (skalowania na wiele maszyn). Framework nie posiada wbudowanego mechanizmu pozwalającego na uruchamiania tego samego procesu scrapowania na wielu maszynach, przez co trzeba uciekać się do używania zewnętrznych narzędzi wspierających ten proces.

Kiedy używać scrapy?

Scrapy idealnie nadaje się w przypadku stron z dobrze ustrukturyzowanymi danymi.

Jeżeli naszym zadaniem jest pobranie ofert ze sklepu dostępnych z poziomu wyszukiwarki – Scrapy będzie świetnym sposobem, gdyż do właśnie takich projektów został stworzony.

Dzięki skalowalności będziemy mogli w prosty sposób zwiększyć szybkość scrapowania, a wykorzystanie dodatkowych zabezpieczeń takich jak proxy pozwoli na masowe scrapowanie z niewielkim ryzykiem zostania zbanowanym.

Scrapy idealnie sprawdza się do pobierania danych ze sklepów internetowych, blogów, stron z ofertami, landing page, wizytówek.

Kiedy nie warto używać Scrapy?

Struktura strony trudnej do scrapowania
Struktura trudna do scrapowania

Scrapy nie będzie najlepszym wyborem w przypadku stron, na których ułożenie danych do pobrania nie jest tak oczywiste. Konieczność dodatkowego crawlowania stron, z których nie pobieramy dane wydłuży cały proces.

Sam framework Scrapy może również nie być wystarczającym w przypadku cyklicznego scrapowania ogromnej ilości danych które wymagają dodatkowego przetwarzania (np. do BigData). Do tego celu przydadzą się dodatkowe usługi pozwalające na skalowanie scrapowania na inne maszyny.

Podsumowanie

Scrapy jest aktualnie jednym z najlepszych narzędzi do scrapowania i crawlowania stron internetowych.

Jego szybkość, łatwa rozszerzalność i stabilność sprawiają, że będzie to rozwiązanie idealne dla wielu klasycznych przypadku scrapowania danych, które świetnie sprawdzą się nawet w rozwiązaniach produkcyjnych.

Podchodząc do tematu pobierania dużej ilości danych zdecydowanie warto rozważyć użycie tego frameworka.

Scrapy jest świetny do masowego scrapowania. Zanim jednak zaczniesz to robić, warto zastanowić się czy na pewno powinno się scrapować. Jeżeli chcesz dowiedzieć się więcej na ten temat zapraszam Cię do wpisu „Kiedy NIE scrapować”.

Dodatkowe materiały

Jeśli chcesz być na bieżąco z najnowszymi materiałami, polub nasz fanpage na Facebooku:

https://www.facebook.com/kamil.kwapisz.python
Tagged ,

7 thoughts on “Web scraping w Scrapy

  1. Sporo czytałem o scrapy. Ale ten poradnik przebija wszystko. Troszkę to jednak skomplikowane na pierwszy rzuta oka.

    1. Dzięki! 🙂

      To prawda, na początku scrapy może wydawać się trudne do ogarnięcia. To częste przy tak dużych frameworkach, które bardzo wymuszają strukturę kodu.

      Jednak jak napisze się swojego pierwszego scrapera w Scrapy to wszystko znacznie się rozjaśnia 🙂

      Pozdrawiam!

  2. Zawsze miałem ogromne problemy z scrapy – nie wiem dlaczego, ale mi to totalnie nie leży.

    1. Scrapy faktycznie jest dość specificzne 🙂 Fakt, że jest to framework, w który musimy się wpasować też nie pomaga… Mnie najbardziej przekonało do siebie tym jak szybko możemy stworzyć stabilny i mega szybki scraper 🙂

  3. Scrapy idealnie nadaje się w przypadku stron z dobrze ustrukturyzowanymi danymi – czy tutaj chodzi o dane uporządkowane schema? Czy coś mi nie świta..?

    1. Nie chodzi tu o żadne schema czy inne bardzo sformalizowane formaty przechowywania danych 🙂 Chodziło mi o strony, w których struktura rozmieszczenia danych jest powtarzalna, jak np. w sklepach internetowych. Każda aukcja sklepu stworzona jest według identycznego schematu, wszystkie strony aukcji są posortowane na kategorie, w prosty sposób możemy wyciągnąć listę stron, które nas interesują (np. wynik wyszukania produktów lub też strona kategorii) 🙂

Comments are closed.