Kapitel 4. Web Crawling Modelle

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Das Schreiben von sauberem und skalierbarem Code ist schwierig genug, wenn du die Kontrolle über deine Daten und deine Eingaben hast. Das Schreiben von Code für Web-Crawler, die eine Vielzahl von Daten von verschiedenen Websites abrufen und speichern müssen, über die der Programmierer keine Kontrolle hat, stellt oft einzigartige organisatorische Herausforderungen dar.

Es kann sein, dass du gebeten wirst, Nachrichtenartikel oder Blogbeiträge von verschiedenen Websites zu sammeln, die alle unterschiedliche Templates und Layouts haben. Der h1 -Tag der einen Website enthält den Titel des Artikels, der h1 -Tag der anderen den Titel der Website selbst und der Titel des Artikels steht in <span id="title">.

Du brauchst vielleicht eine flexible Kontrolle darüber, welche Websites gescraped werden und wie sie gescraped werden, und eine Möglichkeit, so schnell wie möglich neue Websites hinzuzufügen oder bestehende zu ändern, ohne mehrere Codezeilen zu schreiben.

Möglicherweise wirst du gebeten, Produktpreise von verschiedenen Websites abzurufen, um die Preise für ein und dasselbe Produkt zu vergleichen. Vielleicht sind diese Preise in verschiedenen Währungen angegeben, und vielleicht musst du sie auch mit externen Daten aus anderen Quellen als dem Internet kombinieren.

Obwohl die Einsatzmöglichkeiten von Web-Crawlern schier endlos sind, fallen große, skalierbare Crawler meist in eines von mehreren Mustern. Wenn du diese Muster lernst und die Situationen erkennst, auf die sie zutreffen, kannst du die Wartungsfreundlichkeit und Robustheit deiner Webcrawler erheblich verbessern.

In diesem Kapitel geht es in erster Linie um Webcrawler, die eine begrenzte Anzahl von Datentypen (z. B. Restaurantkritiken, Nachrichtenartikel, Unternehmensprofile) von einer Vielzahl von Websites sammeln und diese Datentypen als Python-Objekte speichern, die aus einer Datenbank lesen und schreiben.

Planung und Definition von Objekten

Eine häufige Falle beim Web Scraping ist, dass du die Daten, die du sammeln willst, ausschließlich auf der Grundlage dessen definierst, was du vor Augen hast. Wenn du z. B. Produktdaten sammeln willst, schaust du dir vielleicht zuerst einen Bekleidungsladen an und entscheidest, dass jedes Produkt, das du scrapen willst, die folgenden Felder haben muss:

  • Produktname

  • Preis

  • Beschreibung

  • Größen

  • Farben

  • Stoffart

  • Kundenbewertung

Wenn du dir eine andere Website ansiehst, stellst du fest, dass auf der Seite SKUs (Stock Keeping Units, Lagerhaltungseinheiten, die zur Nachverfolgung und Bestellung von Artikeln verwendet werden) aufgeführt sind. Diese Daten willst du auf jeden Fall auch erfassen, auch wenn sie auf der ersten Seite nicht erscheinen! Du fügst dieses Feld hinzu:

  • Artikel SKU

Auch wenn Kleidung ein guter Anfang ist, solltest du sicherstellen, dass du diesen Crawler auch auf andere Arten von Produkten ausweiten kannst. Du fängst an, die Produktbereiche anderer Websites zu durchsuchen und beschließt, dass du auch diese Informationen sammeln musst:

  • Hardcover/Paperback

  • Matte/Glänzende Drucke

  • Anzahl der Kundenrezensionen

  • Link zum Hersteller

Das ist eindeutig ein unhaltbarer Ansatz. Wenn du jedes Mal, wenn du eine neue Information auf einer Website entdeckst, Attribute zu deinem Produkttyp hinzufügst, führt das dazu, dass du viel zu viele Felder im Blick behalten musst. Und nicht nur das: Jedes Mal, wenn du eine neue Website scrapen willst, bist du gezwungen, eine detaillierte Analyse der Felder der Website und der Felder, die du bisher gesammelt hast, durchzuführen und möglicherweise neue Felder hinzuzufügen (und damit deinen Python-Objekttyp und deine Datenbankstruktur zu ändern). Das Ergebnis ist ein unübersichtlicher und schwer zu lesender Datensatz, der zu Problemen bei der Nutzung führen kann.

One Das Beste, was du tun kannst, wenn du entscheidest, welche Daten du sammeln willst, ist oft, die Websites ganz zu ignorieren. Du beginnst ein Projekt, das umfangreich und skalierbar sein soll, nicht, indem du dir eine einzige Website ansiehst und sagst: "Was gibt es?", sondern indem du sagst: "Was brauche ich?" und dann Wege findest, die benötigten Informationen von dort zu holen.

Vielleicht willst du aber auch einfach nur die Preise von Produkten in verschiedenen Geschäften vergleichen und diese Preise im Laufe der Zeit verfolgen. In diesem Fall brauchst du genug Informationen, um das Produkt eindeutig zu identifizieren, und das war's:

  • Produkttitel

  • Hersteller

  • Produkt-ID-Nummer (falls vorhanden/relevant)

Es ist wichtig zu wissen, dass keine dieser Informationen spezifisch für einen bestimmten Laden ist. Zum Beispiel sind die Produktbewertungen, der Preis und sogar die Beschreibung spezifisch für das Produkt in einem bestimmten Geschäft. Sie können separat gespeichert werden.

Andere Informationen (Farben, aus denen das Produkt besteht) sind produktspezifisch, können aber spärlich sein - sie gelten nicht für jedes Produkt. Es ist wichtig, dass du einen Schritt zurücktrittst und für jeden Artikel, den du in Betracht ziehst, eine Checkliste erstellst und dir die folgenden Fragen stellst:

  • Werden diese Informationen bei den Projektzielen helfen? Wird es ein Hindernis sein, wenn ich sie nicht habe, oder ist es nur "nice to have", hat aber letztlich keine Auswirkungen?

  • Wenn es in der Zukunft helfen könnte, ich mir aber unsicher bin, wie schwierig ist es dann, die Daten zu einem späteren Zeitpunkt wieder zu sammeln?

  • Sind diese Daten redundant zu Daten, die ich bereits gesammelt habe?

  • Ist es logisch sinnvoll, die Daten in diesem speziellen Objekt zu speichern? (Wie bereits erwähnt, macht es keinen Sinn, eine Beschreibung in einem Produkt zu speichern, wenn sich diese Beschreibung von Website zu Website für dasselbe Produkt ändert).

Wenn du dich dafür entscheidest, die Daten zu sammeln, ist es wichtig, ein paar weitere Fragen zu stellen, um dann zu entscheiden, wie du sie im Code speichern und verarbeiten willst:

  • Sind diese Daten spärlich oder dicht? Sind sie relevant und werden sie in jeder Auflistung angezeigt oder nur in einer Handvoll von Auflistungen?

  • Wie groß sind die Daten?

  • Muss ich sie vor allem bei großen Datenmengen regelmäßig abrufen, wenn ich meine Analyse durchführe, oder nur gelegentlich?

  • Wie variabel ist diese Art von Daten? Muss ich regelmäßig neue Attribute hinzufügen, Typen ändern (z. B. Stoffmuster, die häufig hinzugefügt werden), oder sind sie unveränderlich (Schuhgrößen)?

Nehmen wir an, du willst eine Metaanalyse zu Produktattributen und Preisen durchführen: zum Beispiel die Anzahl der Seiten eines Buches oder die Art des Stoffes, aus dem ein Kleidungsstück besteht, und in Zukunft möglicherweise weitere Attribute, die mit dem Preis korrelieren. Du gehst die Fragen durch und stellst fest, dass diese Daten spärlich sind (nur relativ wenige Produkte haben eines der Attribute) und dass du möglicherweise häufig Attribute hinzufügen oder entfernen wirst. In diesem Fall kann es sinnvoll sein, einen Produkttyp zu erstellen, der wie folgt aussieht:

  • Produkttitel

  • Hersteller

  • Produkt-ID-Nummer (falls vorhanden/relevant)

  • Attribute (optionale Liste oder Wörterbuch)

Und ein Attributtyp, der so aussieht:

  • Name des Attributs

  • Attribut Wert

So kannst du im Laufe der Zeit flexibel neue Produktattribute hinzufügen, ohne dass du dein Datenschema umgestalten oder den Code umschreiben musst. Wenn du entscheidest, wie du diese Attribute in der Datenbank speichern willst, kannst du JSON in das Feld attribute schreiben oder jedes Attribut in einer eigenen Tabelle mit einer Produkt-ID speichern. In Kapitel 6 findest du weitere Informationen zur Implementierung dieser Arten von Datenbankmodellen.

Du kannst die vorangegangenen Fragen auch auf die anderen Informationen anwenden, die du speichern musst. Um den Überblick über die Preise der einzelnen Produkte zu behalten, brauchst du wahrscheinlich die folgenden Informationen:

  • Produkt-ID

  • Laden-ID

  • Preis

  • Datum/Zeitstempel Preis wurde gefunden bei

Aber was ist, wenn die Eigenschaften des Produkts tatsächlich den Preis des Produkts verändern? So kann es zum Beispiel sein, dass ein großes Hemd teurer ist als ein kleines, weil das große Hemd mehr Arbeit oder Material erfordert. In diesem Fall könntest du in Erwägung ziehen, das einzelne Hemdenprodukt in separate Produktlisten für jede Größe aufzuteilen (so dass jedes Hemdenprodukt unabhängig vom anderen preislich bewertet werden kann) oder einen neuen Artikeltyp zu erstellen, um Informationen über Instanzen eines Produkts zu speichern, die diese Felder enthalten:

  • Produkt-ID

  • Instanztyp (in diesem Fall die Größe des Hemdes)

Und jeder Preis würde dann wie folgt aussehen:

  • Produkt-Instanz-ID

  • Laden-ID

  • Preis

  • Datum/Zeitstempel Preis wurde gefunden bei

Das Thema "Produkte und Preise" mag zwar sehr spezifisch erscheinen, aber die grundlegenden Fragen, die du dir stellen musst, und die Logik, die du bei der Gestaltung deiner Python-Objekte anwendest, gelten für fast jede Situation.

Wenn du Nachrichtenartikel ausliest, brauchst du vielleicht grundlegende Informationen wie die folgenden:

  • Titel

  • Autor

  • Datum

  • Inhalt

Aber sagen wir, einige Artikel enthalten ein "Überarbeitungsdatum" oder "verwandte Artikel" oder eine "Anzahl von Social Media Shares". Brauchst du diese Angaben? Sind sie für dein Projekt relevant? Wie kannst du die Anzahl der Social Media Shares effizient und flexibel speichern, wenn nicht alle Nachrichtenseiten alle Formen von Social Media nutzen und Social Media Seiten im Laufe der Zeit an Beliebtheit gewinnen oder verlieren?

Wenn man mit einem neuen Projekt konfrontiert wird, kann es verlockend sein, sich sofort in Python zu schreiben, um Websites zu scrapen. Das Datenmodell von, das du erst im Nachhinein erstellst, wird oft stark von der Verfügbarkeit und dem Format der Daten auf der ersten Website beeinflusst, die du scrappst.

Das Datenmodell ist jedoch die Grundlage für den gesamten Code, der es verwendet. Eine schlechte Entscheidung in deinem Modell kann leicht zu Problemen beim Schreiben und Pflegen des Codes führen oder zu Schwierigkeiten bei der Extraktion und effizienten Nutzung der Daten. Vor allem, wenn du mit einer Vielzahl bekannter und unbekannter Websites arbeitest, ist es wichtig, dass du dir genau überlegst und planst, was du sammeln und wie du es speichern musst.

Der Umgang mit verschiedenen Website-Layouts

Eine der beeindruckendsten Leistungen einer Suchmaschine wie Google ist, dass sie es schafft, relevante und nützliche Daten aus einer Vielzahl von Websites zu extrahieren, ohne im Voraus etwas über die Struktur der Website selbst zu wissen. Obwohl wir Menschen in der Lage sind, den Titel und den Hauptinhalt einer Seite sofort zu erkennen (abgesehen von extrem schlechtem Webdesign), ist es viel schwieriger, einen Bot dazu zu bringen, das Gleiche zu tun.

Glücklicherweise geht es beim Webcrawling in den meisten Fällen nicht darum, Daten von Websites zu sammeln, die du noch nie gesehen hast, sondern von ein paar oder ein paar Dutzend Websites, die von einem Menschen vorausgewählt wurden. Das bedeutet, dass du keine komplizierten Algorithmen oder maschinelles Lernen einsetzen musst, um zu erkennen, welcher Text auf der Seite "am ehesten wie ein Titel aussieht" oder welcher wahrscheinlich der "Hauptinhalt" ist. Du kannst diese Elemente manuell bestimmen.

Der naheliegendste Ansatz ist, für jede Website einen eigenen Webcrawler oder Seitenparser zu schreiben. Jeder könnte eine URL, einen String oder ein BeautifulSoup Objekt aufnehmen und ein Python-Objekt für die gescrapte Seite zurückgeben.

Das folgende ist ein Beispiel für eine Content Klasse (die einen Inhalt auf einer Website repräsentiert, z.B. einen Nachrichtenartikel) und zwei Scraper-Funktionen, die ein BeautifulSoup Objekt aufnehmen und eine Instanz von Content zurückgeben:

import requests

class Content:
    def __init__(self, url, title, body):
        self.url = url
        self.title = title
        self.body = body

def getPage(url):
    req = requests.get(url)
    return BeautifulSoup(req.text, 'html.parser')

def scrapeNYTimes(url):
    bs = getPage(url)
    title = bs.find('h1').text
    lines = bs.select('div.StoryBodyCompanionColumn div p')
    body = '\n'.join([line.text for line in lines])
    return Content(url, title, body)

def scrapeBrookings(url):
    bs = getPage(url)
    title = bs.find('h1').text
    body = bs.find('div', {'class', 'post-body'}).text
    return Content(url, title, body)

url = 'https://www.brookings.edu/blog/future-development/2018/01/26/'
    'delivering-inclusive-urban-access-3-uncomfortable-truths/'
content = scrapeBrookings(url)
print('Title: {}'.format(content.title))
print('URL: {}\n'.format(content.url))
print(content.body)

url = 'https://www.nytimes.com/2018/01/25/opinion/sunday/'
    'silicon-valley-immortality.html'
content = scrapeNYTimes(url)
print('Title: {}'.format(content.title))
print('URL: {}\n'.format(content.url))
print(content.body)

Wenn du anfängst, Scraper-Funktionen für weitere Nachrichtenseiten hinzuzufügen, wirst du feststellen, dass sich ein Muster bildet. Die Parsing-Funktion jeder Seite macht im Wesentlichen das Gleiche:

  • Wählt das Titelelement aus und extrahiert den Text für den Titel

  • Wählt den Hauptinhalt des Artikels aus

  • Wählt nach Bedarf andere Inhalte aus

  • Gibt ein Content Objekt zurück, das mit den zuvor gefundenen Strings instanziiert wurde.

Die einzigen wirklich standortabhängigen Variablen sind die CSS-Selektoren, die verwendet werden, um die einzelnen Informationen zu erhalten. Die Funktionen find und find_all von BeautifulSoup nehmen zwei Argumente entgegen - einen Tag-String und ein Wörterbuch mit Schlüssel/Wert-Attributen -, sodass du diese Argumente als Parameter übergeben kannst, die die Struktur der Website selbst und den Ort der Zieldaten definieren.

Um noch komfortabler zu machen, kannst du die Funktion BeautifulSoup select mit einem einzelnen String-CSS-Selektor für jede Information, die du sammeln willst, verwenden und alle diese Selektoren in einem Wörterbuchobjekt ablegen, anstatt mit all diesen Tag-Argumenten und Schlüssel/Wert-Paaren zu arbeiten:

class Content:
    """
    Common base class for all articles/pages
    """
    def __init__(self, url, title, body):
        self.url = url
        self.title = title
        self.body = body

    def print(self):
        """
        Flexible printing function controls output
        """
        print('URL: {}'.format(self.url))
        print('TITLE: {}'.format(self.title))
        print('BODY:\n{}'.format(self.body))

class Website:
    """ 
    Contains information about website structure
    """
    def __init__(self, name, url, titleTag, bodyTag):
        self.name = name
        self.url = url
        self.titleTag = titleTag
        self.bodyTag = bodyTag

Beachte, dass die Klasse Website keine Informationen speichert, die von den einzelnen Seiten selbst gesammelt werden, sondern Anweisungen, wie diese Daten gesammelt werden sollen. Sie speichert nicht den Titel "Mein Seitentitel". Sie speichert lediglich den String-Tag h1, der angibt, wo die Titel zu finden sind. Deshalb heißt die Klasse Website (die Informationen hier beziehen sich auf die gesamte Website) und nicht Content (die Informationen von nur einer einzigen Seite enthält).

Mit diesen Klassen Content und Website kannst du dann eine Crawler schreiben, um den Titel und den Inhalt jeder URL, die für eine bestimmte Webseite angegeben wird, von einer bestimmten Website zu scrapen:

import requests
from bs4 import BeautifulSoup

class Crawler:
    def getPage(self, url):
        try:
            req = requests.get(url)
        except requests.exceptions.RequestException:
            return None        
        return BeautifulSoup(req.text, 'html.parser')

    def safeGet(self, pageObj, selector):
        """
        Utility function used to get a content string from a
​    ​    Beautiful Soup object and a selector. Returns an empty
​    ​    string if no object is found for the given selector
        """
        selectedElems = pageObj.select(selector)
        if selectedElems is not None and len(selectedElems) > 0:
            return '\n'.join(
​    ​    ​    [elem.get_text() for elem in selectedElems])
        return ''

    def parse(self, site, url):
        """
        Extract content from a given page URL
        """
        bs = self.getPage(url)
        if bs is not None:
            title = self.safeGet(bs, site.titleTag)
            body = self.safeGet(bs, site.bodyTag)
            if title != '' and body != '':
                content = Content(url, title, body)
                content.print()

Und hier ist der Code, der die Website-Objekte definiert und den Prozess in Gang setzt:

crawler = Crawler()

siteData = [
    ['O\'Reilly Media', 'http://oreilly.com',
​    'h1', 'section#product-description'],
    ['Reuters', 'http://reuters.com', 'h1',
​    'div.StandardArticleBody_body_1gnLA'],
    ['Brookings', 'http://www.brookings.edu',
​    'h1', 'div.post-body'],
    ['New York Times', 'http://nytimes.com',
​    'h1', 'div.StoryBodyCompanionColumn div p']
]
websites = []
for row in siteData:
    websites.append(Website(row[0], row[1], row[2], row[3]))

crawler.parse(websites[0], 'http://shop.oreilly.com/product/'\
​    '0636920028154.do')
crawler.parse(websites[1], 'http://www.reuters.com/article/'\
​    'us-usa-epa-pruitt-idUSKBN19W2D0')
crawler.parse(websites[2], 'https://www.brookings.edu/blog/'\
​    'techtank/2016/03/01/idea-to-retire-old-methods-of-policy-education/')
crawler.parse(websites[3], 'https://www.nytimes.com/2018/01/'\
​    '28/business/energy-environment/oil-boom.html')

Auch wenn diese neue Methode auf den ersten Blick nicht wesentlich einfacher erscheint als das Schreiben einer neuen Python-Funktion für jede neue Website, stell dir vor, was passiert, wenn du von einem System mit 4 Website-Quellen zu einem System mit 20 oder 200 Quellen übergehst.

Jede Liste von Strings ist relativ einfach zu schreiben. Sie nimmt nicht viel Platz weg. Sie kann aus einer Datenbank oder einer CSV-Datei geladen werden. Sie kann aus einer entfernten Quelle importiert oder an einen Nicht-Programmierer mit etwas Frontend-Erfahrung weitergegeben werden, damit er sie ausfüllt und neue Websites hinzufügt, ohne dass er auch nur eine Codezeile sehen muss.

Der Nachteil ist natürlich, dass du ein gewisses Maß an Flexibilität aufgibst. Im ersten Beispiel erhält jede Website ihre eigene Freiformfunktion, mit der sie den HTML-Code nach Bedarf auswählen und parsen kann, um das Endergebnis zu erhalten. Im zweiten Beispiel muss jede Website eine bestimmte Struktur haben, in der die Existenz von Feldern garantiert ist, die Daten müssen sauber aus dem Feld kommen und jedes Zielfeld muss einen einzigartigen und zuverlässigen CSS-Selektor haben.

Ich glaube jedoch, dass die Leistungsfähigkeit und relative Flexibilität dieses Ansatzes seine tatsächlichen oder vermeintlichen Mängel mehr als wettmacht. Im nächsten Abschnitt geht es um spezifische Anwendungen und Erweiterungen dieser grundlegenden Vorlage, damit du zum Beispiel mit fehlenden Feldern umgehen, verschiedene Arten von Daten sammeln, nur bestimmte Teile einer Website crawlen und komplexere Informationen über Seiten speichern kannst.

Crawler strukturieren

Die Erstellung von flexiblen und anpassbaren Website-Layout-Typen bringt nicht viel, wenn du immer noch jeden Link, den du crawlen willst, von Hand suchen musst. Im vorigen Kapitel wurden verschiedene Methoden vorgestellt, mit denen du Websites crawlen und neue Seiten auf automatisierte Weise finden kannst.

Dieser Abschnitt von zeigt, wie du diese Methoden in einen gut strukturierten und erweiterbaren Website-Crawler einbaust, der automatisiert Links sammeln und Daten entdecken kann. Ich stelle hier nur drei grundlegende Web-Crawler-Strukturen vor, obwohl ich glaube, dass sie für die meisten Situationen gelten, die du wahrscheinlich brauchst, wenn du Websites in freier Wildbahn crawlst, vielleicht mit ein paar Änderungen hier und da. Wenn du mit deinem eigenen Crawling-Problem auf eine ungewöhnliche Situation stößt, hoffe ich, dass du diese Strukturen als Inspiration nutzt, um einen eleganten und robusten Crawler zu entwerfen.

Websites durch die Suche crawlen

Eine der einfachsten Möglichkeiten, eine Website zu crawlen, ist die gleiche Methode, die auch Menschen anwenden: die Suchleiste. Obwohl die Suche auf einer Website nach einem Stichwort oder Thema und das Sammeln einer Liste von Suchergebnissen eine Aufgabe zu sein scheint, die von Website zu Website sehr unterschiedlich ist, machen einige wichtige Punkte dies überraschend trivial:

  • Die meisten Websites rufen eine Liste von Suchergebnissen für ein bestimmtes Thema ab, indem sie dieses Thema als String über einen Parameter in der URL übergeben. Zum Beispiel: http://example.com?search=myTopic. Der erste Teil dieser URL kann als Eigenschaft des Website Objekts gespeichert werden und das Thema kann einfach daran angehängt werden.

  • Nach der Suche präsentieren die meisten Websites die gefundenen Seiten als leicht identifizierbare Liste von Links, meist mit einem praktischen umgebenden Tag wie <span class="result">, dessen genaues Format auch als Eigenschaft des Website Objekts gespeichert werden kann.

  • Jeder Ergebnislink ist entweder eine relative URL (z. B. /articles/page.html) oder eine absolute URL (z. B. http://example. com/articles/page.html). Ob du eine absolute oder relative URL erwartest, kannst du als Eigenschaft des Website Objekts speichern.

  • Nachdem du die URLs auf der Suchseite ausfindig gemacht und normalisiert hast, hast du das Problem erfolgreich auf das Beispiel im vorherigen Abschnitt reduziert - das Extrahieren von Daten aus einer Seite, die ein Website-Format hat.

Schauen wir uns unter eine Implementierung dieses Algorithmus in Code an. Die Klasse Content ist weitgehend identisch mit den vorherigen Beispielen. Du fügst die URL-Eigenschaft hinzu, damit du weißt, wo der Inhalt gefunden wurde:

class Content:
    """Common base class for all articles/pages"""

    def __init__(self, topic, url, title, body):
        self.topic = topic
        self.title = title
        self.body = body
        self.url = url

    def print(self):
        """
        Flexible printing function controls output
        """
        print('New article found for topic: {}'.format(self.topic))
        print('URL: {}'.format(self.url))
        print('TITLE: {}'.format(self.title))
        print('BODY:\n{}'.format(self.body))    

Die Klasse Website wurde um ein paar neue Eigenschaften erweitert. Die searchUrl definiert, wohin du gehen sollst, um Suchergebnisse zu erhalten, wenn du das gesuchte Thema anhängst. Die resultListing definiert die "Box", die Informationen über jedes Ergebnis enthält, und die resultUrl definiert das Tag innerhalb dieser Box, das dir die genaue URL des Ergebnisses anzeigt. Die Eigenschaft absoluteUrl ist ein Boolescher Wert, der dir sagt, ob es sich bei den Suchergebnissen um absolute oder relative URLs handelt.

class Website:
    """Contains information about website structure"""

    def __init__(self, name, url, searchUrl, resultListing,
​    ​    resultUrl, absoluteUrl, titleTag, bodyTag):
        self.name = name
        self.url = url
        self.searchUrl = searchUrl
        self.resultListing = resultListing
        self.resultUrl = resultUrl
        self.absoluteUrl=absoluteUrl
        self.titleTag = titleTag
        self.bodyTag = bodyTag

crawler.py wurde ein wenig erweitert und enthält unsere Website Daten, eine Liste von Themen, nach denen gesucht werden soll, und zwei Schleifen, die alle Themen und Websites durchlaufen. Außerdem enthält sie eine search Funktion, die zur Suchseite für eine bestimmte Website und ein bestimmtes Thema navigiert und alle auf dieser Seite aufgeführten Ergebnis-URLs extrahiert.

import requests
from bs4 import BeautifulSoup

class Crawler:

    def getPage(self, url):
        try:
            req = requests.get(url)
        except requests.exceptions.RequestException:
            return None
        return BeautifulSoup(req.text, 'html.parser')

    def safeGet(self, pageObj, selector):
        childObj = pageObj.select(selector)
        if childObj is not None and len(childObj) > 0:
            return childObj[0].get_text()
        return ''

    def search(self, topic, site):
        """
        Searches a given website for a given topic and records all pages found
        """
        bs = self.getPage(site.searchUrl + topic)
        searchResults = bs.select(site.resultListing)
        for result in searchResults:
            url = result.select(site.resultUrl)[0].attrs['href']
            # Check to see whether it's a relative or an absolute URL
            if(site.absoluteUrl):
                bs = self.getPage(url)
            else:
                bs = self.getPage(site.url + url)
            if bs is None:
                print('Something was wrong with that page or URL. Skipping!')
                return
            title = self.safeGet(bs, site.titleTag)
            body = self.safeGet(bs, site.bodyTag)
            if title != '' and body != '':
                content = Content(topic, title, body, url)
                content.print()


crawler = Crawler()

siteData = [
    ['O\'Reilly Media', 'http://oreilly.com', 'https://ssearch.oreilly.com/?q=',
    'article.product-result', 'p.title a', True, 'h1',
    'section#product-description'],
    ['Reuters', 'http://reuters.com', 'http://www.reuters.com/search/news?blob=',
    'div.search-result-content', 'h3.search-result-title a', False, 'h1',
    'div.StandardArticleBody_body_1gnLA'],
    ['Brookings', 'http://www.brookings.edu',
    'https://www.brookings.edu/search/?s=', 'div.list-content article',
    'h4.title a', True, 'h1', 'div.post-body']
]
sites = []
for row in siteData:
    sites.append(Website(row[0], row[1], row[2],
                         row[3], row[4], row[5], row[6], row[7]))

topics = ['python', 'data science']
for topic in topics:
    print('GETTING INFO ABOUT: ' + topic)
    for targetSite in sites:
        crawler.search(topic, targetSite)

Dieses Skript durchläuft alle Themen in der Liste topics und meldet sich, bevor es mit dem Scraping nach einem Thema beginnt:

GETTING INFO ABOUT python

Dann geht es in einer Schleife durch alle Seiten in der Liste sites und crawlt jede einzelne Seite für jedes einzelne Thema. Jedes Mal, wenn er erfolgreich Informationen über eine Seite abruft, gibt er sie auf der Konsole aus:

New article found for topic: python
URL: http://example.com/examplepage.html
TITLE: Page Title Here
BODY: Body content is here

Beachte, dass die Schleife durch alle Themen und dann durch alle Websites in der inneren Schleife läuft. Warum machen wir es nicht andersherum, indem wir alle Themen von einer Website und dann alle Themen von der nächsten Website sammeln? Indem du zuerst alle Themen durchläufst, kannst du die Last, die auf einem Webserver liegt, gleichmäßiger verteilen. Das ist besonders wichtig, wenn du eine Liste mit Hunderten von Themen und Dutzenden von Websites hast. Du stellst nicht zehntausende von Anfragen an eine Website auf einmal, sondern du stellst 10 Anfragen, wartest ein paar Minuten, stellst weitere 10 Anfragen, wartest ein paar Minuten und so weiter.

Obwohl die Anzahl der Anfragen in beiden Fällen die gleiche ist, ist es im Allgemeinen besser, diese Anfragen so gut wie möglich über die Zeit zu verteilen. Wenn du darauf achtest, wie deine Schleifen strukturiert sind, kannst du das leicht erreichen.

Websites über Links crawlen

Im vorangegangenen Kapitel hast du einige Möglichkeiten kennengelernt, wie du interne und externe Links auf Webseiten identifizierst und dann diese Links zum Crawlen der Website verwendest. In diesem Abschnitt kombinierst du die gleichen grundlegenden Methoden zu einem flexibleren Website-Crawler, der jedem Link folgen kann, der einem bestimmten URL-Muster entspricht.

Diese Art von Crawler eignet sich gut für Projekte, bei denen du alle Daten einer Website erfassen willst - nicht nur die Daten eines bestimmten Suchergebnisses oder Seiteneintrags. Er eignet sich auch gut, wenn die Seiten der Website unübersichtlich oder weit verstreut sind.

Diese Arten von Crawlern benötigen keine strukturierte Methode zum Auffinden von Links, wie im vorherigen Abschnitt über das Crawlen von Suchergebnissen beschrieben. Daher sind die Attribute, die die Suchseite beschreiben, im Website Objekt nicht erforderlich. Da der Crawler jedoch keine spezifischen Anweisungen für die Positionen der Links erhält, nach denen er sucht, brauchst du Regeln, die ihm sagen, welche Seiten er auswählen soll. Du gibst einen targetPattern (regulären Ausdruck für die Ziel-URLs) an und hinterlässt die boolesche Variable absoluteUrl, um dies zu erreichen:

class Website:
    def __init__(self, name, url, targetPattern, absoluteUrl, titleTag, bodyTag):
        self.name = name
        self.url = url
        self.targetPattern = targetPattern
        self.absoluteUrl = absoluteUrl
        self.titleTag = titleTag
        self.bodyTag = bodyTag

class Content:

    def __init__(self, url, title, body):
        self.url = url
        self.title = title
        self.body = body

    def print(self):
        print('URL: {}'.format(self.url))
        print('TITLE: {}'.format(self.title))
        print('BODY:\n{}'.format(self.body))

Die Klasse Content ist dieselbe, die im ersten Crawler-Beispiel verwendet wurde.

Die Klasse Crawler ist so geschrieben, dass sie von der Startseite jeder Website ausgeht, interne Links findet und den Inhalt jedes gefundenen internen Links analysiert:

import re

class Crawler:
    def __init__(self, site):
        self.site = site
        self.visited = []
        
    def getPage(self, url):
        try:
            req = requests.get(url)
        except requests.exceptions.RequestException:
            return None        
        return BeautifulSoup(req.text, 'html.parser')

    def safeGet(self, pageObj, selector):
        selectedElems = pageObj.select(selector)
        if selectedElems is not None and len(selectedElems) > 0:
            return '\n'.join([elem.get_text() for
​    ​    ​    ​    elem in selectedElems])
        return ''
    
    def parse(self, url):
        bs = self.getPage(url)
        if bs is not None:
            title = self.safeGet(bs, self.site.titleTag)
            body = self.safeGet(bs, self.site.bodyTag)
            if title != '' and body != '':
                content = Content(url, title, body)
                content.print()

    def crawl(self):
        """
        Get pages from website home page
        """
        bs = self.getPage(self.site.url)
        targetPages = bs.find_all('a',
​    ​    ​    href=re.compile(self.site.targetPattern))
        for targetPage in targetPages:
            targetPage = targetPage.attrs['href']
            if targetPage not in self.visited:
                self.visited.append(targetPage)
                if not self.site.absoluteUrl:
                    targetPage = '{}{}'.format(self.site.url, targetPage)
                self.parse(targetPage)

reuters = Website('Reuters', 'https://www.reuters.com', '^(/article/)', False,
    'h1', 'div.StandardArticleBody_body_1gnLA')
crawler = Crawler(reuters)
crawler.crawl()

Eine weitere Änderung, die in den vorherigen Beispielen nicht verwendet wurde: Das Objekt Website (in diesem Fall die Variable reuters) ist eine Eigenschaft des Objekts Crawler selbst. Das funktioniert gut, um die besuchten Seiten (visited) im Crawler zu speichern, bedeutet aber, dass für jede Website ein neuer Crawler instanziiert werden muss, anstatt denselben wiederzuverwenden, um eine Liste von Websites zu crawlen.

Ob du dich dafür entscheidest, einen Crawler Website-unabhängig zu machen oder die Website zu einem Attribut des Crawlers zu machen, ist eine Design-Entscheidung, die du im Kontext deiner eigenen Bedürfnisse abwägen musst. Beide Ansätze sind in der Regel in Ordnung.

Zu beachten ist auch, dass dieser Crawler zwar die Seiten der Startseite erfasst, aber nicht weiter crawlt, nachdem alle Seiten erfasst wurden. Vielleicht möchtest du einen Crawler schreiben, der eines der Muster aus Kapitel 3 verwendet und auf jeder Seite, die er besucht, nach weiteren Zielen suchen lässt. Du kannst sogar alle URLs auf jeder Seite verfolgen (nicht nur die, die dem Zielmuster entsprechen), um nach URLs zu suchen, die das Zielmuster enthalten.

Mehrere Seitentypen crawlen

Im Gegensatz zum Crawling durch eine vorher festgelegte Gruppe von Seiten kann das Crawlen durch alle internen Links einer Website eine Herausforderung darstellen, da du nie genau weißt, was du bekommst. Zum Glück gibt es ein paar einfache Methoden, um den Seitentyp zu identifizieren:

Nach der URL
Alle Blog-Beiträge auf einer Website können eine URL enthalten(z.B. http://example.com/blog/title-of-post).
Durch das Vorhandensein oder Fehlen von bestimmten Feldern auf einer Seite
Wenn eine Seite ein Datum, aber keinen Autorennamen hat, könntest du sie als Pressemitteilung kategorisieren. Wenn sie einen Titel, ein Hauptbild und einen Preis, aber keinen Hauptinhalt hat, könnte es sich um eine Produktseite handeln.
Durch das Vorhandensein von bestimmten Tags auf der Seite, um die Seite zu identifizieren
Du kannst die Vorteile von Tags auch dann nutzen, wenn du die Daten innerhalb der Tags nicht sammelst. Dein Crawler könnte nach einem Element wie <div id="related-products"> suchen, um die Seite als Produktseite zu identifizieren, auch wenn der Crawler nicht an den Inhalten der zugehörigen Produkte interessiert ist.

Um den Überblick über mehrere Seitentypen zu behalten, musst du in Python mehrere Typen von Seitenobjekten haben. Das kann auf zwei Arten geschehen:

Wenn die Seiten alle ähnlich sind (d.h. im Wesentlichen die gleichen Inhalte haben), kannst du ein pageType Attribut zu deinem bestehenden Webseitenobjekt hinzufügen:

class Website:
    def __init__(self, name, url, titleTag, bodyTag, pageType):
        self.name = name
        self.url = url
        self.titleTag = titleTag
        self.bodyTag = bodyTag
        self.pageType = pageType

Wenn du diese Seiten in einer SQL-ähnlichen Datenbank speicherst, deutet diese Art von Muster darauf hin, dass alle diese Seiten wahrscheinlich in derselben Tabelle gespeichert werden und dass eine zusätzliche pageType Spalte hinzugefügt wird.

Wenn sich die Seiten/Inhalte, die du auslesen willst, stark voneinander unterscheiden (sie enthalten unterschiedliche Arten von Feldern), kann es sinnvoll sein, für jeden Seitentyp neue Objekte zu erstellen. Natürlich gibt es einige Dinge, die allen Webseiten gemeinsam sind - sie haben alle eine URL und wahrscheinlich auch einen Namen oder einen Seitentitel. Dies ist eine ideale Situation, um Unterklassen zu verwenden:

class Webpage:
    def __init__(self, name, url, titleTag):
        self.name = name
        self.url = url
        self.titleTag = titleTag

Dies ist kein Objekt, das direkt von deinem Crawler verwendet wird, sondern ein Objekt, das von deinen Seitentypen referenziert wird:

class Product(Website):
    """Contains information for scraping a product page"""
    def __init__(self, name, url, titleTag, productNumberTag, priceTag):
        Website.__init__(self, name, url, TitleTag)
        self.productNumberTag = productNumberTag
        self.priceTag = priceTag

class Article(Website):
    """Contains information for scraping an article page"""
    def __init__(self, name, url, titleTag, bodyTag, dateTag):
        Website.__init__(self, name, url, titleTag)
        self.bodyTag = bodyTag
        self.dateTag = dateTag

Diese Produktseite erweitert die Basisklasse Website und fügt die Attribute productNumber und price, die nur für Produkte gelten, und die Klasse Article fügt die Attribute body und date hinzu, die nicht für Produkte gelten.

Mit diesen beiden Klassen kannst du z.B. eine Laden-Website scrapen, die neben Produkten auch Blogbeiträge oder Pressemitteilungen enthält.

Nachdenken über Web Crawler Modelle

Das Sammeln von Informationen aus dem Internet kann wie das Trinken aus einem Feuerwehrschlauch sein. Es gibt eine Menge Zeug da draußen, und es ist nicht immer klar, was du brauchst und wie du es brauchst. Der erste Schritt bei jedem großen Web Scraping-Projekt (und auch bei einigen kleinen) sollte darin bestehen, diese Fragen zu beantworten.

Wenn du ähnliche Daten in verschiedenen Bereichen oder aus verschiedenen Quellen sammelst, solltest du fast immer versuchen, sie zu normalisieren. Der Umgang mit Daten mit identischen und vergleichbaren Feldern ist viel einfacher als der Umgang mit Daten, die vollständig vom Format ihrer ursprünglichen Quelle abhängig sind.

In vielen Fällen solltest du bei der Entwicklung von Scrapern davon ausgehen, dass in Zukunft weitere Datenquellen hinzukommen werden, um den Programmieraufwand zu minimieren, der für das Hinzufügen dieser neuen Quellen erforderlich ist. Auch wenn eine Website auf den ersten Blick nicht in dein Modell zu passen scheint, kann es sein, dass sie auf subtilere Weise damit übereinstimmt. Wenn du in der Lage bist, diese zugrundeliegenden Muster zu erkennen, kannst du dir auf lange Sicht Zeit, Geld und eine Menge Kopfschmerzen sparen.

Auch die Verbindungen zwischen den einzelnen Daten sollten nicht außer Acht gelassen werden. Suchst du nach Informationen, die Eigenschaften wie "Typ", "Größe" oder "Thema" haben, die sich über mehrere Datenquellen erstrecken? Wie speicherst, rufst du diese Attribute ab und konzeptualisierst sie?

Softwarearchitektur ist ein umfangreiches und wichtiges Thema, dessen Beherrschung eine ganze Karriere in Anspruch nehmen kann. Glücklicherweise ist die Software-Architektur für Web Scraping ein sehr viel begrenzteres und überschaubareres Wissen, das relativ leicht erworben werden kann. Wenn du mit dem Scrapen von Daten fortfährst, wirst du wahrscheinlich immer wieder auf die gleichen Grundmuster stoßen. Um einen gut strukturierten Web Scraper zu erstellen, brauchst du nicht viel geheimnisvolles Wissen, aber du musst dir einen Moment Zeit nehmen, um über dein Projekt nachzudenken.

Get Web Scraping mit Python, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.