{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Fortsetzung BeautifulSoup" ] }, { "cell_type": "markdown", "source": [ "Letzte Woche haben wir alle Zitate von der ersten Seite der Website [https://quotes.toscrape.com](https://quotes.toscrape.com) extrahiert.\n", "Heute werden wir den Code in drei Aspekten ergänzen:\n", "\n", "1. Zitate von der ersten Seite mit Metadaten extrahieren\n", "2. Zitate von allen Seiten extrahieren, mit und ohne Metadaten\n", "3. Daten in Dateien schreiben: Beispiel pandas DataFrame in Excel-Tabelle\n", "\n", "Für die Ausführung des Codes brauchen wir zusätzlich zu den Bibliotheken requests und bs4 außerdem noch die Bibliothek pandas, und eine Funktion aus der Bibliothek urllib3. Zusätzlich könnt ihr das Modul memory_profiler installieren, das erlaubt, zu messen, wieviel Speicher zum Ausführen einer Codezelle benötigt wird. Daneben installieren wir ein Paket openpyxl, das zum Schreiben von pandas-DataFrames in Excel-Dateien verwendet wird. Die Bibliothek müssen wir aber nicht laden, weil sie automatisch geladen wird, wenn wir später versuchen, einen Pandas DataFrame in eine Excel-Datei zu schreiben." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "#import sys\n", "#!conda install --yes --prefix {sys.prefix} memory_profiler\n", "#!conda install --yes --prefix {sys.prefix} openpyxl" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "import requests\n", "from bs4 import BeautifulSoup\n", "import pandas as pd\n", "# %load_ext memory_profiler" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## Recap: Zitate von der ersten Seite extrahieren, ohne Metadaten" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "URL = \"https://quotes.toscrape.com/\"\n", "page = requests.get(URL)\n", "\n", "soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", "zitate = soup.find_all(\"span\", class_=\"text\")\n", "\n", "for zitat in zitate:\n", " print(zitat.get_text())" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## Zitate von der ersten Seite extrahieren, mit Metadaten" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "In der letzten Stunde haben wir die BeautifulSoup-Methoden .find() und .find_all() verwendet, um HTML-Elemente in einem BeautifulSoup-Objekt zu finden. Genauso könnten wir auch vorgehen, um neben den Zitaten auch einige Metadaten zu extrahieren, also zum Beispiel auch den Namen der Person, von der das Zitat stammt, und die Tags, mit denen das Zitat versehen wurde." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes = soup.find_all(\"div\", class_=\"quote\")\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "for quote in quotes:\n", " quote_text = quote.find(\"span\", class_=\"text\").get_text()\n", " quote_author = quote.find(\"small\", class_=\"author\").get_text()\n", " quote_tags = quote.find_all(\"a\", class_=\"tag\")\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.get_text())\n", " quotes_dict[\"Text\"].append(quote_text)\n", " quotes_dict[\"Author\"].append(quote_author)\n", " quotes_dict[\"Tags\"].append(tags_text)\n" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# dictionary ist nicht besonders übersichtlich\n", "quotes_dict" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Dataframe ist übersichtlicher\n", "quotes_df = pd.DataFrame.from_dict(quotes_dict)\n", "quotes_df" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Anstelle der Methode .get_text() kann auch das Attribut .text abgerufen werden: Beide geben den Textinhalt des Elements zurück.\n", "\n", "Aber was passiert, wenn ein Element nicht gefunden werden kann, beispielsweise, weil für ein Zitat keine Tags angegeben wurden, oder wenn die Angabe der Autor:in fehlt? In diesem Fall würden die Methoden .find() bzw. .find_all() den Wert None zurückgeben. Die Methode .get_text() (oder das Attribut .text) würde dann auf ein Objekt vom Typ NoneType angewandt werden. Aber NoneType-Objekte haben keine Methode .get_text() und auch kein Attribut .text! Der Code würde also eine Fehlermeldung produzieren und die Ausführung abbrechen. Um das zu verhindern, könnten wir zunächst überprüfen, ob tatsächlich ein Element gefunden wurde. Nur, wenn ein Element gefunden wurde, wird der Text extrahiert, im folgenden Beispiel mithilfe des Attributs .text statt der Methode .get_text(). Wenn das Element nicht gefunden wird, wird eine Nachricht ausgegeben, um dies zu registrieren." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes = soup.find_all(\"div\", class_=\"quote\")\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "for quote in quotes:\n", " quote_text = quote.find(\"span\", class_=\"text\")\n", " quote_author = quote.find(\"small\", class_=\"author\")\n", " quote_tags = quote.find_all(\"a\", class_=\"tag\")\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.text)\n", " if quote_text is not None:\n", " quotes_dict[\"Text\"].append(quote_text.text)\n", " else:\n", " print(\"Zitat hat keinen Text\")\n", " if quote_author is not None:\n", " quotes_dict[\"Author\"].append(quote_author.text)\n", " else:\n", " print(\"Zitat hat keinen Autor\")\n", " if len(tags_text) != 0:\n", " quotes_dict[\"Tags\"].append(tags_text)\n", " else:\n", " print(\"Zitat hat keine Tags\")\n" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Der Einfachheit halber verwenden wir aber im Folgenden den Code ohne die zusätzliche if-else-Verzweigung. Die Suche mit find() und find_all() produziert bei komplexeren Abfragen aber auch bereits ohne die if-else Verzweigung unübersichtlichen Code, weil wir als Argument zusätzlich die CSS-Klasse des gesuchten Elements angeben müssen. Eine einfachere Möglichkeit, direkt nach der CSS-Klasse selbst zu suchen, sind die Methoden .select_one() und .select().\n", "\n", "Zum Nachlesen: [https://beautiful-soup-4.readthedocs.io/en/latest/index.html?highlight=select#css-selectors](https://beautiful-soup-4.readthedocs.io/en/latest/index.html?highlight=select#css-selectors)" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes = soup.select(\"div.quote\")\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "for quote in quotes:\n", " quote_text = quote.select_one(\"span.text\") # oder einfach '.text'\n", " quote_author = quote.select_one(\"small.author\")\n", " quote_tags = quote.select(\"a.tag\")\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.text)\n", " quotes_dict[\"Text\"].append(quote_text.text)\n", " quotes_dict[\"Author\"].append(quote_author.text)\n", " quotes_dict[\"Tags\"].append(tags_text)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Die Suche nach CSS-Selektoren mithilfe der .select_one() und .select() Methoden hat außerdem einen weiteren Vorteil: Sie erlauben, direkt nach Kind- oder Geschwisterelementen eines Elements zu suchen. Bei der Verwendung von .find() und .find_all() hatten wir eine for-Schleife verwendet, um die Suche auf Kindelemente der div-Elemente mit der Klasse 'quote' einzuschränken. In manchen Fällen kann die Verwendung von .select() anstelle von .find_all() eine solche for-Schleife ersetzen. In unserem Beispiel würde zur Suche nach Zitattexten und Autor:innennamen beispielsweise keine for-Schleife erfordern:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Zitattexte: 'div.quote > span.text' findet direkte Kindelemente des div-Elements mit der Klasse 'quote'\n", "soup.select(\"div.quote > span.text\")" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Autor:innen: 'div.quote small.author' findet alle small-Elemente mit der Klasse 'author' innerhalb de des div-Elements mit der Klasse 'quote', auch \"Enkelkinder\"\n", "soup.select(\"div.quote small.author\")" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "`````{admonition} Verständnisfragen\n", ":class: tip\n", "- Recherchiert in den BeautifulSoup-Dokumentationsseiten: Wie kann man nach den Geschwisterelementen eines Elements suchen, das keine Attribute hat? In diesem Fall kann .select() nicht angewandt werden.\n", "\n", "`````" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## Zitate von allen Seiten der Website extrahieren, ohne Metadaten" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Lösung mit for-Schleife: für exakt 10 Unterseiten" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "# Lösungsidee von GitHub-Nutzer:in Bhavya Bindela: https://bhavyasree.github.io/PythonClass/Notebooks/18.scrape-quotes/ . Angepasst für die Extraktion von Zitaten statt Autor:innen\n", "# for Schleife mit Set\n", "\n", "base_url = \"http://quotes.toscrape.com/page/\"\n", "\n", "quotes = set()\n", "\n", "for i in range(1,11):\n", " scrape_url = base_url + str(i)\n", " page = requests.get(scrape_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " for quote in soup.select(\".quote > .text\"):\n", " quotes.add(quote.text) # type(quote) ist bs4.element.Tag: hat Attribut text; add() ist eine set-Methode" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes # sets haben keine Ordnung: das ist unpraktisch" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# haben wir alle zitate extrahiert?\n", "len(quotes)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Diese Lösung nutzt ein python-Set, um die extrahierten Elemente zu speichern. Das ist allerdings etwas unpraktisch, da Sets ungeordnet sind und die Zitate so nicht in chronologischer Reihenfolge gespeichert werden. Es empfiehlt sich deswegen, stattdessen eine Liste zu verwenden:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "base_url = \"http://quotes.toscrape.com/page/\"\n", "\n", "quotes = []\n", "\n", "for i in range (1,11):\n", " scrape_url = base_url + str(i)\n", " page = requests.get(scrape_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " for quote in soup.select(\".quote > .text\"):\n", " quotes.append(quote.text)\n" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Die Ausgabe von %%time und %%memit zeigen, dass die Nutzung des Sets keinen (Effizienz-) Vorteil gegenüber Listen hat: Wir können also genausogut eine Liste verwenden." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Lösung mit while-Schleife: unbekannte Anzahl von Unterseiten" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "# while-Schleife mit Set\n", "# Lösung wieder von GitHub-Nutzer:in Bhavya Bindela: https://bhavyasree.github.io/PythonClass/Notebooks/18.scrape-quotes/ . Angepasst für die Extraktion von Zitaten statt Autor:innen\n", "\n", "page = requests.get(scrape_url)\n", "soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", "page_no = 1\n", "quotes = set()\n", "base_url = \"http://quotes.toscrape.com/page/\"\n", "\n", "while True:\n", " scrape_url = base_url + str(page_no)\n", " page = requests.get(scrape_url)\n", "\n", " # Das funktioniert nur für die Seite quotes.toscrape.com\n", " # Für andere Seiten könnte hier die Bedingung if page.status_code != 200\n", " # getestet werden\n", " if \"No quotes found!\" in page.text:\n", " break\n", "\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " for quote in soup.select(\".quote > .text\"):\n", " quotes.add(quote.text)\n", "\n", " page_no +=1\n" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Die Zeile, die auf den ersten Blick verwundert, ist wahrscheinlich die if-Anweisung: if 'No quotes found!' in page.text: break.\n", "Was hat es damit auf sich?\n", "Die Macher:innen der Seite quotes.toscrape haben sich überlegt, dass auch Seiten, auf denen keine Zitate mehr publiziert sind, existieren sollen, sodass eine HTTP-Anfrage für diese Seiten einen Erfolgscode 200 zurückgeben. Wenn die Seiten nicht existieren würden, könnte einfach die while-Schleife in Abhängigkeit von dem Statuscode abgebrochen werden.\n", "\n", "Das können wir im Vergleich mit einer anderen Seite illustrieren:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# warum if 'No quotes found!' ...?\n", "# Es gibt eine seite 99999: Das ist nur ausnahmsweise auf der Seite quotes.toscrape so.\n", "page = requests.get(\"http://quotes.toscrape.com/page/9999\")\n", "page.status_code" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Auf dieser Seite steht ein einziger Satz\n", "# Durchsucht den String nach dem Satz: Welcher ist es?\n", "page.text" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Anders wäre es z.B. hier:\n", "page = requests.get(\"https://www.projekt-gutenberg.org/balzac/kurtisa2/chap001.html\")\n", "page.status_code # 200\n", "# Es gibt keine Seite 99999\n", "page = requests.get(\"https://www.projekt-gutenberg.org/balzac/kurtisa2/chap99999.html\")\n", "page.status_code # 404" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Auch die while-Schleife können wir wieder zum Erstellen einer Liste anstelle eines Sets verwenden:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "# while-Schleife mit Liste\n", "\n", "page = requests.get(scrape_url)\n", "soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", "page_no = 1\n", "quotes = []\n", "base_url = \"http://quotes.toscrape.com/page/\"\n", "\n", "while True:\n", " scrape_url = base_url + str(page_no)\n", " page = requests.get(scrape_url)\n", "\n", " if \"No quotes found!\" in page.text:\n", " break\n", "\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " for quote in soup.select(\".quote > .text\"):\n", " quotes.append(quote.text)\n", "\n", " page_no +=1" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Anstatt die while-Schleife wie bisher abzubrechen, wenn \"No quotes found!\" auf der Seite steht, kann auch der Next-Button unten rechts auf der Seite zur Formulierung einer Abbruchbedingung genutzt werden. Den Next-Button gibt es nämlich nur auf Seiten, die auch Zitate enthalten. Wir können also eine while-Schleife konstruieren, die zunächst nur den Inhalt der ersten Seite extrahiert, und anschließend den Inhalt aller Unterseiten, die ein Element mit der Klasse \"next\" haben. Das Element mit der Klasse \"next\" hat ein a-Element als Kindelement, dessen Attribut \"href\" als Wert den relativen Pfad der nächsten Unterseite enthält, also zum Beispiel \"/page/2\", \"/page/3\", usw. In jedem Schleifendurchlauf wird überprüft, ob die aktuelle Seite ein a-Element enthält, das Kindelement von einem Element mit der Klasse \"next\" ist. Wenn kein solches Element gefunden wird, dann wird die Schleife mithilfe einer break-Anweisung abgrebrochen." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "base_url = \"http://quotes.toscrape.com\"\n", "current_url = base_url\n", "quotes = []\n", "\n", "while True:\n", " page = requests.get(current_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", " for quote in soup.select(\".quote > .text\"):\n", " quotes.append(quote.text)\n", "\n", " next_elem = soup.select_one(\".next > a\")\n", "\n", " if next_elem is not None:\n", " current_url = next_elem['href'] # auf den Wert des Attributs \"href\" zugreifen\n", " current_url = base_url + current_url\n", " else:\n", " break" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## Zitate von allen Seiten extrahieren, mit Metadaten" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Lösung mit for-Schleife" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "base_url = 'http://quotes.toscrape.com/page/'\n", "\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "for i in range (1,11):\n", " scrape_url = base_url + str(i)\n", " page = requests.get(scrape_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " quotes_divs = soup.select(\".quote\")\n", " for div in quotes_divs:\n", " quotes_dict[\"Text\"].append(div.select_one(\".text\").get_text()) # oder .text\n", " quotes_dict[\"Author\"].append(div.select_one(\".author\").get_text())\n", " quote_tags = div.select(\".tags > a\") # findet Kind-Elemente von dem Element mit der Klasse tags: tags aus der Top Ten Tags-Liste werden nicht gefunden, weil wir bereits in der quote div sind\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.get_text())\n", " quotes_dict[\"Tags\"].append(tags_text)\n", "\n", "quotes_df = pd.DataFrame.from_dict(quotes_dict)" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes_df" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Lösung mit while-Schleife" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "page = requests.get(scrape_url)\n", "soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", "page_no = 1\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "base_url = \"http://quotes.toscrape.com/page/\"\n", "\n", "while True:\n", " scrape_url = base_url + str(page_no)\n", " page = requests.get(scrape_url)\n", "\n", " if \"No quotes found!\" in page.text:\n", " break\n", "\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " quote_divs = soup.select(\".quote\")\n", " for div in quote_divs:\n", " quotes_dict[\"Text\"].append(div.select_one(\".text\").get_text())\n", " quotes_dict[\"Author\"].append(div.select_one(\".author\").get_text())\n", " quote_tags = div.select(\".tags > a\")\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.get_text())\n", " quotes_dict[\"Tags\"].append(tags_text)\n", "\n", " page_no +=1\n", "\n", "quotes_df = pd.DataFrame.from_dict(quotes_dict)" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes_df" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Alternative Lösung, die wieder den Next-Button zur Formulierung der Abbruchbedingung nutzt:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "base_url = \"http://quotes.toscrape.com\"\n", "current_url = base_url\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "while True:\n", " page = requests.get(current_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " quote_divs = soup.select(\".quote\")\n", " for div in quote_divs:\n", " quotes_dict[\"Text\"].append(div.select_one(\".text\").get_text())\n", " quotes_dict[\"Author\"].append(div.select_one(\".author\").get_text())\n", " quote_tags = div.select(\".tags > a\")\n", " tags_text = []\n", " for tag in quote_tags:\n", " tags_text.append(tag.get_text())\n", " quotes_dict[\"Tags\"].append(tags_text)\n", "\n", " next_elem = soup.select_one(\".next > a\")\n", "\n", " if next_elem is not None:\n", " next_path = next_elem['href'] # auf den Wert des Attributs \"href\" zugreifen\n", " current_url = base_url + next_path\n", " else:\n", " break\n", "\n", "quotes_df = pd.DataFrame.from_dict(quotes_dict)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "In allen Lösungen haben wir zwei for-Schleifen ineinander verschachtelt, um die Tags zu allen Zitaten als Liste zu extrahieren. Die Schleife `for tag in tags ...` kann aber auch durch ein etwas übersichtlicheres und effizienteres Konstrukt ersetzt werden, das sich \"List Comprehension\" nennt. Was das genau ist, lernen wir nächste Stunde. So sieht die while-Schleife mit list comprehension aus:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "base_url = \"http://quotes.toscrape.com\"\n", "current_url = base_url\n", "quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", "\n", "while True:\n", " page = requests.get(current_url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", "\n", " quote_divs = soup.select(\".quote\")\n", " for div in quote_divs:\n", " quotes_dict[\"Text\"].append(div.select_one(\".text\").get_text())\n", " quotes_dict[\"Author\"].append(div.select_one(\".author\").get_text())\n", " quote_tags = div.select(\".tags > a\")\n", " tags_text = [tag.get_text() for tag in div.select(\".tags > a\")]\n", " quotes_dict[\"Tags\"].append(tags_text)\n", "\n", " next_elem = soup.select_one(\".next > a\")\n", "\n", " if next_elem is not None:\n", " next_path = next_elem['href'] # auf den Wert des Attributs \"href\" zugreifen\n", " current_url = base_url + next_path\n", " else:\n", " break\n", "\n", "quotes_df = pd.DataFrame.from_dict(quotes_dict)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Lösung mit Funktionen" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Zuletzt wäre auch eine Lösung mithilfe von Funktionsdefinitionen denkbar:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "def get_soup(url):\n", " \"\"\"\n", " Argumente: `url` (String): Die URL der Webseite.\n", " Rückgabewert: `soup` (BeautifulSoup-Objekt): Das BeautifulSoup-Objekt, das den analysierten HTML-Inhalt der Webseite repräsentiert.\n", "\n", " Diese Funktion ruft den HTML-Inhalt der Webseite unter der angegebenen URL mit der requests.get()-Methode ab. Anschließend wird ein BeautifulSoup-Objekt erstellt, indem der HTML-Inhalt mit dem Parser \"html.parser\" analysiert wird. Das resultierende BeautifulSoup-Objekt wird zurückgegeben.\n", " \"\"\"\n", " page = requests.get(url)\n", " soup = BeautifulSoup(page.content, \"html.parser\")\n", " return soup" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 1, "outputs": [], "source": [ "def extract_quotes(soup, quotes_dict):\n", " \"\"\"\n", " Argumente:\n", " `soup` (BeautifulSoup-Objekt): Das BeautifulSoup-Objekt, das den analysierten HTML-Inhalt einer Webseite repräsentiert.\n", " `quotes_dict` (Dictionary): Ein Wörterbuch, das die Zitate enthält.\n", " Rückgabewert:\n", " `quotes_dict` (Dictionary): Das aktualisierte Wörterbuch, das die extrahierten Zitate enthält.\n", "\n", " Die Funktion extrahiert Zitate, Autor:innen, Tags und URLs aus einem BeautifulSoup-Objekt. Sie wählt dazu bestimmte HTML-Elemente mit den entsprechenden Klassen aus und fügt die extrahierten Informationen in das quotes_dict-Wörterbuch ein, das anschließend zurückgegeben wird.\n", " \"\"\"\n", " quote_divs = soup.select(\".quote\")\n", " for div in quote_divs:\n", " quotes_dict[\"Text\"].append(div.select_one(\".text\").get_text())\n", " quotes_dict[\"Author\"].append(div.select_one(\".author\").get_text())\n", " tags_text = [tag.get_text() for tag in div.select(\".tags > a\")]\n", " quotes_dict[\"Tags\"].append(tags_text)\n", " return quotes_dict" ], "metadata": { "collapsed": false, "ExecuteTime": { "start_time": "2023-06-16T14:37:15.126540Z", "end_time": "2023-06-16T14:37:15.132728Z" } } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "def scrape_all_quotes(url, quotes_dict = None):\n", " \"\"\"\n", " Argumente:\n", " `url` (String): Die URL der Webseite, von der die Zitate gesammelt werden sollen.\n", " `quotes_dict` (Dictionary, optional): Ein Wörterbuch, das die Zitate enthält. Wenn nicht angegeben, wird ein neues Wörterbuch erstellt.\n", " Rückgabewert:\n", " `quotes_df` (DataFrame): Ein Pandas DataFrame, der die gesammelten Zitate enthält.\n", "\n", " Die Funktion sammelt Zitate von der angegebenen Webseite und allen Unterseiten. Wenn kein quotes_dict-Wörterbuch bereitgestellt wird, wird ein neues Wörterbuch mit leeren Listen erstellt. Das BeautifulSoup-Objekt wird über get_soup() abgerufen und die extract_quotes()-Funktion extrahiert die Zitate und aktualisiert das Wörterbuch. Wenn keine nächste Seite vorhanden ist (bestimmt durch das Fehlen des \".next\"-Elements), wird ein Pandas DataFrame (quotes_df) aus dem quotes_dict erstellt und zurückgegeben. Andernfalls wird die URL der nächsten Seite abgerufen und scrape_all_quotes() rekursiv mit der nächsten Seiten-URL und dem aktuellen quotes_dict aufgerufen.\n", " \"\"\"\n", " if quotes_dict is None:\n", " quotes_dict = {\"Text\":[], \"Author\":[], \"Tags\":[]}\n", " soup = get_soup(url)\n", " quotes_dict = extract_quotes(soup, quotes_dict)\n", " next_elem = soup.select_one(\".next > a\")\n", "\n", " if next_elem is not None:\n", " next_path = next_elem['href'] # auf den Wert des Attributs \"href\" zugreifen\n", " current_url = base_url + next_path\n", " return scrape_all_quotes(current_url, quotes_dict)\n", " else:\n", " quotes_df = pd.DataFrame.from_dict(quotes_dict)\n", " return quotes_df\n" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "%%time\n", "# %%memit\n", "\n", "# Funktion aufrufen\n", "quotes_df = scrape_all_quotes(\"https://quotes.toscrape.com\")" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "quotes_df" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Bei der Definition von Funktionen beim Web Scraping sind einige Dinge zu beachten:\n", "\n", "Die Funktion `scrape_all_urls()` ist so definiert, dass sie die while-Schleife ersetzt. Um das zu erreichen, wurde ein Prinzip angewandt, das sich **Rekursion** nennt. Dabei ruft sich eine Funktion so lange selbst auf, wie eine bestimmte Bedingung erfüllt ist: Als Rückgabewert gibt die Funktion einen erneuten Funktionsaufruf zurück. Wenn die Bedingung nicht mehr erfüllt ist, wird stattdessen der Dataframe `quotes_df` zurückgegeben. Bei jedem erneuten Aufruf der Funktion wird deswegen das `quotes_dict` als zusätzliches Argument übergeben: So kann das Dictionary bei jedem Aufruf der Funktion weiter befüllt werden. Wichtig ist jedoch zu beachten, dass in Python festgelegt ist, wie oft eine Funktion sich selbst aufrufen darf (das heißt dann \"maximale Rekursionstiefe\"). Wenn die erlaubte Anzahl an Aufrufen überschritten wird, kommt es zu einem schwerwiegenden Fehler, der sich \"Stack Overflow\" nennt. Bei der Verwendung von Rekursion beim Web Scraping ist also Vorsicht geboten! Zwar sind wir beim Scrapen der Seite quotes.toscrape.com noch weit davon entfernt, diese Anzahl zu überschreiten, aber für größere Webscraping-Projekte ist das ein Problem, das berücksichtigt werden muss.\n", "\n", "Mehr Informationen zum Thema Rekursion in Python findet ihr [hier](https://realpython.com/python-recursion/).\n", "\n", "Ein weiterer Aspekt, der bei der Definition von Funktionen zu beachten ist, ist die Verwendung von Defaultargumenten. **Defaultargumente** sind Werte, die bereits in der Funktionsdefinition für einen Parameter angegeben werden. Diese Werte werden automatisch verwendet, wenn beim Funktionsaufruf kein expliziter Wert für das entsprechende Argument angegeben wird. Im ersten Moment würde es vielleicht intuitiv erscheinen, als Defaultargument für die Funktion `scrape_all_urls()` ein leeres Dictionary `quotes_dict` festzulegen, das bei den nachfolgenden Funktionsaufrufen befüllt wird. Allerdings ist das keine gute Idee: Wenn Defaultargumente einen veränderbaren Datentyp haben, werden sie in Python bei einem wiederholten Funktionsaufruf \"mitgenommen\" und nicht durch den Default-Wert ersetzt. Das heißt, dass bei jedem erneuten Funktionsaufruf der `quotes_df` DataFrame wächst, weil das `quotes_dict` nicht nur die Elemente des aktuellen Funktionsaufrufs, sondern auch die Elemente aller vorhergegangener Funktionsaufurfe enthält. Statt direkt das Dictionary als Defaultargument festzulegen, sollte deswegen lieber zunächst None als Defaultwert festgelegt werden. Das Dictionary kann dann mithilfe einer bedingten Anweisung im Funktionskörper erstellt werden, nämlich genau dann, wenn das `quotes_dict` beim Funktionsaufruf None ist. Das ist nur beim ersten Funktionsaufruf der Fall.\n", "\n", "Mehr Informationen zum Umgang mit Defaultargumenten findet ihr [hier](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments)." ], "metadata": { "collapsed": false } }, { "attachments": {}, "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "## Pandas-DataFrame in Exceldatei schreiben\n", "\n", "Zuletzt wollen wir die extrahierten Daten auf unserem Computer abspeichern. Um einen Pandas DataFrame zu speichern, gibt es verschiedene Methoden. Die Methode .to_excel() erlaubt zum Beispiel, einen DataFrame in einer Exceltabelle zu speichern, also in einer Datei mit der Dateiendung .xlsx. Für \"speichern\" sagt man in diesem Kontext auch \"schreiben\": Daten werden in eine Datei geschrieben.\n", "\n", "Die Methode .to_excel() greift unter der Motorhaube auf ein Paket mit dem Namen openpyxl zurück. Diese Paket mussten wir deswegen am Anfang installieren.\n", "\n", "Dokumentation: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_excel.html" ] }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# quotes_df.to_excel(\"quotes_df.xlsx\", index=False) # default-encoding ist UTF-8" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## \\%%time und \\%%memit: Was ist das?\n", "\n", "Wir haben beim Ausführen der Codezellen in diesem Jupyter Notebook jeweils zwei Zeilen am Anfang hinzugefügt:\n", "\n", "- \\%%time berechnet die Laufzeit einer Jupyter Notebook Codezelle.\n", "- \\%%memit berechnet, wie viel Speicher für die Ausführung der Codezelle benötigt wird.\n", "\n", "Damit können wir vergleichen, welche Lösung am effizientesten ist. Mehr Informationen findet ihr wie immer in den Dokumentationsseiten:\n", "\n", "- https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-time\n", "- https://ipython-books.github.io/44-profiling-the-memory-usage-of-your-code-with-memory_profiler/" ], "metadata": { "collapsed": false } } ], "metadata": { "kernelspec": { "name": "python3", "language": "python", "display_name": "Python 3 (ipykernel)" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.0" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }