Wzorzec Observer w Scrapy

Spider wysyłający sygnały do subskrybentów: statystyk, alertów i eksportu

Observer to behawioralny wzorzec projektowy, w którym jeden obiekt publikuje zdarzenie, a inne obiekty reagują na to zdarzenie.

W Scrapy najbliższym praktycznym mechanizmem są sygnały. Scrapy emituje zdarzenia cyklu życia, a rozszerzenia mogą się do nich podpiąć bez bezpośredniego wywoływania ich przez spider.

Spis treści

Problem

Wyobraź sobie spider, który scrapuje produkty. Po zebraniu produktu może być potrzebnych kilka reakcji:

  • Zapis produktu.
  • Aktualizacja metryk.
  • Logowanie postępu crawlowania.
  • Wysłanie podejrzanych produktów do monitoringu.

Jeśli spider wywołuje każdy zależny system bezpośrednio, robi się silnie powiązany z resztą aplikacji:

def parse(self, response):
    product = self.extract_product(response)

    database.save(product)
    metrics.record(product)
    logger.info("scraped product", extra={"product": product})
    monitoring.check(product)

    yield product

Spider zna teraz cztery różne systemy. To utrudnia testowanie, zmiany i ponowne użycie kodu.

Alternatywa w stylu Observer polega na tym, że spider zwraca dane albo Scrapy emituje sygnał, a zainteresowane komponenty reagują osobno.

Observer na jednym diagramie

Subject
  -> Observer A
  -> Observer B
  -> Observer C

Subject tworzy zmianę albo zdarzenie. Obserwatorzy subskrybują to zdarzenie.

Dla sygnałów Scrapy wygląda to bardziej tak:

Scrapy Engine
  -> signal: item_scraped
    -> StatsExtension
    -> LoggingExtension
    -> MonitoringExtension

Spider nie musi wiedzieć, które rozszerzenia słuchają sygnału.

Mapowanie Scrapy na Observer

Koncepcja Observer Koncepcja Scrapy
Subject albo publisher Komponent emitujący sygnał
Observer albo subscriber Funkcja albo metoda podpięta do sygnału
Subscribe crawler.signals.connect(...)
Unsubscribe crawler.signals.disconnect(...)
Notify Dispatch sygnału
Event Zdarzenie cyklu życia Scrapy, np. item_scraped

Sygnały Scrapy są dobre do powiadomień cyklu życia, metryk, logowania, sprzątania i zachowań przekrojowych.

Minimalny przykład sygnału Scrapy

Rozszerzenie reagujące na każdy poprawnie zescrapowany item:

from scrapy import signals

class StatsObserver:
    @classmethod
    def from_crawler(cls, crawler):
        observer = cls()
        crawler.signals.connect(observer.item_scraped, signal=signals.item_scraped)
        return observer

    def item_scraped(self, item, response, spider):
        spider.logger.info("Scraped item: %s", item)

Włączenie rozszerzenia w ustawieniach Scrapy:

EXTENSIONS = {
    "myproject.extensions.StatsObserver": 500,
}

Od tej chwili rozszerzenie reaguje na item_scraped. Spider nie wywołuje StatsObserver.item_scraped() bezpośrednio.

Przykład crawlera produktowego

Spider produktowy powinien skupić się na ekstrakcji danych:

def parse(self, response):
    yield {
        "url": response.url,
        "name": response.css("h1::text").get(),
        "price": response.css(".price::text").get(),
    }

Oddzielne komponenty mogą odpowiadać za oddzielne rzeczy:

Spider -> Item
  -> ProductPipeline zapisuje albo waliduje item
  -> signal item_scraped aktualizuje metryki
  -> signal item_scraped zapisuje logi operacyjne
  -> signal spider_closed flushuje końcowe statystyki crawla

Dzięki temu spider jest skupiony na crawlowaniu i ekstrakcji.

Sygnały czy pipeline’y

Dobierz mechanizm Scrapy do zadania.

Potrzeba Lepszy wybór
Walidacja, wzbogacenie, odrzucenie albo zapis itemu Item pipeline
Logowanie zdarzeń cyklu życia crawla Rozszerzenie z sygnałem
Aktualizacja metryk po zebraniu itemu Rozszerzenie z sygnałem
Otwarcie albo zamknięcie zasobów na starcie lub końcu spidera Rozszerzenie z sygnałem
Zmiana zachowania requestów albo response’ów Middleware
Prosta zależność, która zawsze jest wymagana Bezpośrednie wywołanie metody
Gwarancja dostarczenia, retry, persystencja albo przetwarzanie między maszynami Kolejka albo message broker

Sygnały nie zastępują pipeline’ów. Pipeline’y są normalnym miejscem dla przetwarzania itemów. Sygnały są lepsze do powiadomień i hooków cyklu życia.

Kiedy nie używać Observera

Observer nie zawsze jest najprostszym projektem.

Unikaj go, gdy istnieje tylko jedna oczywista zależność:

Order -> EmailService.send_confirmation()

Observer może tu ukryć relację, która jest prosta i bezpośrednia.

Unikaj go, gdy kolejność powiadomień jest logiką biznesową:

A musi się zakończyć
  -> potem B
    -> potem C

To jest workflow. Użyj jawnej orkiestracji, łańcucha albo sekwencyjnych wywołań.

Nie używaj prostego in-memory Observera, gdy potrzebujesz gwarancji dostarczenia:

Subject wysyła powiadomienie
  -> observer crashuje
    -> powiadomienie przepada

Do trwałego przetwarzania użyj kolejki albo message brokera z retry, acknowledgement i persystencją.

Unikaj zbyt wielu obserwatorów dla częstych zdarzeń. Milion zdarzeń wysłanych do stu obserwatorów daje sto milionów wywołań handlerów. Lepsze może być batchowanie, agregacja, sampling albo inna architektura eventowa.

Praktyczne zasady

  • Spider powinien skupiać się na ekstrakcji danych i zwracaniu requestów albo itemów.
  • Pipeline’y stosuj do walidacji, wzbogacania, zapisu i odrzucania itemów.
  • Sygnały stosuj do zdarzeń cyklu życia, metryk, logowania i sprzątania.
  • Handler sygnału powinien być mały i przewidywalny.
  • Nie chowaj krytycznych workflow w łańcuchach sygnałów.
  • Loguj wystarczająco dużo kontekstu, aby wiedzieć, który handler zareagował na które zdarzenie.
  • Użyj trwałej kolejki, gdy utrata zdarzenia jest niedopuszczalna.

Definicje powiązanych terminów znajdziesz w słowniku inżynierskim.