Scrapy: Gotta scrape ′em all!
Dank dem Internet haben wir heute Zugang zu jeder erdenklichen Information und müssen diese nur noch über unseren Browser aufrufen. Doch letzteres wird schnell zum Problem, da die Auswahl so überwältigend sein kann. Aus dieser Not geboren, wurden Crawler und Scraper entwickelt, die das Internet durchforsten, relevante Informationen extrahieren und sammeln. Die so gewonnen Daten
Da es aber nicht für jeden Anwendungsfall eine entsprechende Suchmaschine gibt, möchte ich euch zeigen, wie ihr mit Python und Scrapy euren eigenen Scraper schreiben könnt.
Wähle deinen Starter!
Die Grundlegenden Aufgaben eines Scrapers sind simpel. Wir wollen den HTML-Code einer Seite herunterladen, relevante Informationen identifizieren und dann speichern. Mit der Standardbibliothek von Python haben wir bereits alle Werkzeuge die wir benötigen. Zusätzlich gibt es auch praktische Tools wie Beautiful Soup die uns helfen den HTML Code zu parsen. Jedoch wollen wir hier den einfachen Weg gehen und installieren deshalb über pip das Framework Scrapy:$ pip install scrapy
Mit Scrapy können wir uns beim Entwickeln auf das auf das wesentliche konzentrieren und mit wenig Code einen robusten Scraper schreiben. Dabei übernimmt Scrapy für uns das eigentliche Herunterladen der Seite, die Fehlerbehandlung, die Parallelisierung verschiedener Anfragen, die Begrenzung der gleichzeitig möglichen Anfragen und vieles mehr. Wir müssen nur noch angeben wo unser Scraper die von uns gewünschten Daten findet und wo sie gespeichert werden sollen.
Der Pokédex
Nachdem geklärt ist, warum wir Scrapy nutzen, ist es an der Zeit unseren ersten Scraper zu schreiben. Dazu erstellen wir die Datei scrapeThemAll.py, importieren verschiedene Scrapy Module und beschreiben die Informationen die wir auslesen möchten. In unserem Fall sind das natürlich Pokémon und konkret interessieren wir uns für den Namen, die Nummer im Pokédex, als auch den jeweiligen Typen und Schwächen. Deswegen legen wir als erstes eine Klasse Pokemon an, die von der Scrapy Klasse Item erbt, und definieren für jede Eigenschaft ein entsprechendes Feld:
from scrapy import Spider from scrapy.item import Item, Field from scrapy.crawler import CrawlerProcess class Pokemon(Item): name = Field() number = Field() type = Field() weakness = Field() evolution = Field()
Danach können wir schon mit dem eigentlichen Scraper loslegen. Wieder nutzen wir eine Scrapy Klasse als Vorlage und definieren den Namen des Scrapers und einen Startpunkt. Da wir unseren eigenen Pokédex vervollständigen wollen, ist für uns der Ideale Startpunkt https://www.pokemon.com/de/pokedex/001, also der offizielle Pokédexeintrag von Bisasam. Auf dieser Seite erhalten wir alle relevanten Informationen zu Bisasam und finden auch einen Link zum nächsten Eintrag. Damit der Scraper weiß, wie er die heruntergeladenen Seite verarbeiten soll, überschreiben wir die Funktion parse. Diese Funktion wird für jede Seite einmal aufgerufen und sollte im Idealfall ein oder mehrere Scrapy Items erzeugen. In unserem Fall haben wir pro Seite genau ein Pokémon und können deswegen in der ersten Zeile, mit der Klasse Pokemon , unser Item initialisieren. Danach extrahieren wir aus dem HTML-Code, über die Funktion css, die für uns relevanten Daten. Besonders schön hierbei ist, dass die Funktion den eigenen Pseudoselektor ::text implementiert, über den man den Text direkt auswählen kann. Alternativ kann man man auch mit XPath-Selektoren arbeiten, jedoch bevorzuge ich ganz klar die gewohnten CSS-Selektoren.
class PokeSpider(Spider): name = 'pokeSpider' start_urls = [ 'https://www.pokemon.com/de/pokedex/001', ] def parse(self, response): item = Pokemon() item['name'] = response.css('.pokedex-pokemon-pagination-title div::text').get().strip() item['number'] = response.css('.pokedex-pokemon-pagination-title .pokemon-number::text').get().strip()[3:] item['type'] = [] item['weakness'] = [] for type in response.css('.pokedex-pokemon-attributes.active .dtm-type li'): item['type'].append(type.css('a ::text').get()) for weakness in response.css('.pokedex-pokemon-attributes.active .dtm-weaknesses li'): item['weakness'].append(weakness.css('a span::text').get().strip()) yield item for next_page in response.css('.pokedex-pokemon-pagination a.next'): yield response.follow(next_page, self.parse)
Wenn alle Felder befüllt sind, übergeben wir das Pokémon mit yield an die Item-Pipeline. Danach schicken wir den Scraper mit der Funktion follow zum nächsten Eintrag, und dieser führt dort dann wieder die parse Funktion aus.
Pokémon lagern
Da unser Scraper nun eigenständig den offiziellen Pokédex durchkämmt, müssen wir uns jetzt noch Gedanken machen wie wir diese Daten verarbeiten und speichern. Mit Scrapy wird diese Aufgabe meist über Pipelines gelöst, welche die Items durchlaufen nachdem sie von einem Scraper gefunden wurden. Eine Pipeline besteht grundlegend aus den drei Funktionen:
- open_spider: Initialisierung der Pipeline, wenn der Scraper gestartet wird.
- close_spider: Aufräumen, nachdem der Scraper beendet wurde.
- process_item: Die Verarbeitung der einzelnen Items.
Für unseren Scraper schreiben wir eine einfache Pipeline, die alle Einträge aus dem Pókedex in eine leicht zu verarbeitende JSON-Datei speichert:
class PokePipeline(object): def open_spider(self, spider): self.file = open('../pokedex.json', 'w') def close_spider(self, spider): self.file.close() def process_item(self, item, spider): line = json.dumps(dict(item)) + "\n" self.file.write(line) return item
Danach müssen wir unsere Pipeline noch beim Scraper registrieren, dazu erweitern wir die Klasse PokeSpider um die Variable custom_settings:
class PokeSpider(Spider): name = 'pokeSpider' start_urls = [ 'https://www.pokemon.com/de/pokedex/001' ] custom_settings = { 'ITEM_PIPELINES': { 'scrapeThemAll.PokePipeline': 400 } }
Ab ins hohe Gras!
Damit ist unser Scraper fertig und wir können anfangen das hohe Gras zu durchstreifen. Scrapy Scraper werden normalerweise über die Konsole gestartet, oft klappt das aber auf Windows-Systemen nicht, da die PATH-Variable falsch konfiguriert ist. Deshalb machen wir unsere Datei scrapeThemAll.py direkt ausführbar, damit können wir den Scraper später mit dem Befehl $ python scrapeThemAll.py
. Um scrapeThemAll.py direkt ausführbar zu machen müssen wir unserem Code nur die für Python-Skripte bekannte Abfrage if __name__ == '__main__':
anfügen. Damit sollte unserer Datei wie folgt aussehen:
import json from scrapy import Spider from scrapy.item import Item, Field from scrapy.crawler import CrawlerProcess class Pokemon(Item): name = Field() number = Field() type = Field() weakness = Field() evolution = Field() class PokeSpider(Spider): name = 'pokeSpider' start_urls = [ 'https://www.pokemon.com/de/pokedex/001' ] custom_settings = { 'ITEM_PIPELINES': { 'scrapeThemAll.PokePipeline': 400 } } def parse(self, response): item = Pokemon() item['name'] = response.css('.pokedex-pokemon-pagination-title div::text').get().strip() item['number'] = response.css('.pokedex-pokemon-pagination-title .pokemon-number::text').get().strip()[3:] item['type'] = [] item['weakness'] = [] for type in response.css('.pokedex-pokemon-attributes.active .dtm-type li'): item['type'].append(type.css('a ::text').get()) for weakness in response.css('.pokedex-pokemon-attributes.active .dtm-weaknesses li'): item['weakness'].append(weakness.css('a span::text').get().strip()) yield item for next_page in response.css('.pokedex-pokemon-pagination a.next'): yield response.follow(next_page, self.parse) class PokePipeline(object): def open_spider(self, spider): self.file = open('../pokedex.json', 'w') def close_spider(self, spider): self.file.close() def process_item(self, item, spider): line = json.dumps(dict(item)) + "\n" self.file.write(line) return item if __name__ == '__main__': crawler = CrawlerProcess() crawler.crawl(PokeSpider) crawler.start()
Scrape mit Respekt!
Jetzt da unser Scraper funktioniert ist es an der Zeit eine gut gemeinte Warnung auszusprechen: Respektiert die Betreiber der Seiten, die ihr mit euren Scrapern besucht! Es kann schnell passieren, dass ihr mit euren Scrapern eine große Menge von Anfragen generiert und damit die Zielserver unter eine ungewöhlich hohe Last legt. Je nach Größe der dahinterliegenden Infrastruktur kann es dann gut sein, dass der Server abstürzt oder ein Admin eurer IP sperrt. In beiden Fällen verlieren beide Seiten und deshalb gebe ich euch hier noch die wichtigsten Regeln, die ihr beachten solltet:- Beachtet die Regeln der robots.txt.
- Überlastet nicht die Server eines Seitenbetreibers.
- Identifiziert euren Scraper mit einem Aussagekräftigen Useragent.
- Nervt nicht die System Administratoren einer Webseite.
Das könnte Dich auch interessieren
Stressmanagement: der richtige Umgang mit Stress
Jeder kennt das Gefühl von Stress. Man ist bereits mit seinen täglichen Aufgaben ausgelastet und trotzdem kommen immer neue Aufgaben dazu. Oft fragt man sich,...
Ein Tag als Praktikant im Home Office Teil 1
Man könnte sich jetzt fragen warum ein Praktikant nach knapp zwei Wochen Einarbeitungszeit im Home-Office gelandet ist. Die kurze Antwort ist Corona. Die lange...
Speedforce – Pt. III: Wasserfall Diagramm
Was bisher geschah: In der letzten Folge haben wir unsere erste Webseite analysiert. Heute stürzen wir uns in die Tiefen des Wasserfall Diagramms... Wasserf...