Wzorzec Observer w Scrapy

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
- Observer na jednym diagramie
- Mapowanie Scrapy na Observer
- Minimalny przykład sygnału Scrapy
- Przykład crawlera produktowego
- Sygnały czy pipeline’y
- Kiedy nie używać Observera
- Praktyczne zasady
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.