{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Einsteig BeautifulSoup"
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "\n",
    "Zum Einstieg in BeautifulSoup wollen wir ein paar Zitate von der Webseite [https://quotes.toscrape.com/](https://quotes.toscrape.com/) scrapen. Für die Übung werden wir zwei Pakete verwenden: Requests und BeautifulSoup. Öffnet jetzt ein neues Jupyter Notebook in JupyerLab und kopiert nacheinander den Code von dieser Seite in euer Jupyter Notebook. Zunächst installieren und importieren wir die Pakete requests und BeautifulSoup:\n"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "#import sys\n",
    "#!conda install --yes --prefix {sys.prefix} requests\n",
    "#!conda install --yes --prefix {sys.prefix} beautifulsoup4"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import requests\n",
    "from bs4 import BeautifulSoup"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "## HTTP-Anfrage stellen mit requests\n",
    "\n",
    "Requests Dokumentation: [https://requests.readthedocs.io/](https://requests.readthedocs.io/)\n",
    "\n",
    "Bevor wir die Anfrage stellen, sollten wir überprüfen, ob die Seite eine robots.txt hat:\n",
    "[https://quotes.toscrape.com/robots.txt](https://quotes.toscrape.com/robots.txt). Die Seite hat keine robots.txt, das heißt, wir müssen uns beim Scrapen der Seite an keine besonderen Vorgaben richten. Jetzt können wir die Anfrage an den Server der Webseite stellen. Dazu brauchen wir nur die URL der Seite und die Funktion `get()` aus dem Paket requests. Die Funktion `get()` formuliert die Anfrage nach den Vorgaben des HTTP-Protokolls. Als Argumente können wir der Funktion genau die Parameter übergeben, die für Header der HTTP-Request definiert sind, also zum Beispiel die Parameter \"User-Agent\" oder \"Accept\", die wir bereits im Beispiel im Kapitel über HTTP gesehen haben. Für den Einstieg verwenden wir einfach die Default-Parameter:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# HTTP get-Request stellen\n",
    "URL = \"https://quotes.toscrape.com/\"\n",
    "page = requests.get(URL)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Jetzt können wir die HTTP Response untersuchen:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Unser Objekt page mit der HTTP-Response hat den Typ 'requests.models.Response'\n",
    "# Objekte von diesem Typ haben Attribute status_code, headers und text, die wir abrufen können\n",
    "type(page)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Attribut Statuscode abrufen\n",
    "page.status_code"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Header der HTTP Response abrufen\n",
    "page.headers"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Body der HTTP Response abrufen\n",
    "page.text"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Body der Response ist ein String\n",
    "type(page.text)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Body als bytes-Objekt\n",
    "page.content"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "## HTML-Dokument parsen und Inhalte extrahieren mit BeautifulSoup\n",
    "\n",
    "BeautifulSoup Dokumentation: [https://beautiful-soup-4.readthedocs.io](https://beautiful-soup-4.readthedocs.io)\n",
    "\n",
    "Dokumentation speziell zu BeautifulSoup-Klassen: [https://tedboy.github.io/bs4_doc/generated/bs4.html#classes](https://tedboy.github.io/bs4_doc/generated/bs4.html#classes)\n",
    "\n",
    "Die `get()`-Funktion hat den Body der HTTP-Response als String geliefert. Zwar könnten wir auch einen String durchsuchen, aber es wäre viel praktischer, wenn wir einfach die einzelnen HTML-Elemente als Attribute abrufen könnten, also genau so, wie wir den Statuscode, Header und Body der HTTP-Response einfach als Attribute eines Response-Objekts abrufen konnten.\n",
    "Genau dazu wurde das Paket BeautifulSoup entwickelt. Mit der folgenden Codezeile kann aus dem Body der HTTP-Response ein BeautifulSoup-Objekt erstellt werden, dessen Attribute gängige HTML-Elemente wie head, title und body sind:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# BeautifulSoup() nimmt den Body einer HTTP-Response als bytes-Objekt an\n",
    "soup = BeautifulSoup(page.content, \"html.parser\")\n",
    "type(soup)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Was ist ein BeautifulSoup Objekt?\n",
    "\n",
    "> \"The BeautifulSoup object represents the parsed document as a whole.\"\n",
    "\n",
    "Quelle: [https://beautiful-soup-4.readthedocs.io/en/latest/#beautifulsoup](https://beautiful-soup-4.readthedocs.io/en/latest/#beautifulsoup)\n",
    "\n",
    "Attribute des BeautifulSoup-Objekts abrufen:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# HTML-Element head\n",
    "soup.head"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# HTML-Element title\n",
    "soup.title"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# HTML-Element body\n",
    "# Hier auskommentiert, da die Ausgabe sehr lang ist\n",
    "# soup.body"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Methoden des BeautifulSoup-Objekts aufrufen:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# .prettify() formatiert den body der HTTP-Response übersichtlich\n",
    "# Hier auskommentiert, da die Ausgabe sehr lang ist\n",
    "# print(soup.prettify())"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Wie eingangs erläutert wollen wir die Zitate von der Seite scrapen. Wenn wir die Seite in den Chrome-Entwicklertools untersuchen, sehen wir, dass die Zitate in einem HTML span-Element liegen. Um die Zitate von der Seite zu extrahieren, müssen wir also zunächst die HTML-Elemente extrahieren, in denen sich die Zitate befinden:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Erstes span-Element ausgeben lassen\n",
    "soup.find(\"span\")"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Alle span-Elemente ausgeben lassen\n",
    "soup.find_all(\"span\")"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Wenn wir die Ausgabe der Methode `.find_all()` durchsuchen, fällt auf, dass unter den gefundenen span-Elementen auch span-Elemente sind, die keine Zitate beinhalten. Wie können wir nur auf die span-Elemente, die Zitate beinhalten, zugreifen? Die span-Elemente, die Zitate beinhalten, haben alle ein Attribut class mit dem Wert \"text\", während die span-Elemente, die keine Zitate beinhalten, ein Attribut class mit dem Wert \"tag-item\" oder \"sh-red\" haben. Es gibt also verschiedene Klassen von span-Elementen. span-Elemente, die Zitate enthalten, können wir deswegen mithilge des Attributs class extrahieren. Aber wie genau machen wir das? Dazu können wir wieder die Methode `.find_all()` verwenden. Die Methode `.find_all()` hat einen Parameter class_, der verwendet werden kann, um die Suche nach HTML-Elementen auf Elemente mit einem bestimmten Wert für das Attribut class einzuschränken:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Alle span-Elemente mit einem Attribut class=\"text\"\n",
    "# Beachtet den Unterstrich im Parameter class_. Der Unterstrich ist notwendig, weil das Wort class in Python ein Signalwort ist, das eine Klassendefinition einleitet. Der unterstrich hat also keine Bedeutung, er ist nur eine Formalität, die verhindert, dass Python den Parameter class_ fälschlich als Beginn einer Klassendefinition interpretiert.\n",
    "soup.find_all(\"span\", class_=\"text\")"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Jetzt findet `.find_all()` alle span-Elemente, die wir brauchen. Jetzt müssen wir nur noch den Text zwischen den Tags des span-Elements extrahieren. Dazu können wir laut den Dokumentationsseiten zum Paket BeautifulSoup die Methode `.get_text()` verwenden:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "zitate = soup.find_all(\"span\", class_=\"text\")\n",
    "# zitate.get_text()"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Die Zeile zitate.get_text() habe ich auskommentiert, weil sie eine Fehlermeldung produziert: `AttributeError: ResultSet object has no attribute 'get_text'. You're probably treating a list of elements like a single element.` Was bedeutet das? zitate ist scheinbar ein \"ResultSet\"-Objekt, und für Objekte von diesem Typ ist keine Methode `.get_text()` definiert. Zur Erinnerung: In Python sind auch Methoden eine Art von Attribut. In der Fehlermeldung ist also mit Attribut kein Eigenschafts-Attribut gemeint, sondern ein Methoden-Attribut, das wir zur besseren Unterscheidung einfach nur Methode nennen."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# zitate ist ein ResultSet Objekt\n",
    "type(zitate)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Um mehr Informationen über das ResultSet Objekt zu bekommen, können wir auf der Seite [https://tedboy.github.io/bs4_doc/generated/generated/bs4.ResultSet.html](https://tedboy.github.io/bs4_doc/generated/generated/bs4.ResultSet.html) nachlesen. Ein ResultSet-Objekt ist also im Grunde eine Python Liste. Die Methode `.get_text()` ist allerdings nur für einzelne Elemente in einem ResultSet definiert. Was müssen wir also machen, damit wir die Methode `.get_text()` anwenden können? Wir brauchen eine Schleife, die über das ResultSet-Objekt zitate iteriert und in jedem Schleifendurchlauf die Methode `.get_text()` für das aktuelle Element aufruft."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Inhalt der span-Elemente mithilfe der Methode .get_text() extrahieren und ausgeben lassen\n",
    "for zitat in zitate:\n",
    "    print(zitat.get_text())"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Voilà! So haben erfolgreich wir die Zitate von der Webseite [https://quotes.toscrape.com/](https://quotes.toscrape.com/) extrahiert.\n",
    "In der nächsten Woche lernt ihr, wie ihr die Zitate in einem nächsten Schritt in einer Datei auf eurem Computer speichern könnt.\n",
    "Wir werden außerdem die Schritte, die wir bisher auf eine einzelne Webseite angewendet haben, so verallgemeinern, dass wir sie auf alle Seiten der __Website__ anwenden können, ohne uns jede einzelne Seite zuerst in den Entwicklertools anzusehen. Denn genau das wollen wir mit dem Webscrapen ja erreichen: Wir wollen ein Skript schreiben, das automatisiert die selbe Art von Daten von vielen Webseiten extrahiert und auf unserem Computer speichert."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "### Quellen\n",
    "\n",
    "```{bibliography}\n",
    "   :list: enumerated\n",
    "   :filter: keywords % \"beautifulsoup\" or keywords % \"requests\"\n",
    "```"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "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
}