Eine alte Datenjournalistenregel besagt: Wenn Du es einem Praktikanten geben willst, schreib einen Scraper. Stimmt nicht immer, aber oft. Denn grundsätzlich geht das sehr einfach. Ein Tutorial.
Dieser Blogpost hat zwei Gründe: Zum einen ist es natürlich super, mal zu zeigen, wie Scraping mit Python funktioniert. Ich nutze das gerne, weil ich a) die Sprache super intuitiv und schnell finde und sie b) super simpel zu benutzen ist – auch von einem Webserver aus. Und zum Zweiten habe ich dann einen Anlaufpunkt, um nachzuschauen, wie ich das immer angehe. Wenn ich es doch mal wieder ein paar Wochen nicht gemacht habe und meine Skripte mal wieder suche.
Ich werde in diesem Scraper keine echten Links angeben, nur eine Dummywebseite. Webscraping ist nämlich so eine Sache. Rechtlich gesehen: Eine Webseite kann in ihren Nutzungsbedingungen Scraping verbieten. Nur: Wenn ich mich nicht anmelden muss, akzeptiere ich in der Regel diese Nutzungsbedingungen nicht. Weil ich dazu gar keine Chance habe.
Dann sollte man trotzdem wissen was man tut. Und zwar aus Fairness: Im blödesten Fall kann ich mit einem Scraper nämlich einen Webserver in die Knie zwingen, ähnlich wie bei einer DOS-Attacke (Denial of Service). Indem ich einen Server nämlich mit vielen Anfragen bombardiere. Umgehen kann ich das zum Beispiel dadurch, dass ich die Anfragen zeitlich dosiert rausschicke. Manche Seiten bauen auch explizit Maßnahmen gegen Scraper ein (zum Beispiel Facebook). Auch dann sollte ich (oder gegebenenfalls ein Jurist) abwägen, ob mein Scraping noch legal ist. Aus journalistischer Sicht kann da viel im Namen des Allgemeininteresses machbar sein.
Die Daten, die ich bekomme, kann ich dann in jedem beliebigen Format speichern. Will ich sie hinterher auswerten, dann beispielsweise als CSV (also Semikolon- oder Komma-separiert) oder etwas fancier als JSON- oder XML-Dateien, die bei Enticklern beliebter sind, weil sie komplexere Unterteilungen zulassen, als so eine flache Exceltabelle (oder CSV).
Wie sieht der Scraper also nun aus?
In Python nutze ich dafür zwei Bibliotheken: Requests, um eine Anfrage an einen Server/eine Webseite zu stellen. Und Beautiful Soup, um diese Webseite in ihre Einzelteile (bzw. HTML-Knoten) zu zerlegen und auszulesen. (Es gäbe auch noch die Bibliothek Scrapy, die so ähnlich funktioniert. Und für ganz verrückte Webseiten auch noch Geschichten wie Selenium, die einen ganzen Webbrowser (inklusive Nutzer) vortäuschen können.) Dieses Bibliotheken müssen wir vorher installieren. Zum Beispiel mit pip install.
Dann starten wir in Python, und schrieben unser Skript mit einem einfachen Verbindungsversuch.
1 2 3 |
import requests # Bibliothek wird importiert... r = requests.get('http://www.benedict-witzenberger.de') # ... und kümmert sich von selbst um die Verbidung, auch https geht. |
Wir können dann das das Objekt r abfragen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
print(r.status_code) #200 - läuft also alles gut. es gibt eine positive Antwort. print(r.text) # Zeigt mir die Antwort der Request an, in diesem Fall das HTML meines Blogs # <!doctype html> # <!--[if lt IE 7 ]> <html class="no-js ie6" lang="de-DE" prefix="og: http://ogp.me/ns#"> <![endif]--> # <!--[if IE 7 ]> <html class="no-js ie7" lang="de-DE" prefix="og: http://ogp.me/ns#"> <![endif]--> # <!--[if IE 8 ]> <html class="no-js ie8" lang="de-DE" prefix="og: http://ogp.me/ns#"> <![endif]--> # <!--[if (gte IE 9)|!(IE)]><!--> <html class="no-js" lang="de-DE" prefix="og: http://ogp.me/ns#"> <!--<![endif]--> # <head> # # <meta charset="UTF-8" /> # <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"> # <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> # usw.... |
Und genau an dieses HTML wollen wir ran. Je nachdem, welchen Teil der Webseite wir abgreifen wollen, müssen wir einen Weg finden, diesen Teil zu identifizieren. Dass kann über HTML-Tags passieren (alle Links zum Beispiel mit dem a-Tag), oder über CSS-Selektoren (zum Beispiel den Autorennamen eines der letzten Posts über diesen hier: #post-231 > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > section:nth-child(1) > p:nth-child(1) > ba:nth-child(2)). Dafür können wir die Entwicklertools unseres Browsers benutzen, und die entsprechende Stelle auswählen – ich nutze aber auch ganz gerne das Selector Gadget. Ein kleines Addon. Das ist sehr benutzerfreundlich, finde ich. (Wirkt nicht so, wenn man die altbackene Webseite anschaut)
Inhalte auslesen mit Beautiful Soup
Mit der Bibliothek Beautiful Soup kann ich dann mein HTML in ein Soup-Objekt umwandeln. Damit kann ich dann auf die Inhalte meiner einzelnen HTML-Knoten zugreifen. Inhalte können Text sein, aber auch Attribute, wie Link-Ziele.
1 2 3 4 |
from bs4 import BeautifulSoup # Die Bibliothek wird geladen, und zwar nur BeautifulSoup aus dem Paket bs4 soup = BeautifulSoup(r.text, 'html.parser') # aus meinem r.text wird ein navigierbares Objekt print(soup.prettify()) # schönere Ausgabe des Soup-Objekts mit Einrückungen, nicht nötig, aber gut für den Test |
Und dann gehts weiter. BeautifulSoup hat ein paar Funktionen, die mir wichtige Elemente sehr leicht ausgeben. Zum Beispiel:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
soup.title # gibt mir das erste Objekt zurück, das in Title-Tags steht. Gibt vermutlich nur eins auf der Seite soup.title.name # gibt mir den Namen des tags zurück. Der ist title. Danach haben wir ja gesucht. soup.a # gibt mir den ersten Link komplett zurück, also mit allem HTML drumherum. Ginge auch mit p oder b, etc. soup.a.attrs['href'] # gibt mir nur das Link-Ziel zurück. 'class' würde mir die Klassen zurückgeben. soup.a.text # gibt mir nur den Text zurück, auf dem der Link liegt soup.find_all('a') # findet alle Links und speichert sie als Liste. Über die kann ich drüber iterieren. soup.find_all("div", class_="Klasse1 Klasse2gehtauch") # Sucht DIVs mit den angegebenen Klassennamen |
Zur Suche hat BeautifulSoup zwei Funktionen: find gibt das erste Ergebnis zurück, find_all alle als Liste.
Ein Beispiel: Example.com
Ein netter Mensch hat die Seite Example.com ins Internet gestellt. Die können wir zu Testzwecken scrapen. Ein Blick in die Entwicklertools zeigt: Die Seite hat eine h1-Überschrift, zwei p-Absätze, wobei der zweite einen Link enthält.
Und genau das wollen wir alles haben. Wir rufen also die Seite auf, wandeln sie in ein BS4-Objekt um, und suchen dann nach h1, p, und dem Linkziel für a.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from bs4 import BeautifulSoup import requests r = requests.get('http://www.example.com/') soup = BeautifulSoup(r.text, 'html.parser') print(soup.h1.string) # Example Domain print(soup.a['href']) # http://www.iana.org/domains/example absaetze = soup.find_all('p') # Speichert alle Absätze als Liste for absatz in absaetze: # Loopt über die Liste print(absatz.text) # Gibt jeden Absatz in der Konsole aus # This domain is established to be used for illustrative examples in documents. You may use this # domain in examples without prior coordination or asking for permission. # More information... |
Damit ich mich noch ein bisschen an Standards halte, füge ich vor mein Skript noch zwei Zeilen ein. Die erste gibt an, wo mein Python3 gespeichert ist (ich könnte ja auch Python2 benutzen, dann würden Teile der Syntax nicht stimmen und ich bekäme einen Fehler). Die zweite Zeile gibt an, in welcher Kodierung ich arbeite. UTF-8 ist am einfachsten, bevor ich Probleme bekomme, wenn das Skript auf Windows- und Mac-Rechnern bearbeitet wurde.
1 2 |
#!/usr/bin/python3.6 # -*- coding: utf-8 -*- |
Zack, feddich. So geht ein einfacher Scraper.