Web Scraping

In 5 Schritten automatisiert Daten sammeln!

Was ist eigentlich Web Scraping?
Unter Web Scraping versteht man das herunterladen strukturierter Daten aus dem Internet.
Automatisiert werden die gewünschten Daten aus den Webseiten extrahiert und für die weitere Verarbeitung vorbereitet.
Dafür nutzen wir die folgenden Bibliotheken:

  • requests – HTTP Requests aus Python heraus einfach ausführen
  • BeautifulSoup4 – Verarbeitung von HTML Inhalten

Unser Ziel:
Eine fertige Anwendung, die uns die aktuellen Vertreter des DAX ausliest.
Zu jedem Dax-Titel erhalten wir den Namen, die ISIN (International Securities Identification Number) und die URL zur Detail-Seite mit weiteren Daten.

Inhalt:
    Add a header to begin generating the table of contents

    Allgemeines zum Web Scraping

    Python kann im Standard schon HTTP Requests erstellen.
    Allerdings wird die Arbeit mit der Bibliothek requests doch noch einmal deutlich vereinfacht.

    Das selbe gilt für BeautifulSoup4.
    Natürlich könnten wir die erhaltenen Strings manipulieren und zerlegen, aber BeautifulSoup4 erleichtert uns schlicht diese Arbeit.

    Um die beiden Bibliotheken zu installieren führen wir einfach einen der beiden Befehle aus – je nach Umgebung: 
    pip install requests BeautifulSoup4 
    conda install requests BeautifulSoup4

    Und schon sind wir bereit loszulegen
    Im Grunde läuft Web Scraping immer gleich ab:

    • Abfrage einer Webseite
    • Herausfiltern der gewünschten Daten anhand diverser Regeln
    • Konvertierung der Daten in ein brauchbares Format

    Schritt 1: Seite abrufen (Request)

    Zunächst brauchen wir unsere Daten.
    Da es sich um Web Scraping handelt, liegen unsere Daten in Ihrer Rohform eben als Webseite rum.
    Und genau an die müssen wir erst einmal gelangen.
    requests hat es sich zur Aufgabe gemacht, den Umgang mit Http in Python so einfach wie nur möglich zu gestalten – und diese Aufgabe erledigt es hervorragend!

    Für diesen Abschnitt werden wir nur GET Aufrufe benötigen.
    Wer allerdings mehr mit dem Web zu tun hat, sollte sich das Modul definitiv näher ansehen!

    Nun aber zu unserer Aufgabe:

    import requests
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    print(seite) 

    Soweit erstmal noch nichts besonderes.
    Wir legen uns eine Variable an (Zeile 3), die die URL für unseren Aufruf vorhält.
    Die URL ist die Webseite des DAX-Index bei der Deutschen Börse.
    Hier gibt es schon mal einige Informationen zum DAX.
    Die WKN, die ISIN, der aktuelle Stand aller vertretenen Firmen mit Live-Kursen und wir können zu jeder FIrma weiter navigieren und uns deren Seiten ansehen, indem wir auf den Namen klicken.

    Das Modul requests gibt uns mit der Funktion .get(URL, PARAMS) die Möglichkeit einfach einen GET abzusetzen und ein Objekt der Anfrage zu bekommen.
    Das Objekt – der Response – enthält alle Daten zur Anfrage.
    Über seite.status_code erhalten wir zum Beispiel den Status der Anfrage – 200, sofern erfolgreich.
    Über seite.content bekommen wir das vollständige HTML der Seite.
    Hier sind sie – unsere Rohdaten.

    Kenne deine Daten...

    Nachdem wir unsere Rohdaten jetzt schon in der Hand haben, ist es Zeit herauszufinden, nach was genau wir eigentlich suchen.
    Unser Ziel ist es beständige Marker zu identifizieren, mit deren Hilfe wir die Daten filtern können.

    Sehen wir uns dazu den HTML Code der Seite im Browser genauer an.
    Die Daten die wir suchen – also die Liste der DAX-Titel – sind alle in einer Liste auf der Seite zu finden.
    Der HTML Code dazu sieht wie folgt aus:

    <!-- DAX Titel -->
    <table id="pushList" class="tablesorter">
        <thead>
        <tr class="row-bordered">
            <th class="g header"><span></span></th>
            <th class="N header headerSortDown"><span>Name</span></th>
            <th class="b hidden-xs header"><span>Bid</span></th>
            <th class="a hidden-xs header"><span>Ask</span></th>
            <th class="p header"><span>Akt.</span></th>
            <th class="j header"><span>±</span></th>
            <th class="J header"><span>±%</span></th>
            <th class="o hidden-xs header"><span>Erster</span></th>
            <th class="h hidden-xs header"><span>Hoch</span></th>
            <th class="l hidden-xs header"><span>Tief</span></th>
            <th class="V hidden-xs header"><span>Tagesvol.</span></th>
            <th class="c hidden-xs header"><span>Vortag</span></th>
            <th class="t header"><span>Zeit</span></th>
            <th class="S hidden-xs header"><span>Börse</span></th>
            <th class="E header"><span></span></th>
        </tr>
        </thead>
        <tbody>
            <tr class="row-bordered odd" id="DE000A1EWWW0" stockid="16">
                <td class="alignleft">
                    <div class="tablesorter" id="DE000A1EWWW0_g_bg">
                        <i class="fa fa-arrow-right black"></i>
                    </div>
                </td>
                <td class="alignleft tooltipk">
                    <div class="tablesorter N" id="DE000A1EWWW0_N_bg" title="Adidas-Aktie">
                       <a href="https://www.boerse.de/aktien/Adidas-Aktie/DE000A1EWWW0" target="_blank">
                            Adidas
                       </a>
                    </div>
                </td>
                ...
            </tr>
        ...
        </tbody>
    </table> 

    Wir sehen:
    Die ganze Liste ist eine HTML-Tabelle mit der ID pushlist.
    Perfekt!
    IDs ändern sich in der Regel nicht so häufig und sollten nur ein einziges Mal pro Seite vorkommen.
    Sie sind also deutlich bessere Marker als CSS Klassen oder andere Konstrukte.
    Danach können wir doch schonmal eingrenzen, das merken wir uns.

    Und wie geht es weiter?
    Irgendwie müssen wir ja noch an die Daten kommen.

    Im Body der Tabelle gibt es einzelne Zeilen für jede Aktie.
    Jede Tabellen-Zeile wiederum hat als HTML-ID die ISIN (International Securities Identification Number) der jeweiligen Aktie.
    Sehr schön!
    Auch das wir gleich mal vermerkt.

    Weiter geht es zu den Spalten.
    Die erste Spalte ist uninteressant.
    Sie zeigt einen Pfeil der den aktuellen Verlauf des Kurses signalisieren soll.
    Die zweite Spalte ist allerdings interessant:
    Wir haben hier nicht nur den Namen der Aktie, sondern auch gleich eine Verlinkung zu Detail-Seite der Aktie dahinter.
    Also direkt der Pfad zu weiteren Informationen!

    Okay, erstmal bis hier hin.
    Was haben wir?

    • Tabelle mit ID pushlist
    • Tabellen-Body enthält Aktien
    • Tabellen-Zeile im Body hat als ID die ISIN der Aktie
    • Tabelle Spalte 2 enthält den Namen und einen Link zur Seite der Aktie

    Versuchen wir erst einmal soweit die Daten zu extrahieren.

    Schritt 2: Daten einsammeln - Die Vorbereitung

    Seite​

    Bisher haben wir in unserer Variablen seite einfach nur einen langen HTML String mit sämtlichem Code hinter der gegebenen URL liegen.
    Um jetzt mit BeautifulSoup arbeiten zu können, müssen wir die Seite in ein passendes Objekt einlesen.
    Glücklicherweise bietet BeautifulSoup dafür direkt einen Konstruktor:

    import requests, bs4
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    bs4_seite = None
    if seite.status_code == 200:
        bs4_seite = bs4.BeautifulSoup(seite.content, 'html.parser')
    else:
        print('Seite konnte nicht geladen werden.', url) 

    In Zeile 6 prüfen wir zunächst, ob der Aufruf der Seite überhaupt geklappt hat. Haben wir also HTML bekommen?
    Wenn ja, können wir fortfahren und in Zeile 7 ein BeautifulSoup-Objekt aus dem Inhalt generieren.
    Alles was wir dafür brauchen ist der Inhalt (seite.content) und den Parser (html.parser). Wir müssen BeautifulSoup also mitteilen, auf welche Art der übergebene Inhalt eingelesen werden kann.
    BeautifulSoup kann neben HTML nämlich auch noch XML einlesen und verarbeiten.

    Schritt 3: Daten filtern - Das Web Scraping beginnt

    Tabelle​

    Um die Tabelle – wir erinnern uns, die ID pushList – aus der Seite zu entnehmen, müssen wir BeautifulSoup mitteilen, was wir finden möchten.
    Dafür wird uns direkt eine Funktion .find angeboten, der wir Parameter für die Suche übergeben können.

    Einwurf:
    Die Parameter-Angabe erfolgt nach dem Schema für CSS-Selektoren.

    Wir wissen, dass wir eine HTML-Tabelle mit der id=”pushList” suchen.

    import requests, bs4
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    bs4_seite = None
    if seite.status_code == 200:
        bs4_seite = bs4.BeautifulSoup(seite.content, 'html.parser')
    else:
        print('Seite konnte nicht geladen werden.', url)
        
    pushList = bs4_seite.find('table', {'id': 'pushList'}) 

    Das wars schon.
    Die Funktion .find bekommt in Zeile 11 zuerst mitgeteilt, um welches HTML-Element es sich handelt – eine table – und im Anschluss, welche Eigenschaften dieses Element erfüllen soll – es soll die id=”pushList” haben.

    In der Variablen pushList befindet sich jetzt ausschließlich der HTML-Code der gesuchten Tabelle.

    Body

    Eine HTML-Tabelle ist in der Regel in 2 Bereiche aufgeteilt:
    Tabellen-Kopf (thead) und Tabellen-Körper (tbody).
    Da sich unsere Daten im Body der Tabelle befinden, müssen wir das vorhandene HTML also noch weiter eindampfen.
    Dafür nutzen wri an unserem Tabellen-Objekt einfach wieder .find, und schränken weiter ein.

    import requests, bs4
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    bs4_seite = None
    if seite.status_code == 200:
        bs4_seite = bs4.BeautifulSoup(seite.content, 'html.parser')
    else:
        print('Seite konnte nicht geladen werden.', url)
        
    pushList = bs4_seite.find('table', {'id': 'pushList'})
    body = pushList.find('tbody') 

    Innerhalb einer Tabelle kommen Kopf und Körper nur ein einziges Mal vor.
    Wir müssen also keine weiteren Eigenschaften benennen, um ein eindeutiges Ergebnis zu erzielen und können direkt nach dem HTML-Element tbody innerhalb der Tabelle filtern.
    Im Anschluss an Zeile 12 halten wir in der Variablen body den HTML-Code von tbody in der Hand und können damit weiter arbeiten.

    Schritt 4: Daten auslesen

    Aktien Zeilen​

    Innerhalb des Body wird jede Aktie als einzelne Zeile dargestellt.
    Tabellen-Zeilen – Table-Rows – werden durch ein tr-Tag gekennzeichnet.
    Das können wir uns nach dem selben Schema auch wieder aus dem Body heraus ziehen.

    Der einzige Unterschied diesmal ist: Wir haben nicht nur eine Zeile.
    Unser gesuchtes Element – tr – wird also häufiger vorkommen.

    Für so einen Fall stellt uns BeautifulSoup die Funktion .find_all zur Verfügung.
    Sie funktioniert genauso wie vorher schon .find, mit dem einzigen Unterschied, dass sie davon ausgeht mehr zu finden und somit eine Liste zurück liefert.

    import requests, bs4
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    bs4_seite = None
    if seite.status_code == 200:
        bs4_seite = bs4.BeautifulSoup(seite.content, 'html.parser')
    else:
        print('Seite konnte nicht geladen werden.', url)
        
    pushList = bs4_seite.find('table', {'id': 'pushList'})
    body = pushList.find('tbody')
    aktien_liste = body.find_all('tr') 

    Mit Zeile 13 ziehen wir uns nun also alle einzelnen Aktien-Zeilen aus dem verbliebenen HTML heraus und legen sie als Liste in der Variablen aktien_liste ab.

    Schritt 5: Daten speichern

    Aktien Daten​

    Jeder einzelne Eintrag in unserer aktien_liste sollte jetzt in etwa so aussehen:

    <tr class="row-bordered odd" id="DE000A1EWWW0" stockid="16">
        <td class="alignleft">
            <div class="tablesorter" id="DE000A1EWWW0_g_bg">
                <i class="fa fa-arrow-right black"></i>
            </div>
        </td>
        <td class="alignleft tooltipk">
            <div class="tablesorter N" id="DE000A1EWWW0_N_bg" title="Adidas-Aktie">
               <a href="https://www.boerse.de/aktien/Adidas-Aktie/DE000A1EWWW0" target="_blank">
                    Adidas
               </a>
            </div>
        </td>
    </tr>
            ... 

    Aus dem Code müssen wir also unsere eigentlichen Ziel-Daten entnehmen:

    • ISIN
    • Name der Aktie
    • URL zur Detail-Seite

    Gehen wir erst einmal Stück für Stück durch, was wir tun müssen und sehen uns dann den Code dazu an:

    An jedem Eintrag der Liste – also direkt an dem tr-Tag, bekommen wir die ISIN über das Attribut id.
    BeautifulSoup gibt uns die Möglichkeit ein HTML-Element wie ein Dictionary zu behandeln.
    Wollen wir also ein bestimmtes Attribut von einem Element, müssen wir es nur als key des Dictionaries behandeln und direkt erfragen:

    isin = aktie['id']

    Als nächstes brauchen wir den Namen der Aktie.
    Den bekommen wir über den Namen des Links zur Aktie, in dem div innerhalb unserer Zeile.

    Okay, nochmal:
    Wir haben ein tr, da drin ist ein div, da drin ist ein a.
    Das a brauchen wir für den Namen der Aktie.

    div = aktie.find('div', {'class': 'tablesorter N'})
    link = div.find('a')
    name = link.text

    Haben wir ein HTML-Element in der Hand, können wir den Inhalt des Elements über .text heraus nehmen. Damit erhalten wir den Namen der Aktien (Adidas in unserem Beispiel-Code hier).

    Die URL zur Detail-Seite heraus zu bekommen ist jetzt keine Schwierigkeit mehr.
    Den Link haben wir bereits in der Variablen link als HTML Element liegen und müssen somit nur noch das Attribut href abfragen:

    url = link['href']

    So. Überstanden. Sehen wir uns einmal den Code dazu an:

    import requests, bs4, json
    
    url = 'https://www.boerse.de/realtime-kurse/Dax-Aktien/DE0008469008'
    seite = requests.get(url)
    bs4_seite = None
    if seite.status_code == 200:
        bs4_seite = bs4.BeautifulSoup(seite.content, 'html.parser')
    else:
        print('Seite konnte nicht geladen werden.', url)
        
    pushList = bs4_seite.find('table', {'id': 'pushList'})
    body = pushList.find('tbody')
    aktien_liste = body.find_all('tr')
    daten = {}
    for aktie in aktien_liste:
        isin = aktie['id']
        div = aktie.find('div', {'class': 'tablesorter N'})
        link = div.find('a')
        daten[isin] = {
            'name': link.text, 
            'url': link['href']
        }
    print(json.dumps(daten, indent=4)) 

    Zusammenfassung - Web Scraping in 5 Schritten

    Seite aufrufen (Request)
    Zuerst laden wir in Zeile 4 die Seite als HTML-Rohversion herunter und prüfen in Zeile 6, ob das auch geklappt hat.

    Daten einsammeln (BeautifulSoup)
    Nachdem wir den HTML-Code nun in der Hand halten, können wir BeautifulSoup in Zeile 7 anweisen die Seite als HTML einzulesen und nus als Objekt bereit zu stellen.

    Daten filtern
    In Zeile 11 können wir uns dann über das HTML-Attribut id die Tabelle mit den gewünschten Daten heraus suchen.

    Daten auslesen
    Nachdem wir in Zeile 12 die Tabelle noch auf den eigentlichen Datensatz – den Body – eingeschränkt haben, können wir in Zeile 13 alle Einzelzeilen als Liste heraus lösen.
    Jede Zeile repräsentiert wiederum eine Aktie.

    Daten speichern
    In dem wir in Zeile 15 über die Liste der Aktien iterieren, bekommen wir den HTML-Code jeder einzelnen Aktie in die Hand.
    Damit können wir dann in Zeile 16 direkt die id des tr-Elements auslesen, um die ISIN der Aktie zu bekommen.
    Den Link und den zugehörigen Namen der Aktie bekommen wir in der Zeile 18 nachdem wir innerhalb der Aktien-Zeile das div und den zugehörigen a-Tag ausgelesen haben.

    BeautifulSoup vs. RegEx​

    Warum sollte ich mich mit BeautifulSoup rumschlagen, wenn ich RegEx kann?
    Die Frage ist durchaus berechtigt!
    Reguläre Ausdrücke suchen nach bestimmten Mustern in einem Text.
    Damit sind sie sehr präziese in dem was sie tun, lassen aber auch kaum Veränderungen zu.
    Sollte eine Webseite also ein bisschen abgeändert werden, passiert es sehr schnell, dass der Ausdruck nicht mehr funktioniert und das Programm angepasst werden muss.
    Reguläre Ausdrücke anzupassen ist nicht immer ganz einfach und gerade wenn sie komplexer werden, weil sie tief in eine Seitenstruktur gehen müssen, kann es schnell unübersichtlich, fehleranfällig und aufwändig werden.

    An sich passiert das mit BeautifulSoup auch. Jedoch viel seltener.
    Die Art wie BeautifulSoup eine Webseite verarbeitet und durchforstet lässt einfach etwas mehr flexibilität zu.
    Das heißt, BeautifulSoup ist robuster als Reguläre Ausdrücke und wir müssen unser Programm seltener anpassen.

    Im Gegenzug sind Reguläre Ausdrücke deutlich schneller. Bis zu 100 Mal schneller.

    Es läuft also darauf hinaus was wir brauchen.
    Eine robustere, oder eine schnellere Anwendung.

    Eine pauschale Antwort gibt es nicht.
    Es kommt einfach auf den Anwendungsfall an.
    Geht es um eine einfache, kleine RegEx, um schnell an Daten zu kommen ist die natürlich zu bevorzugen.
    Sollte es aber um komplexere Daten oder Konstrukte gehen würde ich auf jeden Fall BeautifulSoup bevorzugen.

    Kommentar verfassen

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

    Scroll to Top