{ "cells": [ { "cell_type": "markdown", "source": [ "# Beispiel 2: Library of Congress API" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Die Library of Congress (LOC) bietet eine Reihe sehr gut dokumentierter APIs zur Abfrage von Metadaten, Dateien und Volltexten aus dem Bestand der Bibliothek. Eine davon ist die API der Sammlung US-amerikanischer historischer Zeitungen Chronicling America. Diese API werden wir in dieser Stunde kennenlernen.\n", "\n", "- Übersicht über die LOC APIs: https://guides.loc.gov/digital-scholarship/accessing-digital-materials#s-lib-ctab-26648178-2\n", "- Dokumentation zur Chronicling America API: https://chroniclingamerica.loc.gov/about/api/\n", "\n", "Zunächst machen wir uns mit der Chronicling America API vertraut. Welche Daten können darüber abgefragt werden?\n", "\n", "Für unser Beispiel werden wir die Volltexte zu allen Ergebnissen einer Suche nach Schlüsselwörtern in den Volltexten der Zeitungen abfragen und herunterladen (Abschnitt \"Searching the directory and newspaper pages using OpenSearch\"). Die Volltexte sind mithilfe von OCR-Verfahren erstellt, also mithilfe von automatischer Bilderkennung. Unsere Suchabfrage liefert also nur diejenigen Zeitungen, in denen die Suchwörter korrekt erkannt wurden." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Vorbereitung" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# wir müssen zunächst die Anaconda Einstellungen ändern, damit wir das Paket ratelimit installieren könenn:\n", "# https://stackoverflow.com/questions/48493505/packagesnotfounderror-the-following-packages-are-not-available-from-current-cha\n", "# import sys\n", "# !conda config --append channels conda-forge" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Paket ratelimit installieren\n", "# import sys\n", "# !conda install --yes --prefix {sys.prefix} ratelimit" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Pakete importieren\n", "import requests\n", "import os\n", "import math\n", "import time\n", "# from ratelimit import limits, RateLimitException, sleep_and_retry" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Exploration der Chronicling America API" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Wie in der letzten Stunde müssen wir zur Abfrage von Daten wieder eine URI nach den Vorgaben der API Dokumentation zusammensetzen.\n", "\n", "Suchabfragen können mit einem `?` an die URL https://chroniclingamerica.loc.gov/search/pages/results/ angefügt werden.\n", "Es gibt laut [Dokumentationsseite](https://chroniclingamerica.loc.gov/about/api/) drei verschiedene Abfrageparameter:\n", "\n", "- andtext: the search query\n", "- format: 'html' (default), or 'json', or 'atom' (optional)\n", "- page: for paging results (optional)\n", "\n", "Diese Parameter werden wir uns der Reihe nach ansehen." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Parameter andtext" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Der andtext Parameter: Volltexte nach Schlagwörtern oder Phrasen durchsuchen\n", "# Suche nach Schlagwörtern book AND review\n", "url_1 = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=book+review\"\n", "url_2 = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=book%20review\"" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Eine aufmerksame Betrachtung der Ergebnisse der Suche nach einem Suchbegriff über die Suchmaske der Website https://chroniclingamerica.loc.gov zeigt, dass die Suche über die Suchmaske genau dieselben Ergebnisse liefert wie die API-Abfrage. Das ist nicht erstaunlich, denn die URL, die beim Verwenden der Suchmaske generiert wird, ist ganz ähnlich aufgebaut wie die URL, die wir für die API-Abfrage erstellen, mit dem einzigen Unterschied, dass die Suchparameter etwas anders aussehen:\n", "\n", ":::{figure-md}\n", "<img src=\"loc_ca_suche.png\" alt=\"Suchmaske Chronicling America\" class=\"bg-transparent\" width=\"80%\">\n", "\n", "Einfache Suche über die Suchmaske der Seite Chronicling America.\n", ":::\n", "\n", "Wir können diese URL nutzen, um den andtext-Parameter besser zu verstehen. Wenn wir in der einfachen Suche nach dem Suchbegriff \"book review\" suchen, dann steht in der URL \"book+review\". Wenn wir stattdessen die Erweiterte Suche (Tab Advanced Search) verwenden und nach einer Phrase suchen, dann steht in der URL der Zusatz \"&phrasetext=book+review\":\n", "\n", ":::{figure-md}\n", "<img src=\"loc_ca_erweiterte_suche.png\" alt=\"Erweiterte Suche Chronicling America\" class=\"bg-transparent\" width=\"80%\">\n", "\n", "Erweiterte Suche über die Suchmaske der Seite Chronicling America.\n", ":::\n", "\n", "Tatsächlich akzeptiert auch die Chronicling America API eine Abfrage-URI mit dem Zusatz &phrasetext:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Suche nach Phrase \"book review\"\n", "url_3 = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review\"\n", "# woher weiß ich das: manuelle Suche über \"advanced search\" und URL untersuchen\n", "# https://chroniclingamerica.loc.gov/search/pages/results/?dateFilterType=yearRange&date1=1770&date2=1963&language=&ortext=&andtext=&phrasetext=book+review&proxtext=&proxdistance=5&rows=20&searchType=advanced\n", "search_results = requests.get(url_3)\n", "# search_results" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Parameter format" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Der format-Parameter: Ergebnisse im JSON-Format abfragen\n", "# https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\n", "# erste Seite der Suchergebnisse: 20 Ergebnisse je Seite\n", "url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "search_results = requests.get(url)\n", "# search_results" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "```{note}\n", "JSON im Chrome Browser ansehen\n", "\n", "Zur Ansicht der JSON-Datei im Chrome Browser können wir wieder auf die Entwicklertools zurückgreifen. Die Standardansicht ist nämlich sehr schwer lesbar, weil der JSON-String nicht formatiert ist. Um eine formatierte Ansicht zu erhalten, befolgt die folgenden Schritte: Entwicklertools öffnen -> \"Sources\"-Tab auswählen-> Link anklicken\n", "```" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Parameter page" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Der page Parameter: Nur die ausgewählte Ergebnisseite abfragen\n", "first_page = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json&page=1\"\n", "first_page_results = requests.get(first_page)\n", "# first_page_results" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Per Default werden immer die ersten 20 Suchergebnisse (also die erste Seite der Suchergebnisse) ausgegeben, wenn der page-Parameter weggelassen wird:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "default_page = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "default_page_results = requests.get(default_page)\n", "first_page_results.content == default_page_results.content" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Mit diesem Wissen könenn wir eine Testabfrage durchführen.\n", "\n", "Für unsere Abfragen wählen wir JSON als Rückgabeformat aus, weil wir den JSON-String bequem parsen können, indem wir den String in ein Python Dictionary umwandeln:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "# JSON-String in Python Dictionary umwandeln\n", "search_results = requests.get(url).json()\n", "print(len(search_results[\"items\"]))" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Das Dictionary enthält einen Schlüssel \"items\" mit einer Liste der Suchergebnisse als Wert. Die Suchergebnisse sind selbst als Dictionaries organisiert. Jedes Suchergebnis-Dictionary enthält einen Schlüssel \"ocr_eng\" mit den Volltexten:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# erstes Suchergebnis auf der ersten Seite der Suchergebnisse\n", "search_results[\"items\"][0][\"ocr_eng\"]" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Um die Volltexte für alle Suchergebnisse auf der ersten Seite abzurufen und zu speichern, können wir eine for-Schleife entwerfen:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Volltexte für die gesamte erste Seite der Suchergebnisse speichern\n", "# items = search_results[\"items\"]\n", "\n", "# for item in items:\n", "# ocr_text = item[\"ocr_eng\"]\n", "# title = item[\"title\"]\n", "# date = item[\"date\"]\n", "# with open(f\"{title}_{date}.txt\", \"w\", encoding=\"utf-8\") as file:\n", "# file.write(ocr_text)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Das wollen wir jetzt für alle Suchergebnisse auf allen Seiten reproduzieren." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Abfrage aller Volltexte mit \"book review\"\n", "\n", "Zunächst legen wir in unserem aktuellen Arbeitsverzeichnis (=Ordner, in dem die Jupyter Notebooks liegen) ein neues Verzeichnis an, in dem wir die Volltexte abspeichern werden:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# neues Verzeichnis anlegen: in diesem Ordner werden die Textdateien gespeichert\n", "# if not os.path.exists(\"loc_ocr\"):\n", "# os.makedirs(\"loc_ocr\")\n", "# Arbeitsverzeichnis wechseln: Die Texte sollen im Verzeichnis \"spokentext_corpus\" gespeichert werden\n", "# os.chdir(os.path.join(os.getcwd(), \"loc_ocr\"))" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Wie gehen wir vor, um jetzt unsere for-Schleife oben nacheinander auf alle Ergebnisseiten anzuwenden?\n", "Eine Idee wäre die Verwendung einer while Schleife mit HTTP Antwort != 200 als break-Bedingung. Diese Strategie ist aber nur anwendbar, wenn beim Abruf einer ungültigen Seite eine HTTP-Antwort ungleich 200 zurückgegeben wird. Das müssen wir zunächst überprüfen: Was passiert, wenn eine nicht existierende Seite aufgerufen wird?\n", "Als Beispiel rufen wir die Seite https://chroniclingamerica.loc.gov/search/pages/results/?rows=20&format=json&sequence=0&phrasetext=book+review&andtext=&page=1640 auf.\n", "\n", "Tatsächlich gibt es eine Umleitung auf Seite 1 mit einem gültigen HTTP-Statuscode. Wir können also in diesem Fall die Strategie mit der while-Schleife nicht verwenden.\n", "\n", "Eine andere Idee ist die Verwendung einer for-Schleife. Dazu müssen wir aber die Gesamtzahl der Ergebnisseiten kennen. Die Gesamtzahl der Ergebnisseiten können wir aber einfach ermitteln:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Gesamtanzahl der Ergebnisseiten ermitteln: Anzahl der Ergebnisse durch Anzahl der Ergebnisse pro Seite teilen\n", "url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "search_results = requests.get(url).json()\n", "pages_float = search_results[\"totalItems\"] / search_results[\"itemsPerPage\"]\n", "pages = math.ceil(pages_float)+1 # aufrunden und 1 addieren\n", "pages" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Zu der Gesamtzahl der Seiten addieren wir 1, da wir später die range(1, n)-Funktion verwenden wollen, welche eine Integersequenz von Zahl 1 bis Zahl n-1 generiert.\n", "\n", "Unsere for-Schleife sieht dann so aus:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# Volltexte zu allen Ergebnissen von allen Ergebnisseiten speichern\n", "# url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json&page=\"\n", "\n", "# for page in range(1, pages):\n", "# request_url = url + str(page)\n", "# response = requests.get(request_url).json()\n", "# # for-Schleife für eine einzelne Ergebnisseite einsetzen\n", "# items = response[\"items\"]\n", "# for item in items:\n", "# ocr_text = item[\"ocr_eng\"]\n", "# title = item[\"title\"]\n", "# date = item[\"date\"]\n", "# with open(f\"{title}_{date}.txt\", \"w\", encoding=\"utf-8\") as file:\n", "# file.write(ocr_text)" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "Aber Achtung! Beim Ausführen des Codes oben gibt es nach einigen Schleifendurchläufen eine Fehlermeldung: JSONDecodeError: Expecting value: line 1 column 1 (char 0). Die Fehlermeldung entsteht dann, wenn die HTTP-Anfrage keine erfolgreiche Antwort liefert. Das liegt mit großer Wahrscheinlichkeit daran, dass wir uns nicht an die Einschränkungen der LOC gehalten haben und die HTTP-Anfrage dadurch ab einem bestimmten Punkt abgelehnt wird. Wenn wir dann versuchen, den Antwortbody mithilfe der .json()-Methode in ein Python Dictionary umzuwandeln, teilt der Python interpreter uns mit, dass das nicht möglich ist, weil wir die Methode nicht auf einen gültigen JSON-String angewendet haben.\n", "\n", "Bei der Abfrage von sehr vielen Seiten müssen wir uns also nach den Einschränkungen der LOC richten. Die LOC hat Einschränkungen für die der Chronicling America API übergeordnete Seite loc.gov festgelegt, und wir können vermuten, dass die Einschränkungen auch für die Chronicling America API gelten: https://www.loc.gov/apis/json-and-yaml/working-within-limits\n", "\n", "Um auf der sicheren Seite zu sein, richten wir uns nach der restriktivsten Vorgabe, nach der nur 20 Abfragen alle 10 Sekunden erlaubt sind.\n", "\n", "Wie können wir also die HTTP-Abfragen auf 20 Abfragen je 10 Sekunden einschränken? Was wir brauchen nennt sich \"rate limiting\", also eine Begrenzung der Abfragerate, die wir in unseren Code einbauen müssen.\n", "\n", "Um die Abfragerate einzuschränken, gibt es zwei etablierte Möglichkeiten:\n", "\n", "1) **Funktion `time.sleep()` aus dem Paket time**. Die Funktion time.sleep(x) kann in den Schleifenkörper einer for-Schleife eingefügt werden, um den nächsten Schleifendurchlauf um x Sekunden zu verzögern. Diese Methode ist einstiegsfreundlich, aber ungenau, weil die Laufzeit der Schleife selbst nicht in die Wartezeit mit einbezogen wird, sodass der nächste Schleifendurchlauf länger als notwendig verzögert wird.\n", "2) **Python Dekoratoren aus dem Paket ratelimit**. Wesentlich effizienter und eleganter ist die Verwendung von sogenannten Python Dekoratoren bzw. Decorators. Das Paket ratelimit bietet zwei solche Dekoratoren, die dazu verwendet werden können, um zu registrieren, wie häufig eine Funktion nacheinander aufgerufen wird, und die ab einer bestimmten Anzahl wiederholter Aufrufe eine Wartepause erzwingen. Um Decorators verwenden zu können, müssen wir unsere Abfrage jedoch in eine Funktion verpacken." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "```{note}\n", "Python Dekoratoren (Decorators)\n", "\n", "> A decorator in Python is a function that accepts another function as an argument. The decorator will usually modify or enhance the function it accepted and return the modified function. This means that when you call a decorated function, you will get a function that may be a little different that may have additional features compared with the base definition.\n", "\n", "Quelle: [Michael Droscill (2017).](https://python101.pythonlibrary.org/chapter25_decorators.html)\n", "\n", "Dekoratoren beruhen auf einem komplexen Konzept und wir können hier nicht tiefer einsteigen, aber wenn die ein oder andere Person doch etwas tiefer einsteigen will, kann ich diese beiden Ressourcen empfehlen:\n", "- Primer on Python Decorators, https://realpython.com/primer-on-python-decorators/\n", "- Python Decorators in 15 Minutes, https://www.youtube.com/watch?v=r7Dtus7N4pI\n", "\n", "Bei der Verwendung der Dekoratoren aus dem Paket ratelimit verwenden wir diese Anleitung von Akshay Ranganath:\n", "- Rate Limiting with Python, https://akshayranganath.github.io/Rate-Limiting-With-Python/\n", "```" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# 1) Rate limiting mit time.sleep()\n", "\n", "# url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "# search_results = requests.get(url).json()\n", "# pages_float = search_results[\"totalItems\"] / search_results[\"itemsPerPage\"]\n", "# pages = math.ceil(pages_float) # aufrunden\n", "\n", "# url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json&page=\"\n", "# for page in range(1, pages):\n", "# request_url = url + str(page)\n", "# response = requests.get(request_url).json()\n", "# # for-Schleife für eine einzelne Ergebnisseite einsetzen\n", "# items = response[\"items\"]\n", "# for item in items:\n", "# ocr_text = item[\"ocr_eng\"]\n", "# title = item[\"title\"]\n", "# date = item[\"date\"]\n", "# with open(f\"{title}_{date}.txt\", \"w\", encoding=\"utf-8\") as file:\n", "# file.write(ocr_text)\n", "# time.sleep(10)" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "# 2) Rate limiting mit Python decorators\n", "\n", "# url = \"https://chroniclingamerica.loc.gov/search/pages/results/?andtext=&phrasetext=book+review&format=json\"\n", "# search_results = requests.get(url).json()\n", "# pages_float = search_results[\"totalItems\"] / search_results[\"itemsPerPage\"]\n", "# pages = math.ceil(pages_float) # aufrunden\n", "# url = url + \"&page=\"\n", "\n", "# TEN_SECONDS = 10\n", "# CALLS_PER_TEN_SECONDS = 20 # 20 Abfragen in 10 Sekunden\n", "\n", "# @sleep_and_retry\n", "# @limits(calls=CALLS_PER_TEN_SECONDS, period=TEN_SECONDS)\n", "# def get_fulltext(url):\n", "# response = requests.get(url).json()\n", "# # for-Schleife für eine einzelne Ergebnisseite einsetzen\n", "# items = response[\"items\"]\n", "# for item in items:\n", "# ocr_text = item[\"ocr_eng\"]\n", "# title = item[\"title\"]\n", "# date = item[\"date\"]\n", "# with open(f\"{title}_{date}.txt\", \"w\", encoding=\"utf-8\") as file:\n", "# file.write(ocr_text)\n", "\n", "# for page in range(1, pages):\n", "# request_url = url + str(page)\n", "# get_fulltext(request_url)\n" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "```{note}\n", "Konstanten (Constants)\n", "\n", "Im Code oben verwenden wir Großbuchstaben, um die beiden Variablen `CALLS_PER_TEN_SECONDS` und `TEN_SECONDS` zu benennen. Diese Schreibweise hat sich in Python für Konstanten etabliert, also für Variablen, deren Wert sich im Programmverlauf nicht ändert.\n", "```" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "### Quellen\n", "\n", "```{bibliography}\n", " :list: enumerated\n", " :filter: keywords % \"decorators\" or keywords % \"loc\"\n", "```" ], "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 }