Web Scraping mit Scrapy und MongoDB


Scrapy ist ein robustes Python-Web-Scraping-Framework, das Anfragen asynchron verwalten, Links folgen und Website-Inhalte analysieren kann. Zum Speichern von Scraped-Daten können Sie MongoDB verwenden, eine skalierbare NoSQL-Datenbank, die Daten in einem JSON-ähnlichen Format speichert. Die Kombination von Scrapy mit MongoDB bietet eine leistungsstarke Lösung für Web-Scraping-Projekte, die die Effizienz von Scrapy und die flexible Datenspeicherung von MongoDB nutzt.

In diesem Tutorial erfahren Sie, wie Sie:

  • Richten Sie ein Scrapy-Projekt ein und konfigurieren Sie es
  • Erstellen Sie mit Scrapy einen funktionellen Web-Scraper
  • Extrahieren Sie Daten von Websites mithilfe von Selektoren
  • Speichern gecrackte Daten in einer MongoDB-Datenbank
  • Testen und debugieren Sie Ihren Scrapy Web Scraper

Wenn Sie neu im Web-Scraping sind und nach flexiblen und skalierbaren Tools suchen, dann ist dies das richtige Tutorial für Sie. Sie profitieren auch vom Erlernen dieses Toolkits, wenn Sie schon einmal Websites gescrapt haben, aber die Komplexität Ihres Projekts mit Beautiful Soup and Requests zu groß geworden ist.

Um dieses Tutorial optimal nutzen zu können, sollten Sie über grundlegende Python-Programmierkenntnisse verfügen, objektorientierte Programmierung verstehen, problemlos mit Paketen von Drittanbietern arbeiten und mit HTML und CSS vertraut sein.

Am Ende wissen Sie, wie Sie statische Daten aus dem Internet abrufen, analysieren und speichern, und Sie sind mit mehreren nützlichen Tools vertraut, mit denen Sie viel tiefer gehen können.

Bereiten Sie das Scraper-Gerüst vor

Sie richten zunächst die erforderlichen Tools ein und erstellen eine grundlegende Projektstruktur, die als Rückgrat für Ihre Scraping-Aufgaben dient.

Während Sie das Tutorial durcharbeiten, erstellen Sie ein vollständiges Web-Scraping-Projekt und gehen es als ETL-Prozess (Extrahieren, Transformieren, Laden) an:

  • Extrahieren Sie Daten von der Website mit einem Scrapy-Spider als Webcrawler.
  • Transformieren Sie diese Daten, indem Sie sie beispielsweise mit einer Item-Pipeline bereinigen oder validieren.
  • Laden Sie die transformierten Daten mit einer Item-Pipeline in ein Speichersystem wie MongoDB.

Scrapy stellt ein Gerüst für alle diese Prozesse bereit, und Sie nutzen dieses Gerüst, um das Web-Scraping zu erlernen, indem Sie der robusten Struktur folgen, die Scrapy bereitstellt und auf der sich zahlreiche Web-Scraping-Projekte im Unternehmensmaßstab stützen.

Zuerst installieren Sie Scrapy und erstellen ein neues Scrapy-Projekt. Anschließend erkunden Sie die automatisch generierte Projektstruktur, um sicherzustellen, dass Sie gut gerüstet sind, um mit der Erstellung eines leistungsstarken Web Scrapers fortzufahren.

Installieren Sie das Scrapy-Paket

Um mit Scrapy zu beginnen, müssen Sie es zunächst mit pip installieren. Erstellen und aktivieren Sie eine virtuelle Umgebung, um die Installation von Ihrer globalen Python-Installation getrennt zu halten. Anschließend können Sie Scrapy installieren:

(venv) $ python -m pip install scrapy

Nachdem die Installation abgeschlossen ist, können Sie sie überprüfen, indem Sie den Befehl scrapy ausführen und die Ausgabe anzeigen:

(venv) $ scrapy
Scrapy 2.11.2 - no active project

Usage:
  scrapy <command> [options] [args]

Available commands:
  bench         Run quick benchmark test
  fetch         Fetch a URL using the Scrapy downloader
  genspider     Generate new spider using pre-defined templates
  runspider     Run a self-contained spider (without creating a project)
  settings      Get settings values
  shell         Interactive scraping console
  startproject  Create new project
  version       Print Scrapy version
  view          Open URL in browser, as seen by Scrapy

  [ more ]      More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command

Das Befehlszeilenprogramm (CLI) sollte den Hilfetext von Scrapy anzeigen. Dies bestätigt, dass Sie das Paket korrekt installiert haben. Als nächstes führen Sie den hervorgehobenen Befehl startproject aus, um ein Projekt zu erstellen.

Erstellen Sie ein Scrapy-Projekt

Scrapy basiert auf Projekten. Im Allgemeinen erstellen Sie für jedes Web-Scraping-Projekt, an dem Sie arbeiten, ein neues Projekt. In diesem Tutorial arbeiten Sie am Scraping einer Website namens „Books to Scrape“, damit Sie Ihr Projekt books nennen können.

Wie Sie vielleicht bereits im Hilfetext erkannt haben, bietet das Framework einen Befehl zum Erstellen eines neuen Projekts:

(venv) $ scrapy startproject books

Dieser Befehl ähnelt dem Befehl, den Sie zum Generieren des Gerüsts für ein neues Django-Projekt verwenden, und Scrapy und Django sind zwei ausgereifte Frameworks, die gut miteinander interagieren können.

In Scrapy generiert der Befehl startproject ein neues Scrapy-Projekt mit dem Namen books, das Ihnen eine Projektstruktur mit mehreren automatisch generierten Dateien und Verzeichnissen einrichtet:

books/
│
├── books/
│   │
│   ├── spiders/
│   │   └── __init__.py
│   │
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   └── settings.py
│
└── scrapy.cfg

Scrapy basiert auf dieser Struktur und hilft Ihnen dabei, Ihr Scrapy-Projekt effizient zu organisieren. Klicken Sie sich durch die verschiedenen Dateien und Ordner und überlegen Sie, was sie jeweils im Kontext eines Web-Scraping-Projekts bedeuten könnten:

  • books/ ist das Stammverzeichnis Ihres Scrapy-Projekts.
  • books/spiders/ ist ein Verzeichnis zum Speichern Ihrer Spider-Definitionen.
  • books/items.py ist eine Datei zum Definieren der Datenstruktur Ihrer gescrapten Artikel.
  • books/middlewares.py ist eine Datei zum Verwalten Ihrer benutzerdefinierten Middlewares.
  • books/pipelines.py ist eine Datei zum Definieren von Pipelines für die Elementverarbeitung.
  • books/settings.py ist die Einstellungsdatei zum Konfigurieren Ihres Projekts.

In diesem Tutorial berühren Sie die meisten der oben aufgeführten Dateien und fügen ihnen Ihre benutzerdefinierten Funktionen hinzu. Nachdem Sie Ihr Projektgerüst eingerichtet haben, werfen Sie nun einen Blick auf die Quellwebsite, von der Sie Informationen extrahieren möchten.

Untersuchen Sie die Quelle, die Sie abkratzen möchten

Sie werden mit Ihren Web-Scraping-Ambitionen nicht weit kommen, wenn Sie die Struktur der Website, von der Sie Informationen extrahieren möchten, nicht kennen. Daher ist es ein wesentlicher erster Schritt in diesem Prozess, die Zielwebsite zu überprüfen. Der schnellste Weg, eine Website zu inspizieren, besteht darin, Ihren Browser zu öffnen, zur Website zu navigieren und herumzuklicken.

Sehen Sie sich die Website in Ihrem Browser an

Navigieren Sie zunächst als Benutzer durch die Zielwebsite in Ihrem Browser. Identifizieren Sie die spezifischen Elemente, die Sie entfernen möchten. Wenn Sie beispielsweise Buchtitel, Preise und URLs aus einem Buchladen extrahieren, überprüfen Sie den HTML-Code, um diese Elemente zu finden. Dies erreichen Sie am besten mit den integrierten Entwicklertools Ihres Browsers.

Sobald Sie die Entwicklertools in Ihrem Browser geöffnet und ein Buchelement identifiziert haben, wird eine Benutzeroberfläche angezeigt, die der unten gezeigten ähnelt:

Nachdem Sie eine gute Vorstellung davon bekommen haben, wie die Informationen auf der Website organisiert sind, möchten Sie die eindeutigen Selektoren für jede Information sammeln, die Sie durchsuchen möchten. Sie möchten aus jedem Buch drei Datenpunkte extrahieren:

  1. Titel
  2. Preis
  3. URL

Sie können die Entwicklertools verwenden, um XPath-Ausdrücke oder CSS-Selektoren für jedes dieser Elemente zu sammeln, indem Sie mit der rechten Maustaste auf das Element klicken und Kopieren → Selektor kopieren oder Kopieren → XPath kopieren auswählen . Wenn Sie beispielsweise die CSS-Selektoren für das erste Buch auf der Website kopieren, sollten Sie für jedes der Elemente die folgenden Werte erhalten:

Title

#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > Article > h3 > a

Price

#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > Artikel > div.product_price > p.price_color

URL

#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > Article > h3 > a

Diese sind viel zu lang, um den Überblick zu behalten! Möglicherweise stellen Sie jedoch fest, dass alle auf die gleiche Weise beginnen. Das liegt daran, dass es sich um absolute Pfade handelt, die beim Stammelement des HTML-Dokuments beginnen.

Es sieht so aus, als ob Sie alle Informationen, die Sie interessieren, in einem <article>-Tag finden können. Gehen Sie zurück zu Ihrem Browser und identifizieren Sie das <article>-Element:

<article class="product_pod">
  <div class="image_container">
    <a href="catalogue/a-light-in-the-attic_1000/index.html"><img
        src="media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg"
        alt="A Light in the Attic" class="thumbnail"></a>
  </div>
  <p class="star-rating Three">
    <i class="icon-star"></i>
    <i class="icon-star"></i>
    <i class="icon-star"></i>
    <i class="icon-star"></i>
    <i class="icon-star"></i>
  </p>
  <h3><a href="catalogue/a-light-in-the-attic_1000/index.html"
      title="A Light in the Attic">A Light in the ...</a></h3>
  <div class="product_price">
    <p class="price_color">£51.77</p>
    <p class="instock availability">
      <i class="icon-ok"></i>
      In stock
    </p>
    <form>
      <button type="submit" class="btn btn-primary btn-block"
        data-loading-text="Adding...">Add to basket</button>
    </form>
  </div>
</article>

Die hervorgehobenen Zeilen bestätigen, dass Sie alle drei Informationen für jedes Buch in seinem <article>-Element finden können – und dieses Element hat sogar einen Klassennamen, product_pod!

Sie können daher planen, zunächst alle <article>-Elemente mit dem Klassennamen product_pod anzusprechen. Diese stellen das übergeordnete Element eines einzelnen Buchs dar, das alle gewünschten Informationen enthält. Sie können dann alle diese durchlaufen und die relevanten Informationen mit viel kürzeren Selektoren extrahieren.

Aber gibt es eine gute Möglichkeit, zu bestätigen, welche Selektoren Ihren Wünschen entsprechen, bevor Sie das gesamte Spider-Skript schreiben? Ja, das gibt es! Sie können mit der integrierten Shell von Scrapy genau das und noch mehr tun.

Vorschau der Daten mit der Scrapy Shell

Bevor Sie einen vollwertigen Spider schreiben, ist es oft sinnvoll, eine Vorschau anzuzeigen und zu testen, wie Sie Daten aus einer Webseite extrahieren. Die Scrapy-Shell ist ein interaktives Tool, mit dem Sie Daten aus Webseiten in Echtzeit untersuchen und extrahieren können. Dies hilft beim Experimentieren mit XPath-Ausdrücken und CSS-Selektoren, um sicherzustellen, dass sie korrekt auf die gesuchten Elemente abzielen.

Sie können die Shell öffnen und auf die Site verweisen, die Sie durchsuchen möchten, indem Sie den Befehl shell gefolgt von der URL der Site verwenden:

(venv) $ scrapy shell http://books.toscrape.com

Dieser Befehl lädt die angegebene URL und bietet Ihnen eine interaktive Umgebung zum Erkunden der HTML-Struktur der Seite. Sie können es sich wie eine interaktive Python-REPL vorstellen, jedoch mit Zugriff auf Scrapy-Objekte und den HTML-Inhalt der Zielseite.

Wenn Sie diesen Befehl ausführen, werden einige Protokolle angezeigt, gefolgt von Gebrauchsanweisungen:

...
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x1070dcd40>
[s]   item       {}
[s]   request    <GET https://books.toscrape.com/>
[s]   response   <200 https://books.toscrape.com/>
[s]   settings   <scrapy.settings.Settings object at 0x10727ac90>
[s]   spider     <BookSpider 'book' at 0x107756990>
[s] Useful shortcuts:
[s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser
>>>

Die Shell stellt mehrere vorimportierte Objekte bereit, wobei response das wichtigste für die Überprüfung der Daten ist, die Sie möglicherweise extrahieren möchten.

Das response-Objekt verfügt über mehrere nützliche Attribute und Methoden, mit denen Sie die Seite auf ähnliche Weise überprüfen können, wie Sie es zuvor mit den Entwicklertools Ihres Browsers getan haben:

  • .url enthält die URL der Seite.
  • .status zeigt Ihnen den HTTP-Statuscode der Antwort.
  • .headers zeigt alle HTTP-Header der Antwort an.
  • .body enthält die Rohbytes der Antwort.
  • .text enthält den dekodierten Text der Antwort als Unicode-String.

Um beispielsweise den HTTP-Statuscode der Antwort zu überprüfen, können Sie den folgenden Befehl ausführen:

>>> response.status
200

Das response-Objekt enthält auch den Inhalt der Seite, die Sie geladen haben. Sie können damit den HTML-Code untersuchen und die Elemente finden, die Sie durchsuchen möchten. Um den HTML-Inhalt der Seite anzuzeigen, können Sie .text verwenden:

>>> response.text
'<!DOCTYPE html>\n<!--[if lt IE 7]>
...

Das Drucken des gesamten HTML-Codes ist jedoch normalerweise überwältigend, daher ist es praktischer, XPath-Ausdrücke oder CSS-Selektoren zu verwenden, um auf bestimmte Elemente abzuzielen.

Scrapy verwendet die Parsel-Bibliothek, um XPath-Ausdrücke und CSS-Selektoren zu verarbeiten. Parsel bietet eine Selector-Klasse, die Sie unabhängig von Scrapy verwenden können. Dies ist nützlich, wenn Sie mit der HTML-Analyse experimentieren möchten:

>>> from parsel import Selector

>>> html = "<html><body><h1>Hello, world!</h1></body></html>"
>>> selector = Selector(text=html)
>>> text = selector.xpath("//h1/text()").get()
>>> text
'Hello, world!'

Sie können dieselben Parsel-Selektoren in Scrapy verwenden, um das response-Objekt zu analysieren.

Sie können jetzt die CSS-Selektoren ausprobieren, die Sie mit Ihren Entwicklertools gefunden haben, um zu überprüfen, ob sie auf die gewünschten Informationen abzielen können. Sie könnten stattdessen auch XPath-Ausdrücke verwenden, aber CSS-Selektoren werden Ihnen wahrscheinlich vertrauter sein, wenn Sie sich mit der Webentwicklung beschäftigt haben.

Zuerst möchten Sie im HTML einen Drilldown zu den <article>-Elementen durchführen, die alle Informationen für ein Buch enthalten, und dann jeden Buchtitel auswählen, indem Sie nur den relevanten Teil des langen CSS-Selektors verwenden, den Sie benötigen. Habe durch Kopieren herausgefunden:

>>> all_book_elements = response.css("article.product_pod")

>>> for book in all_book_elements:
...     print(book.css("h3 > a::attr(title)").get())
...
A Light in the Attic
Tipping the Velvet
Soumission
(...)

Sie haben zunächst alle Buchcontainerelemente als Ziel ausgewählt und das Ergebnis all_book_elements zugewiesen. Aufrufe von .css() geben andere Selector-Objekte zurück, sodass Sie mit einem weiteren Aufruf von .css() einen weiteren Drilldown durchführen können. Dieses Mal definieren Sie, dass Sie den Wert des HTML-Attributs title aller Linkelemente haben möchten, die in einem <h3>-Element enthalten sind. Durch Aufrufen von .get() für den resultierenden Selektor erhalten Sie die erste Übereinstimmung der Ergebnisse als Zeichenfolge.

Mit diesem Ansatz können Sie alle Buchtitel erfolgreich auf Ihrem Terminal drucken.

Sie haben bestätigt, dass Sie alle Buchtitel mit einem verkürzten CSS-Selektor erhalten können. Richten Sie nun die URLs und Preise auf ähnliche Weise aus:

>>> for book in all_book_elements:
...     print(book.css("h3 > a::attr(href)").get())
...
catalogue/a-light-in-the-attic_1000/index.html
catalogue/tipping-the-velvet_999/index.html
catalogue/soumission_998/index.html
(...)

>>> for book in all_book_elements:
...     print(book.css(".price_color::text").get())
...
£51.77
£53.74
£50.10
(...)

Großartig, Sie können alle gewünschten Informationen adressieren. Bevor Sie diesen Ansatz in Ihrem Spider verwenden, sollten Sie die Konzepte der CSS-Syntax berücksichtigen, die Sie verwendet haben, um auf die richtigen Elemente abzuzielen:

h3 and a

Zielt auf Elemente dieses HTML-Elementtyps ab

>

Gibt ein untergeordnetes Element an

.price_color and article.product_pod

Gibt einen Klassennamen an und gibt optional an, auf welchem Element der Klassenname stehen soll

::text

Zielt auf den Textinhalt eines HTML-Tags ab

::attr(href)

Zielt auf den Wert eines HTML-Attributs ab, in diesem Fall auf die URL in einem href-Attribut

Sie können die CSS-Syntax innerhalb von Selektoren verwenden, um auf bestimmte Informationen auf Websites abzuzielen, sei es der Text innerhalb eines Elements oder sogar der Wert eines HTML-Attributs. An diesem Punkt haben Sie kürzere CSS-Selektoren identifiziert und bestätigt, die Ihnen Zugriff auf alle gesuchten Informationen ermöglichen:

Book elements

article.product_pod

URL

h3 > a::attr(href)

Title

h3 > a::attr(title)

Price

.price_color::text

Das sieht viel überschaubarer aus! Sie können diese Selektoren beibehalten, da Sie sie beim Schreiben Ihres Spiders nur kurz verwenden werden.

Wenn Sie die Scrapy-Shell auf diese Weise verwenden, können Sie Ihre Selektoren verfeinern und sicherstellen, dass Ihr Spider die gewünschten Daten korrekt extrahiert, wenn Sie den eigentlichen Spider-Code schreiben. Dieser interaktive Ansatz spart Zeit und reduziert Fehler im Scraping-Prozess.

Erstellen Sie Ihren Web Scraper mit Scrapy

Nachdem Sie nun ein solides Verständnis für die Inspektion von Websites erworben und Ihr Scrapy-Projekt erfolgreich erstellt haben, ist es an der Zeit, Ihren Spider zu erstellen. Beim Schreiben Ihres Spiders geht es jedoch nicht nur darum, ihn auf eine Website zu richten und loszulassen. Sie müssen das Spinnennetz sorgfältig weben, um sicherzustellen, dass es die von Ihnen benötigten Daten genau und effizient erfasst.

In diesem Abschnitt behandeln Sie grundlegende Konzepte wie das Erstellen von Elementklassen zum Strukturieren Ihrer Daten, das Anwenden von Selektoren zum Festlegen der Elemente, die Sie extrahieren möchten, und die Handhabung der Paginierung zum Durchlaufen mehrseitiger Datensätze. Am Ende dieses Abschnitts verfügen Sie nicht nur über einen funktionsfähigen Web Scraper, sondern Sie werden auch ein tieferes Verständnis dafür haben, wie Sie Ihre Spider anpassen und optimieren können, um verschiedene Web Scraping-Herausforderungen zu bewältigen.

Sammeln Sie die Daten in einem Artikel

Ein Item ist ein Container, der die Struktur der Daten definiert, die Sie sammeln möchten. Es handelt sich um eine Python-Zuordnung, was bedeutet, dass Sie Werte ähnlich wie mit einem Python-Wörterbuch zuweisen können.

Ein Item bietet außerdem zusätzliche Funktionen, die für Web Scraping nützlich sind, wie z. B. Validierung und Serialisierung. Das Definieren eines Elements für Ihre Daten hilft dabei, die von Ihnen erfassten Daten zu organisieren und zu standardisieren, was einen geringeren Aufwand für die spätere Verarbeitung und Speicherung bedeutet.

Um mit der Verwendung eines Elements zu beginnen, müssen Sie eine Klasse erstellen, die von scrapy.Item erbt, und die Felder, die Sie scrapen möchten, als Instanzen von scrapy.Field definieren. Mit diesem Ansatz können Sie die Datenstruktur auf saubere und verwaltbare Weise spezifizieren.

Die Standardprojektstruktur, die Sie zuvor erstellt haben, bietet einen Ort zum Definieren Ihrer Artikelklassen in einer Datei namens items.py. Öffnen Sie die Datei und fügen Sie der generierten Klasse BooksItem Code mit Feldern für die URL, den Titel und den Preis jedes Buchs hinzu:

import scrapy

class BooksItem(scrapy.Item):
    url = scrapy.Field()
    title = scrapy.Field()
    price = scrapy.Field()

In diesem Beispiel erbt BooksItem von Item und Sie definieren .url, .title und .price als Felder. Die Klasse Field ist ein Platzhalter, der lediglich ein Alias für ein Python-dict ist. Es lässt sich in Item integrieren, sodass Sie damit die Felder definieren können, die Item enthalten wird.

Nachdem Sie BooksItem definiert haben, können Sie es in Ihrem Spider zum Sammeln von Daten verwenden. Sie nutzen die Selektoren, die Sie mit der Shell erkundet haben, um Ihrem Spider dabei zu helfen, URLs, Buchtitel und Preise auf der Zielwebsite zu finden.

Schreiben Sie einen Scrapy Spider

Endlich ist es an der Zeit, die Teile zusammenzusetzen, die Sie bisher sorgfältig zusammengesetzt haben. Das Framework verfügt über einen weiteren Befehl, mit dem Sie bequem einen Spider innerhalb Ihres Projekts erstellen können. Stellen Sie sicher, dass Sie in Ihren Projektordner mit dem Namen books/ navigiert sind, und führen Sie dann den Befehl aus, um Ihren ersten Spider zu generieren:

(venv) $ cd books/
(venv) $ scrapy genspider book https://books.toscrape.com/
Created spider 'book' using template 'basic' in module:
  books.spiders.book

Durch die Übergabe eines Spider-Namens und der Ziel-URL an genspider erstellt Scrapy eine neue Datei im Verzeichnis spiders/ Ihres Projekts. Öffnen Sie es und werfen Sie einen Blick darauf. Sie sehen eine neue Klasse, die von scrapy.Spider erbt und den von Ihnen eingegebenen Namen mit Spider als Suffix hat – zum Beispiel BookSpider in deinem Fall. Es enthält auch einige zusätzliche Gerüste, die Sie jetzt mit Ihren spezifischen Informationen bearbeiten.

Jede Basisspinne hat einen Namen und muss zwei weitere Informationen bereitstellen:

  1. Wo fange ich mit dem Schaben an?
  2. So analysieren Sie die Antwort

Sie können diese Informationen mit start_urls bzw. .parse() bereitstellen. start_urls ist eine Liste von Zeichenfolgen, die in Ihrem Fall nur eine URL enthält, aber auch mehr enthalten könnte. Ihr bevorzugtes Web-Scraping-Framework hat diesen Wert und den Namen der Spinne bereits ausgefüllt, als Sie das Spinnengerüst mit genspider eingerichtet haben.

Die .parse()-Methode eines Spider ist eine Rückrufmethode, die Scrapy mit dem heruntergeladenen response-Objekt für jede URL aufruft. Es sollte die Logik enthalten, um die relevanten Informationen aus der Antwort zu extrahieren. Sie können mit den CSS-Selektoren arbeiten, die Sie zuvor identifiziert haben, um die Informationen aus der Antwort zu extrahieren, und dann Ihr BooksItem verwenden, um die Daten zu sammeln:

import scrapy

from books.items import BooksItem

class BookSpider(scrapy.Spider):
    name = "book"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/"]

    def parse(self, response):
        for book in response.css("article.product_pod"):
            item = BooksItem()
            item["url"] = book.css("h3 > a::attr(href)").get()
            item["title"] = book.css("h3 > a::attr(title)").get()
            item["price"] = book.css(".price_color::text").get()
            yield item

Sie haben mit nur wenigen Codezeilen eine Scrapy-Spinne erstellt! Beachten Sie, wie Sie den Code, den Sie in der Shell geschrieben haben, wiederverwendet haben. Sie mussten die Informationen lediglich in einer BooksItem-Instanz sammeln, was Sie mithilfe der Wörterbuchsyntax erledigten. Schließlich haben Sie yield verwendet, um jedes Element zu generieren.

Sie richten .parse() so ein, dass zuerst alle Bücher auf der aktuellen Seite gefunden werden und dann jedes Buch durchlaufen wird. Für jedes Buch wird eine BooksItem-Instanz erstellt und mithilfe von CSS-Selektoren URL, Titel und Preis extrahiert. Es weist diese Werte den entsprechenden Feldern Ihres BooksItem zu und übergibt dann die Artikelinstanz an die Artikelpipeline.

In diesem Fall ruft Python .parse() nur einmal auf. Wenn Sie jedoch an einem größeren Scraping-Projekt arbeiten, das mehrere start_urls umfasst, ruft das Framework es automatisch auf für jeden von ihnen.

Nachdem sich Ihre Spinne nun bequem in ihrem neu aufgebauten Netz eingelebt hat, können Sie sie losschicken, um Daten aus dem Internet abzurufen.

Extrahieren Sie Daten von der Website

Sie haben Ihre Spinne fertig zusammengesetzt. Um es auszuführen und die gescrapten Daten anzuzeigen, öffnen Sie Ihr Terminal und bestätigen Sie, dass Sie sich noch in Ihrem Scrapy-Projektverzeichnis befinden. Von dort aus können Sie den Spider mit dem Befehl crawl starten:

(venv) $ scrapy crawl book

Scrapy beginnt mit dem Crawlen der angegebenen URL. Es wird eine Reihe von Protokollierungsinformationen auf Ihrem Terminal ausdrucken. Zwischen den Protokollen sollten Sie auch die extrahierten Daten für jedes Buch auf der Landingpage ausgedruckt sehen:

2024-08-28 10:26:48 [scrapy.utils.log] INFO: Scrapy 2.11.2 started (bot: books)
...
{'price': '£51.77',
 'title': 'A Light in the Attic',
 'url': 'catalogue/a-light-in-the-attic_1000/index.html'}
2024-08-28 10:26:50 [scrapy.core.scraper] DEBUG: Scraped from <200 https://books.toscrape.com/>
{'price': '£53.74',
 'title': 'Tipping the Velvet',
 'url': 'catalogue/tipping-the-velvet_999/index.html'}
2024-08-28 10:26:50 [scrapy.core.scraper] DEBUG: Scraped from <200 https://books.toscrape.com/>
...
2024-08-28 10:26:50 [scrapy.core.engine] INFO: Spider closed (finished)

Großartig! Sie haben erfolgreich alle URL-, Titel- und Preisinformationen von der ersten Seite des Dummy-Buchladens extrahiert! Das ist jedoch nur Seite eins von vielen.

Sie fragen sich vielleicht, ob Sie jede Seite zu start_url hinzufügen oder eine for-Schleife einrichten müssen, um alle verfügbaren Seiten zu durchlaufen. Keine Sorge, es gibt eine robustere und einfachere Möglichkeit, die bereits in das Framework integriert ist!

Behandeln Sie die Paginierung und folgen Sie URLs

Viele Websites zeigen Daten auf mehreren Seiten an. Wenn Ihr Spider mit Paginierung umgehen und URLs folgen kann, kann er durch mehrere Seiten einer Website navigieren und von jeder Seite Daten extrahieren. In Scrapy müssen Sie Ihrem Spider nur ein wenig Code hinzufügen, damit er die Paginierung bewältigen und Ihre Zielwebsite ordnungsgemäß crawlen kann.

Unter Web-Crawling versteht man das systematische Durchsuchen des Webs, um Informationen zu indizieren und zu sammeln. Die Hauptfunktion eines Webcrawlers besteht darin, Links auf Webseiten zu folgen, um neue Inhalte zu entdecken und Daten zu sammeln. Beim Web Scraping kann ein Crawler durch verschiedene Seiten einer Website navigieren, relevante Informationen extrahieren und zur weiteren Verwendung speichern.

Doch bevor Sie Ihren Spider aktualisieren können, müssen Sie verstehen, wie die Website mit der Paginierung umgeht. Öffnen Sie Ihren Browser oder die Scrapy-Shell und überprüfen Sie die Website, um die Paginierungssteuerelemente zu finden.

Auf der Books to Scrape-Website finden Sie die Paginierungslinks unten auf der Seite:

<li class="next"><a href="catalogue/page-2.html">next</a></li>

Die Schaltfläche Weiter führt zur nächsten Ergebnisseite. Es wird als Listenelement (<li>) mit einer Klasse namens "next" implementiert, die ein Linkelement (<a>) mit enthält die URL der nächsten Seite. Wenn Sie darauf klicken, werden Sie sehen, dass die Website die nächste Seite lädt und verschiedene Buchelemente anzeigt.

Gehen Sie mit diesem Wissen im Hinterkopf zurück zu book.py und ändern Sie Ihren vorhandenen BookSpider, um die Paginierung zu handhaben und Daten von allen Ergebnisseiten zu extrahieren:

import scrapy

from books.items import BooksItem

class BookSpider(scrapy.Spider):
    name = "book"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/"]

    def parse(self, response):
        for book in response.css("article.product_pod"):
            item = BooksItem()
            item["url"] = book.css("h3 > a::attr(href)").get()
            item["title"] = book.css("h3 > a::attr(title)").get()
            item["price"] = book.css(".price_color::text").get()
            yield item

        next_page = response.css("li.next > a::attr(href)").get()
        if next_page:
            next_page_url = response.urljoin(next_page)
            yield scrapy.Request(url=next_page_url, callback=self.parse)

Sie haben damit begonnen, das Attribut href des relevanten Elements als Ziel festzulegen und seinen Wert in next_page zu speichern. Da es sich bei dieser URL jedoch nicht um eine vollständig qualifizierte URL handelt, müssen Sie sie mit der Basis-URL kombinieren, bevor Scrapy eine neue Anfrage senden kann. Sie tun dies mit .urljoin() innerhalb des bedingten Blocks. Schließlich erzeugen Sie ein scrapy.Request-Objekt, übergeben ihm die URL, die Sie gerade verkettet haben, und verwenden die Methode .parse() als Rückruf.

Dieses Setup ermöglicht es dem Framework, eine weitere Anfrage an die zweite Seite des Buchladens zu stellen, wiederum unter Verwendung der von Ihnen geschriebenen Methode .parse(), wodurch der Scraping-Prozess fortgesetzt wird. Der Prozess wird rekursiv fortgesetzt, bis keine nächsten-Links mehr vorhanden sind, wodurch effektiv alle Seiten der Website gelöscht werden.

Ihr Spider ist nun für die Paginierung eingerichtet. Sie können es erneut ausführen, um Daten von allen Seiten zu extrahieren:

(venv) $ scrapy crawl book

Scrapy beginnt auf der ersten Seite, extrahiert die gewünschten Daten, folgt den Paginierungslinks und fährt mit dem Extrahieren von Daten von nachfolgenden Seiten fort, bis es die letzte Seite erreicht. Die Ausgabe in Ihrem Terminal sieht ähnlich aus wie zuvor, dauert jedoch länger und liefert Ihnen die Buchinformationen für alle Bücher, die im Buchladen vorhanden sind!

Der Umgang mit Paginierung und das Verfolgen von URLs sind wesentliche Fähigkeiten für die Entwicklung effektiver Webcrawler. Durch die Integration der Paginierungslogik in Ihren Spider stellen Sie sicher, dass Ihr Web Scraper durch mehrere Seiten navigieren und umfassende Daten von der Zielwebsite extrahieren kann. Mit diesem Ansatz können Sie leistungsfähigere und flexiblere Scraper erstellen, die eine Vielzahl von Websites verarbeiten können.

Speichern Sie die Scraped-Daten in MongoDB

Sie haben einen funktionierenden Spider erstellt, der den gesamten Buchladen durchsucht und den Paginierungslinks folgt, um alle benötigten Daten zu extrahieren. Derzeit sendet Scrapy diese Daten an den Standardfehlerstrom (stderr), den Sie in Ihrem Terminal sehen können.

Sie könnten Code schreiben, um ihn in einer JSON-Datei zu speichern, aber Sie erstellen einen Scraper, der in der Lage sein sollte, große Datenmengen zu verarbeiten und noch lange in der Zukunft zu funktionieren.

Aus diesem Grund ist es normalerweise eine gute Idee, die von Ihnen erfassten Daten in einer Datenbank zu speichern. Eine Datenbank kann Ihnen dabei helfen, die Informationen sicher und bequem zu organisieren und aufzubewahren. MongoDB ist aufgrund seiner Flexibilität und Fähigkeit zur Verarbeitung dynamischer und halbstrukturierter Daten eine ausgezeichnete Wahl für die Verarbeitung von Daten, die aus dem Internet stammen. In diesem Abschnitt erfahren Sie, wie Sie die Scraped-Daten in einer MongoDB-Sammlung speichern können.

Richten Sie eine MongoDB-Sammlung auf Ihrem Computer ein

Bevor Sie MongoDB verwenden können, müssen Sie es auf Ihrem System installieren. Der Installationsprozess unterscheidet sich je nach Betriebssystem. Befolgen Sie daher unbedingt die für Ihr Betriebssystem geeigneten Anweisungen.

Nachdem Sie MongoDB erfolgreich installiert haben, können Sie Ihre Installation mit Ihrem Terminal überprüfen:

$ mongod --version
db version v7.0.12
Build Info: {
    "version": "7.0.12",
    "gitVersion": "b6513ce0781db6818e24619e8a461eae90bc94fc",
    "modules": [],
    "allocator": "system",
    "environment": {
        "distarch": "aarch64",
        "target_arch": "aarch64"
    }
}

Die genaue Ausgabe, die Sie erhalten, hängt von der von Ihnen installierten Version sowie Ihrem Betriebssystem ab. Aber es wird ähnlich wie die Ausgabe aussehen, die Sie oben sehen können. Wenn Ihr Terminal stattdessen eine Fehlermeldung anzeigt, müssen Sie Ihre Installation noch einmal überprüfen und es erneut versuchen.

Wenn Sie MongoDB zum ersten Mal verwenden, müssen Sie möglicherweise einige Einrichtungsaufgaben durchführen, um es für die Datenspeicherung vorzubereiten.

Standardmäßig speichert MongoDB Daten in /data/db. Möglicherweise müssen Sie dieses Verzeichnis erstellen und sicherstellen, dass es über die richtigen Berechtigungen verfügt. Zum Beispiel:

$ sudo mkdir -p /data/db
$ sudo chown -R `id -u` /data/db

Wenn MongoDB noch nicht ausgeführt wird, können Sie es mit dem Befehl mongod starten. Dadurch wird der MongoDB-Server gestartet und an den Standardport 27017 gebunden.

Um mit Ihrer MongoDB-Instanz zu interagieren, können Sie die MongoDB-Shell verwenden, indem Sie mongosh in Ihrem Terminal ausführen. Dadurch werden Sie mit dem MongoDB-Server verbunden und können Datenbankoperationen ausführen:

$ mongosh
Current Mongosh Log ID: 66868598a3dbed30a11bb1a2
Connecting to:          mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.10
Using MongoDB:          7.0.12
Using Mongosh:          2.2.10

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

test>

In der MongoDB-Shell möchten Sie eine neue Datenbank mit einer neuen Sammlung erstellen. Sie rufen die Datenbank books_db und die Sammlung books auf:

test> use books_db
switched to db books_db
books_db> db.createCollection("books")
{ ok: 1 }
books_db> show collections
books
books_db>

Sobald Ihre neue Datenbank und Sammlung eingerichtet ist, können Sie wieder in Scrapy eintauchen, um Ihren Spider mit MongoDB zu verbinden. Sie richten es so ein, dass alle gelöschten Daten in Ihre neue Bücher-Sammlung gelangen.

Stellen Sie über Scrapy eine Verbindung zu einer MongoDB-Datenbank her

Sie verwenden die Drittanbieterbibliothek pymongo, um innerhalb Ihres Scrapy-Projekts eine Verbindung zu Ihrer MongoDB-Datenbank herzustellen. Zuerst müssen Sie pymongo von PyPI installieren:

(venv) $ python -m pip install pymongo

Nachdem die Installation abgeschlossen ist, können Sie Informationen zu Ihrer MongoDB-Instanz zu Ihrem Scrapy-Projekt hinzufügen.

Als Sie Ihr Projekt erstellt haben, haben Sie auch eine Datei mit dem Namen settings.py erhalten. Es ist ein zentraler Ort, an dem Sie Einstellungen für Ihr Projekt festlegen können und der bereits einige Informationen enthält. Es enthält auch viele Hinweise, für welche zusätzlichen Einstellungen Sie es verwenden können.

Ein Anwendungsfall für settings.py besteht darin, das Web-Scraping-Framework mit einer Datenbank zu verbinden. Öffnen Sie settings.py und fügen Sie die MongoDB-Verbindungsdetails am Ende der Datei hinzu:

# ...

MONGO_URI = "mongodb://localhost:27017"
MONGO_DATABASE = "books_db"

Später laden Sie diese Variablen in Ihre Pipeline und verwenden sie, um eine Verbindung zur books_db-Datenbank herzustellen, die auf Ihrem lokalen Computer ausgeführt wird. Wenn Sie eine Verbindung zu einer gehosteten MongoDB-Instanz herstellen, müssen Sie die Informationen entsprechend anpassen.

Verarbeiten Sie die Daten über eine Scrapy-Pipeline

Die Item-Pipelines von Scrapy sind eine leistungsstarke Funktion, die es Ihnen ermöglicht, die gescrapten Daten zu verarbeiten, bevor Sie sie speichern oder weiterverarbeiten. Pipelines erleichtern verschiedene Nachbearbeitungsaufgaben wie Reinigung, Validierung und Speicherung. In diesem Abschnitt erstellen Sie eine Elementpipeline zum Speichern von Scraped-Daten in Ihrer neuen MongoDB-Sammlung, die den load-Prozess eines ETL-Workflows widerspiegelt.

Eine Item-Pipeline ist eine Python-Klasse, die mehrere Methoden zur Verarbeitung von Elementen definiert, nachdem Ihr Spider sie aus dem Internet entfernt hat:

  • .open_spider() wird aufgerufen, wenn der Spider geöffnet wird.
  • .close_spider() wird aufgerufen, wenn der Spider geschlossen wird.
  • .process_item() wird für jede Elementpipelinekomponente aufgerufen. Es muss entweder ein Element zurückgeben oder eine DropItem-Ausnahme auslösen.
  • .from_crawler() erstellt eine Pipeline aus einem Crawler, um der Pipeline die allgemeinen Projekteinstellungen zur Verfügung zu stellen.

Sie implementieren alle diese Methoden beim Erstellen Ihrer benutzerdefinierten Pipeline, um die gescrapten Elemente in Ihre MongoDB-Sammlung einzufügen. Während das Laden von Daten in eine Datenbank ein möglicher Anwendungsfall einer Item-Pipeline ist, können Sie sie auch für andere Zwecke verwenden, beispielsweise zum Bereinigen von HTML-Daten oder zum Validieren von Scraped-Daten.

Sie haben MongoDB und pymongo bereits installiert, eine Sammlung erstellt und die MongoDB-spezifischen Einstellungen zur Datei settings.py Ihres Projekts hinzugefügt. Als Nächstes definieren Sie eine Pipeline-Klasse in books/pipelines.py. Diese Pipeline stellt eine Verbindung zu MongoDB her und fügt die gelöschten Elemente in die Sammlung ein:

import pymongo
from itemadapter import ItemAdapter

class MongoPipeline:
    COLLECTION_NAME = "books"

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get("MONGO_URI"),
            mongo_db=crawler.settings.get("MONGO_DATABASE"),
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        self.db[self.COLLECTION_NAME].insert_one(ItemAdapter(item).asdict())
        return item

Sie haben einen scheinbar großen Codeblock hinzugefügt. Das meiste davon ist jedoch Boilerplate-Code. Hier erfahren Sie, was jedes Stück bewirkt:

  • itemadapter umschließt verschiedene Datencontainer, um sie auf einheitliche Weise zu verarbeiten. Das Paket wurde als Abhängigkeit von Scrapy installiert.

  • COLLECTION_NAME gibt den Namen der MongoDB-Sammlung an, in der Sie die Elemente speichern möchten. Dieser sollte mit dem Namen der Sammlung übereinstimmen, die Sie zuvor eingerichtet haben.

  • .__init__() initialisiert die Pipeline mit dem MongoDB-URI und dem Datenbanknamen. Sie können auf diese Informationen zugreifen, weil Sie sie mithilfe der Klassenmethode .from_crawler() vom Crawler abrufen.

  • .from_crawler() ist eine Klassenmethode, die Ihnen Zugriff auf alle Kernkomponenten von Scrapy, wie z. B. die Einstellungen, ermöglicht. In diesem Fall verwenden Sie es, um die MongoDB-Einstellungen aus settings.py über den Scrapy-Crawler abzurufen.

  • .open_spider() öffnet eine Verbindung zu MongoDB, wenn der Spider startet.

  • .close_spider() schließt die MongoDB-Verbindung, wenn der Spider fertig ist.

  • .process_item() fügt jedes gelöschte Element in die MongoDB-Sammlung ein. Diese Methode enthält normalerweise die Kernfunktionalität einer Pipeline.

Wenn Ihre MongoPipeline vorhanden ist, müssen Sie sie noch im Kontext Ihres Projekts aktivieren, damit das Framework sie bei der nächsten Ausführung verwendet. Um die Pipeline zu aktivieren, müssen Sie sie zu Ihren Projekteinstellungen in settings.py hinzufügen.

Wenn Sie mit scrapy startproject ein Scrapy-Projektgerüst generieren, enthält Ihre Datei settings.py bereits einen Eintrag für ITEM_PIPELINES. Wie die meisten Einstellungen in dieser Datei ist sie auskommentiert. Sie können diesen Eintrag finden, den Python-Kommentar entfernen und dann den qualifizierten Namensspeicherort Ihres neuen Pipeline-Objekts hinzufügen:

ITEM_PIPELINES = {
    "books.pipelines.MongoPipeline": 300,
}

Sie können Ihrem Projekt Pipelines als Einträge in einem Wörterbuch hinzufügen, wobei der qualifizierte Name Ihrer Pipeline-Klasse der Schlüssel und eine Ganzzahl der Wert ist. Die Ganzzahl bestimmt die Reihenfolge, in der Scrapy die Pipelines ausführt. Niedrigere Zahlen bedeuten eine höhere Priorität.

Durch die Definition einer Item-Pipeline können Sie sowohl transform- als auch load-Vorgänge nahtlos in Ihre Web-Scraping-Aufgaben integrieren. Nachdem Sie Ihre MongoDB-Verbindung in Ihrem Projekt eingerichtet haben, ist es an der Zeit, den Scraper ein weiteres Mal auszuführen und zu beurteilen, ob er wie erwartet funktioniert.

Vermeiden Sie das Hinzufügen doppelter Einträge

Wenn Sie Ihren Schaber zum ersten Mal in Betrieb nehmen, sollte alles wie erwartet funktionieren. Scrapy füllt Ihre MongoDB-Sammlung mit den Informationen von der Website. Wenn Sie die Länge Ihrer Sammlung nach der ersten Ausführung in der Mongo-Shell überprüfen, werden Sie feststellen, dass sie 1000 Dokumente enthält:

books_db> db.books.countDocuments()
1000

Wenn Sie den Scraper jedoch noch einmal ausführen, hat sich die Länge Ihrer Buchdatenbank verdoppelt und enthält nun zweitausend Dokumente. Dieses Problem besteht, weil MongoDB standardmäßig eine eindeutige ID für jedes Dokument erstellt, die unter anderem auf Zeitstempeln basiert. Daher wird jedes gelöschte Element als neues Dokument betrachtet und ihm eine neue ID zugewiesen, was zu einer Datenduplizierung in Ihrer Datenbank führt.

Dies mag in manchen Projekten gewünscht sein, aber hier möchten Sie jedes gescrapte Buchelement nur einmal behalten, auch wenn Sie den Scraper mehrmals ausführen. Dazu müssen Sie ein eindeutiges ID-Feld für Ihre Daten erstellen und angeben, das MongoDB zur Identifizierung jedes Dokuments verwenden kann.

Öffnen Sie books/pipelines.py erneut und fügen Sie die Logik hinzu, die erforderlich ist, um anhand eines neuen eindeutigen id-Felds, das Sie aus dem Hashing der einzelnen Seiten-URL ableiten, nach Duplikaten zu suchen:

import hashlib
import pymongo
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem

class MongoPipeline:
    COLLECTION_NAME = "books"

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get("MONGO_URI"),
            mongo_db=crawler.settings.get("MONGO_DATABASE"),
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        item_id = self.compute_item_id(item)
        if self.db[self.COLLECTION_NAME].find_one({"_id": item_id}):
            raise DropItem(f"Duplicate item found: {item}")
        else:
            item["_id"] = item_id
            self.db[self.COLLECTION_NAME].insert_one(ItemAdapter(item).asdict())
            return item

    def compute_item_id(self, item):
        url = item["url"]
        return hashlib.sha256(url.encode("utf-8")).hexdigest()

Um das Sammeln doppelter Elemente bei mehreren Scraper-Läufen zu vermeiden, haben Sie Aktualisierungen auf .process_item() angewendet und die neue Hilfsmethode .compute_item_id() hinzugefügt:

  • Zeilen 1 und 4 enthalten zwei neue Importe, einen für das Standardbibliotheksmodul hashlib, das Sie zum Verarbeiten der URLs in eindeutige Bezeichner in .compute_item_id() verwenden. . Der zweite Import bringt DropItem von scrapy.Exceptions, was eine schnelle Möglichkeit ist, doppelte Elemente zu löschen und in die Protokolle von Scrapy zu schreiben.

  • Zeile 28 ruft .compute_item_id() auf und weist die gehashte Ausgabe item_id zu.

  • Zeilen 29 und 30 fragen die MongoDB-Sammlung ab, um zu prüfen, ob ein Element mit derselben _id bereits vorhanden ist. Wenn Python ein Duplikat findet, löst der Code eine DropItem-Ausnahme aus, die das Framework anweist, dieses Element zu verwerfen und nicht weiter zu verarbeiten. Wenn kein Duplikat gefunden wird, fährt es mit den nächsten Schritten fort.

  • Zeilen 31 bis 34 bilden die else-Bedingung, bei der das gelöschte Element noch nicht in der Datenbank vorhanden ist. Dieser Code ist nahezu der Code, den Sie vor dem Hinzufügen der Deduplizierungslogik hatten, allerdings mit einem wichtigen Unterschied. Bevor Sie das Element in Ihre Sammlung einfügen, fügen Sie die berechnete item_id als Wert für das Attribut ._id zu Ihrem Item hinzu. MongoDB verwendet ein Feld namens _id als Primärschlüssel, sofern vorhanden.

Beachten Sie, dass die Methode wie zuvor das Element zurückgibt. Dies ermöglicht die weitere Verarbeitung durch andere Pipelines, falls Sie welche erstellen.

Bevor Sie Ihre aktualisierte Pipeline ausführen können, müssen Sie das neue Feld _id in einem BooksItem berücksichtigen. Öffnen Sie books/items.py und fügen Sie es Ihrer Artikeldefinition hinzu:

import scrapy

class BooksItem(scrapy.Item):
    _id = scrapy.Field()
    url = scrapy.Field()
    title = scrapy.Field()
    price = scrapy.Field()

Sie müssen nichts an Ihrem Spider ändern und füllen jetzt jedes BooksItem an zwei verschiedenen Stellen in Ihrem Code auf:

  1. Ihr Spider fügt Werte für .url, .title und .price zu einem BooksItem hinzu.
  2. Ihre Artikelpipeline fügt den Wert für ._id hinzu, nachdem sie ihn aus .url berechnet hat.

Das ist ein schönes Beispiel für die Leistungsfähigkeit und Flexibilität, die Ihnen die Verwendung der integrierten Prozesse von Scrapy beim Aufbau Ihres Web Scrapers bietet.

Wie kurz erwähnt, verwendet .compute_item_id() Pythons hashlib, um den URL-Wert zu hashen. Die Verwendung der gehashten URL als eindeutige Kennung bedeutet, dass Python Ihrer Sammlung kein Buch hinzufügt, das Sie zweimal von derselben URL extrahiert haben.

Was Sie als eindeutige Kennung betrachten, ist eine Designentscheidung, über die Sie für jedes Projekt individuell nachdenken müssen. In diesem Fall funktioniert die Verwendung der gehashten URL gut, da Sie mit einem Dummy-Buchladen arbeiten, in dem jede Buchressource eine eindeutige URL hat. Wenn sich jedoch die Daten für eines dieser Bücher ändern würden – und dabei die gleiche URL beibehalten wird –, würde die aktuelle Implementierung dieses Element löschen.

Wenn das Löschen des Elements in einem solchen Fall nicht das beabsichtigte Verhalten ist, können Sie Ihren Code stattdessen aktualisieren, indem Sie einen Upsert-Vorgang verwenden. In MongoDB können Sie Upserts mit db.collection.updateOne() durchführen, wobei upsert auf „true“ gesetzt ist. pymongo übersetzt diesen Vorgang in Python-Code:

# ...

    def process_item(self, item, spider):
        item_id = self.compute_item_id(item)
        item_dict = ItemAdapter(item).asdict()

        self.db[self.COLLECTION_NAME].update_one(
            filter={"_id": item_id},
            update={"$set": item_dict},
            upsert=True
        )

        return item

# ...

Wenn Sie dieses Setup verwenden, müssen Sie nicht einmal Ihr BooksItem aktualisieren, da es den Elementadapter umgeht und die _id des Dokuments direkt erstellt, wenn es mit MongoDB interagiert.

Wenn Sie .update_one() mit pymongo verwenden, müssen Sie zwei Hauptargumente übergeben:

  1. Der Filter, um das Dokument in Ihrer Sammlung zu finden
  2. Der Aktualisierungsvorgang

Das dritte Argument, das Sie angeben, upsert=True, stellt sicher, dass MongoDB das Element einfügt, wenn es nicht vorhanden ist. Wie bereits erwähnt, müssen Sie den Wert für _id nicht explizit zu Ihrem BooksItem hinzufügen, da .update_one() diesen Wert von übernimmt Filterargument und verwendet es nur zum Erstellen der Dokumente in Ihrer Sammlung.

Sie müssen Ihre bestehende Sammlung löschen, die noch die zeitstempelbasierten IDs enthält, bevor dieser Code die beabsichtigte Wirkung hat:

books_db> db.books.drop()
true
books_db> db.books.countDocuments()
0

Jetzt können Sie Ihren Scraper noch ein paar Mal laufen lassen und dann überprüfen, wie viele Dokumente Sie in Ihrer Sammlung haben. Die Zahl sollte immer bei tausend bleiben.

Bedenken Sie, dass der von Ihnen gewählte Ansatz möglicherweise von dem Projekt abhängt, an dem Sie arbeiten. So oder so haben Sie jetzt eine bessere Vorstellung davon, wie Sie doppelte Elemente löschen und vorhandene Einträge mit neuen Daten aktualisieren können.

Debuggen und testen Sie Ihren Scrapy Web Scraper

Sie werden wahrscheinlich in Situationen geraten, in denen Ihr Schaber nicht das tut, was Sie von ihm erwarten. Web-Scraping-Projekte haben immer mit mehreren Faktoren zu tun, die zu unerwarteten Ergebnissen führen können. Das Debuggen wird wahrscheinlich ein wesentlicher Bestandteil Ihres Scraper-Entwicklungsprozesses sein. Scrapy bietet mehrere leistungsstarke Tools, die Ihnen beim Debuggen Ihrer Spider helfen und sicherstellen, dass sie wie vorgesehen funktionieren.

Das Schreiben von Tests für Ihren Spider stellt sicher, dass Ihr Code weiterhin wie erwartet funktioniert. Tests sind ein entscheidender Teil jedes Softwareentwicklungsprozesses und Web Scraping bildet da keine Ausnahme.

In diesem Abschnitt erfahren Sie, wie Sie die Debugging- und Testtools von Scrapy verwenden, einschließlich der Protokollierungsfunktion, der Scrapy-Shell, einer Fehlerrückruffunktion und Spider-Verträgen. Sie schreiben außerdem einige benutzerdefinierte Unit-Tests, um zu bestätigen, dass Ihr Spider wie erwartet funktioniert.

Protokollieren Sie Informationen mit dem Logger

Die Protokollierung ist ein grundlegendes Werkzeug, um zu verstehen, was Ihre Spinne bei jedem Schritt tut. Standardmäßig protokolliert das Framework verschiedene Ereignisse, Sie können die Protokollierung jedoch an Ihre Bedürfnisse anpassen. Scrapy verwendet die logging-Bibliothek von Python, die Teil der Standardbibliothek ist. Sie müssen es nicht einmal importieren, da Spider-Objekte bereits Zugriff auf einen Logger haben.

Öffnen Sie Ihre Datei book.py und fügen Sie eine Protokollierung hinzu, um zu verfolgen, wann Ihr Spider zu einer neuen Seite navigiert:

# ...

    def parse(self, response):
        for book in response.css("article.product_pod"):
            item = BooksItem()
            item["url"] = book.css("h3 > a::attr(href)").get()
            item["title"] = book.css("h3 > a::attr(title)").get()
            item["price"] = book.css(".price_color::text").get()
            yield item

        next_page = response.css("li.next > a::attr(href)").get()
        if next_page:
            next_page_url = response.urljoin(next_page)
            self.logger.info(
                f"Navigating to next page with URL {next_page_url}."
            )
            yield scrapy.Request(url=next_page_url, callback=self.parse)

# ...

Sie haben Code hinzugefügt, der von Ihrem BookSpider auf den .logger zugreift und Informationen protokolliert, wenn der Spider zu einer neuen Seite mit der Protokollierungsebene INFO navigiert.

Standardmäßig protokolliert das Framework den Schweregrad DEBUG, was beim Debuggen hilfreich sein kann, aber möglicherweise überwältigend ist, wenn es bei jeder Ausführung angezeigt wird. Nachdem Sie nun eine Debug-Meldung mit einem höheren Schweregrad hinzugefügt haben, können Sie zu dieser Ebene wechseln, um nur Meldungen mit INFO und einem höheren Schweregrad auf Ihrer Konsole anzuzeigen.

Sie können die Protokollierung in Ihrem Spider direkt über Konstanten in settings.py konfigurieren:

# ...

LOG_LEVEL = "INFO"

Nachdem Sie diese Zeile zu settings.py hinzugefügt haben, führen Sie Ihren Scraper ein weiteres Mal aus. Sie sehen keine Debug-Informationen, sondern nur Protokolle mit einem Schweregrad von INFO und höher.

Wenn Sie den Spider ausführen, während Sie eine leere Sammlung füllen, erhalten Sie nur die Information, dass Scrapy zu einer anderen Seite navigiert ist.

Wenn Sie es jedoch ein zweites Mal ausführen, sehen Sie eine andere Ausgabe, je nachdem, welche Deduplizierungslogik Sie in MongoPipeline.process_item() beibehalten möchten:

  • Wenn Sie mit Upserts und .update_one() von Pymongo arbeiten, werden nur die Protokollmeldungen angezeigt, wenn Scrapy zu einer neuen Seite navigiert.

  • Wenn Sie immer noch mit einem expliziten Feld ._id und DropItem arbeiten, werden Sie mit WARNING-Protokollen überschwemmt. Scrapy schreibt automatisch eine WARNUNG, wenn es eine DropItem-Ausnahme abfängt.

Es ist wahrscheinlich eine gute Idee, alle Warnungen zu protokollieren, und Sie möchten die Protokolldatei möglicherweise für eine spätere Überprüfung aufbewahren. Sie können in den Scrapy-Einstellungen einen Wert für LOG_FILE festlegen, um in eine Datei statt in stderr zu schreiben, und den LOG_LEVEL erneut anpassen, sodass Python nur Warnungen in die Protokolldatei schreibt:

# ...

LOG_LEVEL = "WARNING"
LOG_FILE = "book_scraper.log"

Mit diesen beiden Einstellungen haben Sie die Protokollierungsstufe und den Speicherort der Protokolldatei in Ihrem Scrapy-Projekt angepasst. Wenn Sie LOG_LEVEL auf "WARNING" setzen, bedeutet dies, dass Python nur Meldungen mit höherem Schweregrad protokolliert. Die Einstellung LOG_FILE gibt die Datei an, in der Scrapy die Protokolle speichert.

Wenn Sie jetzt Ihren Spider ausführen, wird in Ihrer Konsole keine Ausgabe angezeigt. Stattdessen erscheint eine neue Datei, in die Scrapy Informationen über alle abgelegten Elemente schreibt – vorausgesetzt, Sie arbeiten mit dem Code, der DropItem verwendet:

2024-08-28 13:11:32 [scrapy.core.scraper] WARNING: Dropped: Duplicate item found: {'price': '£51.77',
 'title': 'A Light in the Attic',
 'url': 'catalogue/a-light-in-the-attic_1000/index.html'}
{'price': '£51.77',
 'title': 'A Light in the Attic',
 'url': 'catalogue/a-light-in-the-attic_1000/index.html'}
...

Wenn Sie mit Upserts arbeiten, sollte eine leere Protokolldatei angezeigt werden. Gut zu wissen, dass nichts schief gelaufen ist!

Die Protokollierung kann Ihnen helfen, den Fluss Ihres Spiders zu verfolgen, Variablenwerte zu überprüfen und herauszufinden, wo möglicherweise etwas schiefläuft. Es ist oft eine gute Idee, die Protokollierungsstufe auf DEBUG zu setzen und Protokolle in eine Datei zu schreiben, damit Sie nachvollziehen können, was Ihr Spider getan hat, falls er bei einem Lauf fehlschlägt.

Behandeln Sie Fehler mit errback

Mit Scrapy können Sie Fehler mithilfe einer Fehlerrückruffunktion elegant behandeln. Wenn Sie eine Methode an den Parameter errback eines Request-Objekts übergeben, verwendet Scrapy diese zur Behandlung von Ausnahmen. Dies ist besonders hilfreich, wenn es um Netzwerkfehler oder unerwartete Serverantworten geht, und Sie können damit den Fehler protokollieren, ohne Ihr Programm zu beenden.

Öffnen Sie book.py und fügen Sie Ihrem Spider eine neue Methode zur Fehlerbehandlung hinzu:

import scrapy

from books.items import BooksItem

class BookSpider(scrapy.Spider):
    # ...

    def log_error(self, failure):
        self.logger.error(repr(failure))

In .log_error() fangen Sie die Ausnahme ab und protokollieren sie mit dem Schweregrad ERROR. Dies kann Ihnen bei der Diagnose von Problemen helfen, ohne die Ausführung des Spiders zu stoppen.

Als nächstes müssen Sie Ihre neue Methode in den Code integrieren. Um Fehler abzufangen, die beim Senden von Anfragen auftreten, müssen Sie zusätzlich zu callback den Parameter errback angeben. Auf diese Weise können Sie sicherstellen, dass Scrapy Fehler mithilfe Ihrer Methode .log_error() behandelt:

# ...

def parse(self, response):
  # ...

  if next_page:
      next_page_url = response.urljoin(next_page)
      self.logger.info(
          f"Navigating to next page with URL {next_page_url}."
      )
      yield scrapy.Request(
          url=next_page_url,
          callback=self.parse,
          errback=self.log_error,
      )

Indem Sie den Namen Ihrer Fehlerrückrufmethode an scrapy.Request übergeben, stellen Sie sicher, dass alle Anfragen für weitere Seiten des Buchladens, die einen Fehler zurückgeben, protokolliert werden.

Wenn Sie auch .log_error() für die erste Anfrage an Books to Scrape verwenden möchten, müssen Sie Ihren Code umgestalten, um .start_requests() zu verwenden, anstatt sich nur darauf zu verlassen auf .start_urls:

# ...

class BookSpider(scrapy.Spider):
    name = "book"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url, callback=self.parse, errback=self.log_error
            )

    def parse(self, response):
        # ...

Durch das Hinzufügen von .start_requests() erhalten Sie zusätzliche Flexibilität bei der Handhabung der ersten Anfrage an jede URL in start_urls. Scrapy ruft diese Methode nur einmal für jede Start-URL auf. In diesem Beispiel haben Sie Ihre neue Methode .log_error() auch zu dieser ersten Anfrage hinzugefügt, indem Sie sie an errback übergeben haben. Wenn Sie Ihre eigene Fehlerbehandlung mit errback definieren, können Sie Fehlerbedingungen in Ihren Web-Scraping-Spidern effizient beheben und verfeinern.

Nachdem Sie einige von Scrapy angebotene Debugging-Tools kennengelernt haben, sehen Sie sich nun an, wie Sie Tests schreiben können, um sicherzustellen, dass Ihr Scraper wie vorgesehen funktioniert.

Unterzeichnen Sie einige Spider-Verträge

Scrapy bietet eine Funktion namens Verträge, mit der Sie Testfälle direkt in die Dokumentzeichenfolgen Ihres Spiders einbetten können. Ähnlich wie Pythons doctest ermöglicht diese Funktion eine schnelle Validierung von Spider-Methoden und stellt sicher, dass sie wie erwartet funktionieren, ohne dass umfangreiche Test-Setups erforderlich sind.

Bei Spider-Verträgen handelt es sich im Wesentlichen um Behauptungen, die Sie in den Dokumentstring Ihrer Spider-Methoden einfügen können. Sie definieren das erwartete Verhalten der Methoden:

  • @url gibt die URL an, die Scrapy abrufen soll.
  • @returns gibt die erwartete Anzahl von Elementen oder Anfragen an, die der Spider zurückgeben soll.
  • @scrapes listet die Felder auf, die die Elemente enthalten sollen.

Öffnen Sie Ihre Datei book.py und fügen Sie einen Vertrag zu Ihrer Methode .parse() hinzu:

# ...

    def parse(self, response):
        """
        @url https://books.toscrape.com
        @returns items 20 20
        @returns request 1 50
        @scrapes url title price
        """
        for book in response.css("article.product_pod"):
            # ...

Sie haben der Dokumentzeichenfolge von .parse() vier Verträge hinzugefügt:

  1. @url https://books.toscrape.com weist Scrapy an, die angegebene URL abzurufen, um die Parse-Methode zu testen. Dieser Vertrag ist für die Ausführung aller anderen Tests erforderlich.
  2. @returns items 20 20 gibt an, dass die Methode genau zwanzig Elemente zurückgeben soll. Die erste Zahl gibt das erwartete Minimum an Elementen an, die zweite das Maximum.
  3. @returns request 1 50 bedeutet, dass .parse() mindestens eine und höchstens fünfzig Anfragen generieren sollte. Dieser Teil ist für den von Ihnen eingerichteten Paginierungscode relevant.
  4. @scrapes url title price gibt an, dass jeder zurückgegebene Artikel die Felder url, title und price enthalten soll.

Mit nur diesen vier Zeilen haben Sie grundlegende Tests für .parse() eingerichtet, die Ihnen dabei helfen können, Probleme mit Ihrem Scraper frühzeitig zu erkennen.

Um die Vertragstests auszuführen, können Sie den Befehl check in Ihrem Terminal verwenden:

(venv) $ scrapy check book
...
----------------------------------------------------------------------
Ran 3 contracts in 1.399s

OK

Beim Ausführen dieses Befehls ruft Scrapy die angegebene URL ab, führt .parse() aus und validiert die Ergebnisse anhand der Behauptungen im Dokumentstring. Wenn eine der Behauptungen fehlschlägt, gibt das Framework eine Fehlermeldung aus, die angibt, was schief gelaufen ist.

Beachten Sie, dass der @url-Vertrag eine Voraussetzung für die erfolgreiche Ausführung der anderen Verträge ist. Scrapy listet es in der Ausgabe nicht als Testbedingung auf, weshalb die Meldung lautet, dass Scrapy drei Verträge ausgeführt hat.

Sollte einer der Verträge scheitern, stellt Scrapy Details zum Scheitern bereit, beispielsweise die Anzahl der Artikel, die die angegebenen Erwartungen nicht erfüllten. Bearbeiten Sie Ihre Verträge, um zu sehen, wie ein fehlgeschlagener Test aussieht. Wenn Sie mit dem unittest-Modul von Python vertraut sind, werden Sie die Ausgabe wahrscheinlich erkennen. Unter der Haube verwenden Verträge unittest, um die Tests zu organisieren und auszuführen.

Kurz gesagt, Spider-Verträge bieten einige Vorteile, wie zum Beispiel:

  • Schnelle Validierung: Verträge bieten eine schnelle Möglichkeit, Ihre Spider zu validieren, ohne umfangreiche Test-Frameworks einzurichten. Dies ist besonders während der Entwicklung nützlich, wenn Sie sicherstellen möchten, dass Ihre Spider-Methoden ordnungsgemäß funktionieren.

  • Dokumentation: Die Dokumentzeichenfolgen mit Verträgen dienen als Dokumentation und machen das erwartete Verhalten Ihrer Spider-Methoden deutlich. Dies kann für Teammitglieder oder beim erneuten Durchsuchen des Codes nach einiger Zeit von Vorteil sein.

  • Automatisiertes Testen: Verträge können in Ihre automatisierte Testpipeline integriert werden, um sicherzustellen, dass Ihre Spider weiterhin ordnungsgemäß funktionieren, wenn Sie Änderungen an Ihrer Codebasis vornehmen.

Durch die Integration von Spider-Verträgen in Ihren Entwicklungsprozess können Sie die Zuverlässigkeit und Wartbarkeit Ihrer Web-Scraper verbessern und sicherstellen, dass sie unter verschiedenen Bedingungen wie erwartet funktionieren.

Schreiben Sie Unit-Tests für detaillierte Tests

Spider-Verträge sind eine großartige Möglichkeit, schnelle Tests zu erstellen. Wenn Sie jedoch mit einem komplexen Scraper arbeiten und dessen Funktionalität detaillierter überprüfen möchten, kann es eine gute Idee sein, Unit-Tests zu schreiben, um einzelne Komponenten zu testen.

Um Komponententests für die BookSpider-Methoden zu schreiben, müssen Sie die Scrapy-Umgebung simulieren. Dabei geht es um vorgetäuschte Antworten und Anfragen, um sicherzustellen, dass sich Ihre Spider-Methoden wie erwartet verhalten.

Erstellen Sie in Ihrem obersten Projektordner books/ einen Ordner tests/. Erstellen Sie in diesem Ordner zwei Dateien:

  • __init__.py initialisiert den Ordner als Python-Paket.
  • test_book.py speichert die Komponententests für Ihren Scraper.

Nehmen Sie sich jetzt einen Moment Zeit und überlegen Sie, welche Aspekte Ihres Schabers Sie testen möchten. Es kann sinnvoll sein, noch einmal zu überdenken, was Ihre Spider-Verträge bereits abdecken, und dann darauf mit zusätzlichen Spezifika aufzubauen.

Während es beispielsweise Verträge gibt, die die Anzahl der pro Seite gescrapten Elemente und die von Scrapy extrahierten Elemente überprüfen, gibt es keine Überprüfung, ob es sich dabei um die richtigen Elemente handelt. Ihre Verträge wissen nichts über den Inhalt der extrahierten Elemente. Es könnte eine gute Idee sein, einen Unit-Test zu schreiben, um diese Lücke zu schließen.

Darüber hinaus möchten Sie möglicherweise bestätigen, dass jeder Aufruf von .parse() die richtigen Elemente aus dem HTML identifiziert, alle Bücher abruft und eine neue Anfrage für die Paginierungs-URL erstellt:

import unittest

class BookSpiderTest(unittest.TestCase):

    def test_parse_scrapes_all_items(self):
        """Test if the spider scrapes books and pagination links."""
        pass

    def test_parse_scrapes_correct_book_information(self):
        """Test if the spider scrapes the correct information for each book."""
        pass

    def test_parse_creates_pagination_request(self):
        """Test if the spider creates a pagination request correctly."""
        pass

if __name__ == "__main__":
    unittest.main()

Nachdem Sie Ihre Absichten festgelegt und eine grundlegende Testklassenstruktur eingerichtet haben, können Sie mit dem Schreiben Ihres Testcodes beginnen. Ihre Tests profitieren oft von einer .setUp()-Methode, die die notwendigen Einstellungen erstellt, die jeder Test benötigt, um ordnungsgemäß zu funktionieren.

Für diese Unit-Tests möchten Sie eine kleine Stichprobe des HTML-Codes der Seite vergleichen. Sie benötigen außerdem Zugriff auf Ihren Book Spider und einige der Klassen, die das Framework intern zur Bearbeitung von Anfragen und Antworten verwendet. Da Sie dieses Setup für jeden Test benötigen, ist es ein guter Kandidat, in eine .setUp()-Methode zu wechseln:

import unittest
from scrapy.http import HtmlResponse
from books.spiders.book import BookSpider

class BookSpiderTest(unittest.TestCase):
    def setUp(self):
        self.spider = BookSpider()
        self.example_html = """
            Insert the example HTML here
        """
        self.response = HtmlResponse(
            url="https://books.toscrape.com",
            body=self.example_html,
            encoding="utf-8"
        )

# ...

Die Methode .setUp() initialisiert vor jedem Test eine BookSpider-Instanz und erstellt eine Beispiel-HTML-Zeichenfolge, die zwei Bucheinträge und einen Link zur nächsten Seite enthält. Anschließend simuliert es eine Anfrage an die Site, indem es ein HtmlResponse-Objekt mit diesem Beispiel-HTML zurückgibt. Sie möchten, dass der Beispiel-HTML dem realen Seiten-HTML so nahe wie möglich kommt, ohne Ihre Codebasis zu überfordern.

Sie können den Beispiel-HTML-Code, den dieses Tutorial verwendet, aus den herunterladbaren Materialien in books/tests/sample.html abrufen:

Dieser Auszug aus der Landingpage von Books to Scrape enthält zwei vollständige Buchelemente und den Paginierungslink, ist aber ansonsten gekürzt, damit die Zeichenfolge nicht unnötig lang wird. Fügen Sie dieses HTML-Snippet zwischen den dreifachen Anführungszeichen ein, um es self.example_html zuzuweisen.

Wenn Ihr Setup eingerichtet ist, können Sie Ihren ersten Test zum Scheitern bringen:

# ...

class BookSpiderTest(unittest.TestCase):
    # ...

    def test_parse_scrapes_all_items(self):
        """Test if the spider scrapes all books and pagination links."""
        # There should be two book items and one pagination request
        book_items = []
        pagination_requests = []

        self.assertEqual(len(book_items), 2)
        self.assertEqual(len(pagination_requests), 1)

Wenn Sie zu diesem Zeitpunkt Ihren Testcode ausführen, erhalten Sie einen fehlgeschlagenen und zwei bestandene Tests. Navigieren Sie in Ihr oberstes Verzeichnis books/. Wenn Sie noch nicht dort sind, führen Sie den Befehl unittest aus:

(venv) $ python -m unittest
.F.
======================================================================
FAIL: test_parse_scrapes_all_items (tests.test_book.BookSpiderTest.test_parse_scrapes_all_items)
Test if the spider scrapes all items including books and pagination links.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/books/tests/test_book.py", line 169, in test_parse_scrapes_all_items
    self.assertEqual(len(book_items), 2)
AssertionError: 0 != 2

----------------------------------------------------------------------
Ran 3 tests in 0.011s

FAILED (failures=1)

Die leeren Tests werden bestanden, da sie noch keine Behauptungen enthalten. Ihr .test_parse_scrapes_all_items() schlägt fehl, weil Sie die Methode .parse() noch nicht aufrufen und ihren Rückgabewert nicht verarbeiten. Springen Sie zurück in den Code und fügen Sie die fehlenden Teile hinzu, damit Ihr Test besteht:

# ...

from scrapy.http import HtmlResponse, Request
from books.items import BooksItem

class BookSpiderTest(unittest.TestCase):
    # ...

    def test_parse_scrapes_all_items(self):
        """Test if the spider scrapes all books and pagination links."""
        # Collect the items produced by the generator in a list
        # so that it's possible to iterate over it more than once.
        results = list(self.spider.parse(self.response))

        # There should be two book items and one pagination request
        book_items = [item for item in results if isinstance(item, BooksItem)]
        pagination_requests = [
            item for item in results if isinstance(item, Request)
        ]

        self.assertEqual(len(book_items), 2)
        self.assertEqual(len(pagination_requests), 1)

Sie beginnen mit dem Hinzufügen eines Imports für Request aus scrapy.http. Sie verwenden diese Klasse, um zu vergleichen, ob die Paginierungsanforderung, die das Framework generieren soll, Teil der Ergebnisse ist. Sie möchten außerdem bestätigen, dass Scrapy die Buchinformationen in einem BooksItem sammelt, also importieren Sie diese aus books.items.

Beachten Sie, dass .parse() ein Generator ist. Der Einfachheit halber sammeln Sie alle angezeigten Elemente in einer Liste. Anschließend fügen Sie zwei Listenverständnisse hinzu, die alle dicts- und Request-Objekte in den beiden Listen zusammenfassen, die Sie zuvor beim Einrichten der Gliederung dieser Testmethode erstellt haben. Der Beispiel-HTML-Code enthält zwei Buchelemente und eine Paginierungs-URL. Sie vergleichen diese also in Ihren Behauptungen.

Sie können unittest jetzt ein weiteres Mal ausführen und alle drei Tests sollten erfolgreich sein:

(venv) $ python -m unittest
...
----------------------------------------------------------------------
Ran 3 tests in 0.011s

Die beiden anderen Testmethoden können Sie mit einem ähnlichen Ansatz weiterentwickeln. Im zusammenklappbaren Abschnitt unten finden Sie eine Beispielimplementierung:

Sie können den folgenden Code kopieren und in Ihre Datei test_book.py einfügen:

import unittest

from scrapy.http import HtmlResponse, Request

from books.items import BooksItem
from books.spiders.book import BookSpider


class BookSpiderTest(unittest.TestCase):
    def setUp(self):
        self.spider = BookSpider()
        self.example_html = """
            Insert the example HTML here
        """
        self.response = HtmlResponse(
            url="https://books.toscrape.com",
            body=self.example_html,
            encoding="utf-8",
        )

    def test_parse_scrapes_all_items(self):
        """Test if the spider scrapes all books and pagination links."""
        # Collect the items produced by the generator in a list
        # so that it's possible to iterate over it more than once.
        results = list(self.spider.parse(self.response))

        # There should be two book items and one pagination request
        book_items = [
            item for item in results if isinstance(item, BooksItem)
        ]
        pagination_requests = [
            item for item in results if isinstance(item, Request)
        ]

        self.assertEqual(len(book_items), 2)
        self.assertEqual(len(pagination_requests), 1)

    def test_parse_scrapes_correct_book_information(self):
        """Test if the spider scrapes the correct information for each book."""
        results_generator = self.spider.parse(self.response)

        # Book 1
        book_1 = next(results_generator)
        self.assertEqual(
            book_1["url"], "catalogue/a-light-in-the-attic_1000/index.html"
        )
        self.assertEqual(book_1["title"], "A Light in the Attic")
        self.assertEqual(book_1["price"], "£51.77")

        # Book 2
        book_2 = next(results_generator)
        self.assertEqual(
            book_2["url"], "catalogue/tipping-the-velvet_999/index.html"
        )
        self.assertEqual(book_2["title"], "Tipping the Velvet")
        self.assertEqual(book_2["price"], "£53.74")

    def test_parse_creates_pagination_request(self):
        """Test if the spider creates a pagination request correctly."""
        results = list(self.spider.parse(self.response))
        next_page_request = results[-1]
        self.assertIsInstance(next_page_request, Request)
        self.assertEqual(
            next_page_request.url,
            "https://books.toscrape.com/catalogue/page-2.html",
        )


if __name__ == "__main__":
    unittest.main()

Beachten Sie, dass Sie den Wert für .example_html immer noch mit dem Beispiel-HTML aktualisieren müssen, das Sie in books/tests/sample.html in den herunterladbaren Ressourcen finden:

Dieses Setup stellt sicher, dass einzelne Komponenten des BookSpider auf Korrektheit getestet werden, was dazu beiträgt, Probleme frühzeitig im Entwicklungsprozess zu erkennen und zu beheben.

Bewältigen Sie häufige Web-Scraping-Herausforderungen

Web Scraping kann ein leistungsstarkes Tool sein, aber es ist oft nicht so einfach, wie Sie es sich erhoffen. Unabhängig davon, ob Sie mit dynamisch generierten Inhalten zu tun haben oder Anti-Scraping-Mechanismen umgehen müssen, ist es für den Aufbau eines robusten und zuverlässigen Web Scrapers unerlässlich, auf die Bewältigung dieser Hindernisse vorbereitet zu sein.

In diesem Abschnitt erkunden Sie einige der häufigsten Herausforderungen, denen Sie begegnen können. Außerdem lernen Sie einige praktische Lösungen kennen, um diese mithilfe von Scrapy und seinem umfangreichen Ökosystem zu überwinden.

Ausgestattet mit diesen Tools und Best Practices sind Sie besser darauf vorbereitet, robuste Scraper zu entwickeln, die auch von anspruchsvolleren Websites wertvolle Daten extrahieren können.

Versuchen Sie fehlgeschlagene Anforderungen erneut

Beim Web Scraping werden häufig zahlreiche Anfragen an Webserver gesendet. Einige dieser Anfragen können aufgrund von Netzwerkproblemen, Serverausfällen oder vorübergehenden Blockaden von der Zielseite zu Verbindungsfehlern führen. Diese Fehler können den Scraping-Prozess stören und zu einer unvollständigen Datenerfassung führen. Durch die Implementierung einer robusten Wiederholungslogik in Ihrem Projekt können Sie sicherstellen, dass vorübergehende Probleme nicht zum Ausfall Ihres Spiders führen.

Scrapy bietet integrierte Unterstützung für die Wiederholung von Anfragen, bei denen bestimmte Fehlertypen auftreten. Diese RetryMiddleware ist standardmäßig aktiviert, sodass Sie in vielen Fällen nichts tun müssen und Scrapy häufig fehlgeschlagene Anfragen automatisch für Sie wiederholt. Die Middleware wiederholt fehlgeschlagene Anfragen eine bestimmte Anzahl von Malen, bevor sie aufgibt, was die Chancen einer erfolgreichen Datenextraktion erhöht.

Wenn Sie das Verhalten der RetryMiddleware anpassen müssen, können Sie dies bequem direkt in Ihrer Datei settings.py tun. Von den verfügbaren Konfigurationsoptionen gibt es drei, die Sie möglicherweise für Ihr Projekt anpassen möchten:

  • RETRY_ENABLED: Aktiviert oder deaktiviert die Wiederholungs-Middleware. Es ist standardmäßig aktiviert.
  • RETRY_TIMES: Gibt die maximale Anzahl von Wiederholungsversuchen für eine fehlgeschlagene Anfrage an. Standardmäßig wiederholt das Framework eine fehlgeschlagene Anfrage zweimal.
  • RETRY_HTTP_CODES: Definiert die HTTP-Antwortcodes, die einen Wiederholungsversuch auslösen sollen.

Wie bereits erwähnt, wiederholt Scrapy Ihre Anfragen standardmäßig. Wenn Sie jedoch die Wiederholungs-Middleware für Ihr Scrapy-Projekt außerhalb der Standardeinstellungen konfigurieren müssen, können Sie diese Werte in der Datei settings.py ändern:

# ...

RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 429]

In diesem Beispiel haben Sie die Anzahl der Wiederholungsversuche pro Anfrage auf 3 geändert und die Standard-HTTP-Codes angepasst, sodass Scrapy Anfragen nur dann wiederholt, wenn es HTTP 500 empfängt (interner Serverfehler). und 429 (Zu viele Anfragen) Fehlercodes. Für die meisten Scraper-Projekte ist es jedoch normalerweise eine gute Idee, bei den Standardeinstellungen zu bleiben, die Scrapy implementiert.

Kurz gesagt, Scrapy übernimmt die Wiederholungslogik standardmäßig für Sie über die integrierte RetryMiddleware, die Sie auch anpassen können. Dadurch können Sie eine zuverlässigere Datenextraktion erreichen und die Auswirkungen vorübergehender Probleme reduzieren.

Umgang mit dynamischen Inhalten

Das Scraping dynamischer Inhalte, wie z. B. von JavaScript gerenderter Seiten, stellt eine Reihe einzigartiger Herausforderungen dar. Herkömmliche Web-Scraping-Techniken, die auf statischem HTML basieren, funktionieren nicht, wenn Sie mit Inhalten arbeiten, die dynamisch generiert werden. Es gibt einige Ansätze, mit denen Sie dennoch Zugriff auf die Daten erhalten, an denen Sie interessiert sind:

  • Reproduktionsanfragen: Häufig ruft Ihr Browser die benötigten Daten über API-Aufrufe im Hintergrund ab. Indem Sie die vom Browser gestellten Netzwerkanfragen mithilfe der Entwicklertools in Ihrem Browser überprüfen, können Sie diese API-Endpunkte identifizieren und ähnliche Anfragen direkt von Ihrem Scraper stellen. Dies umgeht die Notwendigkeit des JavaScript-Renderings und ist effizienter.

  • Verwenden Sie Splash, um JavaScript vorab zu rendern: Splash ist ein Headless-Browser, der speziell für Web Scraping entwickelt wurde. Es ermöglicht Ihnen, JavaScript zu rendern und den resultierenden HTML-Code zu erfassen. Scrapy verfügt über eine spezielle Middleware für Splash, Scrapy-Splash genannt, mit der Sie es nahtlos in Ihr Projekt integrieren können.

  • Automatisieren Sie einen Browser: Tools wie Selenium und Playwright bieten eine vollständige Browserautomatisierung, einschließlich der Ausführung von JavaScript. Selenium ist gut etabliert und weit verbreitet, während Playwright neuere, schnellere und zuverlässigere Alternativen mit integrierter Unterstützung für mehrere Browser bietet. Scrapy kann mit dem Paket scrapy-playwright in Playwright integriert werden.

Während das Scraping von Inhalten aus mit JavaScript gerenderten Seiten sicherlich die Schwierigkeit Ihres Web-Scraping-Projekts erhöht, bietet Ihnen Scrapy hilfreiche Integrationen, die Ihnen bei der Erledigung Ihrer Aufgabe helfen.

Verwalten Sie Anti-Scraping-Mechanismen

Viele Websites nutzen Anti-Scraping-Maßnahmen, um ihre Daten zu schützen und eine faire Nutzung ihrer Ressourcen sicherzustellen. Einige dieser Maßnahmen können Sie mit unterschiedlichem Aufwand umgehen.

Mit Scrapy können Sie einige der am häufigsten verwendeten Ansätze direkt in der Datei settings.py eines Projekts anpassen.

Websites blockieren häufig Anfragen von Clients ohne oder mit Standard-User-Agent-Headern. Das Festlegen eines benutzerdefinierten Benutzeragenten in Ihren Projekteinstellungen kann dabei helfen, einen echten Browser nachzuahmen:

# ...

USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"

Sie können auch eine Middleware verwenden, um Benutzeragenten und Proxys zu rotieren, was bei der Verteilung von Anfragen helfen und die Wahrscheinlichkeit einer Blockierung verringern kann:

# ...

DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None,
    "scrapy_user_agents.middlewares.RandomUserAgentMiddleware": 400,
}

# ...

Schließlich können Sie Verzögerungen zwischen Anfragen einführen, was auch dazu beitragen kann, die Auslösung von Anti-Bot-Maßnahmen zu vermeiden:

# ...

DOWNLOAD_DELAY = 2

Wenn eine Website viele Abwehrmechanismen gegen Web-Scraper aufbaut, sollten Sie überlegen, ob es wirklich eine gute Idee ist, sie zu scrapen. Für jede Website, die Sie scrapen möchten, sollten Sie immer die Datei robots.txt überprüfen und diese einhalten. Diese Datei legt die Berechtigungen der Website für Webcrawler fest. Scrapy hat hierfür auch eine Einstellung, die standardmäßig aktiviert ist:

# ...

ROBOTSTXT_OBEY = True

# ...

Durch die Anwendung dieser Techniken und Konfigurationen können Sie häufige Herausforderungen beim Web-Scraping bewältigen und sicherstellen, dass Ihre Scraper robust, effizient und respektvoll gegenüber den Websites sind, mit denen Sie interagieren.

Abschluss

Wenn Sie diesem Tutorial folgen, haben Sie Scrapy und MongoDB erfolgreich für ein vollständiges Web-Scraping-Projekt verwendet, das dem ETL-Prozess folgt. Gute Arbeit!

Sie haben eine breite Palette von Scrapy-Funktionen kennengelernt, einschließlich der im Lieferumfang enthaltenen Testfunktionen und der Art und Weise, wie Sie diese erweitern können. Sie haben außerdem Einblicke in die Bewältigung häufiger Web-Scraping-Herausforderungen erhalten, z. B. die Verwaltung von Wiederholungsversuchen, den Umgang mit dynamischen Inhalten und die Umgehung von Anti-Scraping-Mechanismen.

In diesem Tutorial haben Sie Folgendes gelernt:

  • Richten Sie ein Scrapy-Projekt ein und konfigurieren Sie es
  • Erstellen Sie mit Scrapy einen funktionellen Web-Scraper
  • Extrahieren Sie Daten von Websites mithilfe von Selektoren
  • Speichern gecrackte Daten in einer MongoDB-Datenbank
  • Testen und debuggen Sie Ihren Scrapy Web Scraper

Mit diesen Fähigkeiten sind Sie gut gerüstet, um eine Vielzahl von Web-Scraping-Projekten in Angriff zu nehmen und dabei die Effizienz von Scrapy und die Flexibilität von MongoDB zu nutzen. Egal, ob Sie Daten für Forschungszwecke sammeln, eine datengesteuerte Anwendung erstellen oder einfach nur das Internet erkunden, die Kenntnisse und Tools, die Sie in diesem Tutorial erworben haben, werden Ihnen von großem Nutzen sein. Kratzen Sie weiter und bleiben Sie respektvoll!

Verwandte Artikel