HTML parsen mit Beautiful Soup#

Beautiful Soup ist eine Python-Bibliothek mit der Sie die mit requests heruntergeladene Seite analysieren (*to parse*) können, dies wird ermöglicht durch die gezielte Navigation über die verschiedenen HTML-Elemente. Wir werden die jüngste Version bs4 nutzen. Gegebenenfalls müssen Sie Beautiful Soup in der Version 4.x herunterladen wie im Kapitel “Installation von Third-Party-Paketen” beschrieben und in der Dokumentation der Bibliothek genau ausgeführt.

Wenn Sie die Bibliothek installiert haben, dann importieren wir sie in unsere Programmierumgebung. Diesmal benötigen wir allerdings nicht den gesamten Umfang, sondern lediglich einzelne Bestandteile. Mit dem nachfolgenden Code importieren wir nur das, was wir auch tatsächlich zum Scraping einer Website benötigen. Das spart Rechenleistung! Wir können zugleich die importierten Pakete mit einem Alias versehen, das bietet sich insbesondere bei längeren Namen an:

from bs4 import BeautifulSoup as bs

War die Installation des Pakets erfolgreich, dann sollten Sie ohne Fehlermeldung weiterarbeiten können. Wir können nun die mit requests heruntergeladene HTML-Datei in ein BeautifulSoup-Objekt transformieren. Standardmäßig wird dazu ein html-parser verwendet, der speziell für die Verarbeitung von HTML-Dokumenten entwickelt wurde. Im Paketumfang sind aber auch einige andere Parser enthalten, zum Beispiel einer für XML-Dateien.

Das BeautifulSoup-Objekt repräsentiert die Website nun als eine verschachtelte Datenstruktur, die wir uns verhältnismäßig übersichtlich mit der Methode .prettify() ansehen können:

# get http with requests
response = requests.get('https://www.geschichte.hu-berlin.de/de/bereiche-und-lehrstuehle/digital-history')

# create BeautifulSoup-object
soup = bs(response.text, "html.parser")

# print BeautifulSoup-object
print(soup.prettify())

Informationen aus der HTML-Struktur auslesen#

Um den Umgang mit HTML-Dokumente zu üben, nutzen wir als Beispiel die Landing-Page der Professur für Digital History. In den nachfolgenden Codeblöcken sehen Sie einige Beispiele dafür, wie Sie unterschiedliche Elemente innerhalb des Dokuments aufrufen können.

Es gibt im Wesentlichen zwei Navigationsmethoden:

  1. Angabe von Tags, die an das BeautifulSoup-Objekt angehängt werden, z.B. soup.a für ein Anker-Element oder soup.h1 für eine Überschrift erster Ordnung

  2. CSS-Selektoren in Kombination mit der Methode select(), z.B. soup.select("div.hu-base-row div > div") sucht nach einem div-Element, das direkt unter einem div-Element angesiedelt ist, das wiederum irgendwo in einem div-Element mit der Klasse (class="") "hu-base-row" eingegliedert ist

Wir können uns beispielsweise den Titel einer Website anzeigen lassen, also den Text, der im Browser auf den jeweiligen Tabkarten angezeigt wird. Zu finden ist diese Information in einem title-Tag:

print(soup.title, "\n")
print(f"Der Output \"{soup.title.text.strip()}\" ist vom Datentyp {type(soup.title.text)}).\n")
print(f"Der Output \"{soup.title.string}\" ist vom Datentyp {type(soup.title.string)}.")

So erhalten wir einmal das vollständige title-Element und einmal lediglich die Zeichenkette, die in den title-Tags eingefasst ist. Für letzteres gibt es zwei Herangehensweisen:

  1. .text

  2. .string

Ersteres liefert Ihnen als Datentyp eine einfache Zeichenkette zurück. Wenn ein Tag-Element lediglich Text enthält, dann ist dies ausreichend. Es kann aber auch sein, dass ein HTML-Element noch weitere children, also Unterelemente, beinhaltet, wenn Sie in einer solchen Struktur noch tiefergehen möchten, dann bietet es sich an, mit der zweiten Variante weiterzuarbeiten, da Sie hier als Datentypen eine navigierbare Zeichenkette erhalten.

Nice to know: Einige überflüssige Whitespaces können Sie durch Anhängen von .strip() eliminieren.

Ein HTML-Dokument durchsuchen#

Tags können verschiedene Attribute haben, Anker-Tags beispielsweise das Attribut “href”, das wiederum häufig eine URL als Wert hat. Wenn wir nicht nur den ersten Link abrufen wollen, sondern alle, dann können wir die Methode find_all(tagname, attrs, recursive, string, limit, **kwargs) einsetzen. Der Methode werden quasi Filter übergeben anhand derer das HTML-Dokument analysiert wird. Es werden alle Nachkommen (descendants) eines Tags durchsucht und nur diejenigen zurückgegeben, die Ihrem definierten Filter entsprechen.

Genutzt werden können dazu einfache Strings oder Listen, um auf Tag-Namen oder Attribute zu referieren, aber auch Reguläre Ausdrücke. Hier einige Beispiele:

# retrieve first link with a value for the attribute "href"
first_link = soup.a["href"]
print("Erster Link referenziert:\n", first_link, "\n")
# retrieve all links with a value for the attribute "href", regardless of what the value is
all_links = soup.find_all("a", href=True)
print("Alle Links mit einem Referenzattribut:\n", all_links, "\n")
# retrieve all a- and p-tags
all_a_and_p = soup.find_all(["a", "p"])
print("Alle Anker- und Absatz-Elemente:\n", all_a_and_p, "\n")
# retrieve all metadata
all_metainfo = soup.find_all("meta")
print("Alle Metainformationen:\n", all_metainfo, "\n")

Während find_all() das komplette Dokument durchsucht und entsprechende Ergebnisse als Liste zurückliefert, kann es freilich auch sein, dass eine ganz bestimmte Information auszulesen ist. Nach einem spezifischen Ergebnis suchen wir mit der Methode find(tagname, attrs, recursive, string, **kwargs).

Mit ihr können wir nun beispielsweise den Volltext aus der HTML-Struktur extrahieren. Dazu müssen wir uns anschauen, in welchem HTML-Element sich der Text befindet. Am einfachsten geht dies, wenn wir in den Quellcode schauen. Wie sie im Browser den Quellcode einer Website untersuchen können, können Sie im Exkurs “HTML-Basics” noch einmal nachlesen.

Wenn Sie den Text inspiziert haben, werden Sie festgestellt haben, dass er sich aus verschiedenen p-Tags zusammensetzt, die children eines div-Tags mit der ID id="content-core" sind. Diese Informationen können wir nun nutzen, um den Text gezielt auszulesen und einer Variablen zuzuweisen:

# retrieve fulltext
content_fulltext = soup.find("div", {"id":"content-core"}).text.strip() 
print(content_fulltext)

Beachten Sie die Syntax: Wir suchen zunächst nach einem div-Element und zwar nach einem div-Element mit einer ganz bestimmten ID. Diese ID können wir durch eine Notation definieren, die Sie auch schon von Dictionaries kennen. Wir suchen gewissermaßen nach einem bestimmten Schlüssel mit einem bestimmten Wert. Etwaige HTML-Elemente wie beispielsweise p-Tags, die sich in diesem Bereich befinden, können Sie durch das Anhängen von .text ausschließen. Überflüssige Whitespaces entfernen wir mit .strip().

Eine weitere wichtige Information, die man extrahieren könnte, stellt stets auch das Veröffentlichungsdatum dar. Mit den bisher kennengelernten Inhalten können wir auch dies nach einer Inspektion des Quellcodes herunterladen. Auf der Website befindet sich das Datum in der Fußzeile. Es ist in einem nicht näher spezifizierten span-Element eingebettet, das wiederum child eines span-Elements der Klasse "documentModified" ist. Diese Informationen sind zu unspezifisch, um das Datum gezielt auszulesen. Was aber direkt ansteuerbar ist, ist der sibling mit der Zeichenkette “zuletzt geändert”. Diese Information können wir uns zu Nutze machen:

# retrieve date of publication
report_issued = soup.find("span", string="zuletzt geändert").next_sibling.text.strip()
print(report_issued)

Wir steuern also zunächst dasjenige span-Element an, das einen String des Inhalts “zuletzt geändert” enthält. Davon interessiert uns aber nur der Inhalt des direkt darauf folgenden Tags, den wir per .next_sibling.text ansteuern. Überflüssige Whitespaces entfernen wir wieder mit .strip().

Wenn die Daten einer API in XML zurückgegeben werden, können Sie mit Beautiful Soup ebenso vorgehen, wie im vorherigen Abschnitt am Beispiel von HTML veranschaulicht wurde. XML ist, ähnlich wie HTML, eine Auszeichnungssprache mit der bestimmte Elemente eines Dokuments maschinenlesbar gemacht werden können. Tags und deren Attribute funktionieren in XML analog wie bei HTML. Sie können die XML-Struktur eines Dokuments oftmals auch dazu nutzen, um mit geringem Aufwand Informationen auszulesen.