…von einer Web-API

Was ist eine Web-API?

Ein Application Programming Interface (API) ist eine Schnittstelle zum programmgesteuerten Arbeiten mit einer bestimmten Anwendung. Eine API muss nicht zwingend das Web einbeziehen. Viele lokale Anwendungen auf eurem Computer verfügen über eigene APIs, sodass wir über Python oder eine andere Sprache mit ihm interagieren können. In unserem nächsten Beispiel erhalten wir die Daten jedoch über eine Web-API, d.h. über HTTP-Requests und -Responses. Viele aktuelle API sind genauer spezifiziert als sog. REST-APIs, d.h., sie kommunizieren über das HTTP/HTTPS-Protokoll mit den Consumers: HTTP-Requests verwenden meist die GET- oder POST-Methode sowie in der URL definierte Parameter, um die Anfrage zu stellen. Die HTTP-Responses haben eine ähnliche Struktur, enthalten jedoch zusätzlich noch einen HTTP-Statuscode.

Beispiel OSM Nomination API

In unserem nächsten Beispiel holen wir unser Daten von der OpenStreetMap Nomination API. Diese ist erreichbar über die URL https://nominatim.openstreetmap.org/search?. Um z.B. Informationen über das Berlin Congress Center in Berlin im JSON-Format zu erhalten, sollte die URL https://nominatim.openstreetmap.org/search.php?q=Alexanderplatz+Berlin&format=json angegeben werden, und wenn ihr euch den entsprechenden Kartenausschnitt anzeigen lassen wollt, so müsst ihr einfach nur &format=json weglassen.

Installation

Für die Kommunikation mit solchen REST-APIs ist die requests-Bibliothek hilfreich. Mit Spack könnt ihr requests in eurem Kernel bereitstellen:

$ spack env activate python-374
$ spack install py-requests ^python@3.7.4%gcc@9.1.0

Alternativ könnt ihr requests auch mit anderen Paketmanagern installieren, z.B.

$ pipenv install requests

Anschließend definieren wir die Basis-URL und die Parameter. Nominatim erwartet mindestens die folgenden beiden Parameter

Schlüssel

Werte

q

Adressabfrage, die folgende Spezifikationen erlaubt: street, city, county, state, country und postalcode.

format

Format, in dem die Daten zurückgegeben werden. Möglich Werte sind html, xml, json, jsonv2, geojson und geocodejson.

Die Abfrage kann dann gestellt werden mit:

[1]:
import requests

base_url = 'https://nominatim.openstreetmap.org/search?'
params = {
    'q': 'Alexanderplatz, Berlin',
    'format':'json',
}
r = requests.get(base_url,params=params)
[2]:
r.status_code
[2]:
200
[3]:
r.json()
[3]:
[{'place_id': 234847916,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'relation',
  'osm_id': 131761,
  'boundingbox': ['52.5200695', '52.5232601', '13.4103097', '13.4160798'],
  'lat': '52.521670650000004',
  'lon': '13.413278026558228',
  'display_name': 'Alexanderplatz, Mitte, Berlin, 10178, Deutschland',
  'class': 'highway',
  'type': 'pedestrian',
  'importance': 0.6914982526373583},
 {'place_id': 53256307,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'node',
  'osm_id': 4389211800,
  'boundingbox': ['52.5231653', '52.5232653', '13.414475', '13.414575'],
  'lat': '52.5232153',
  'lon': '13.414525',
  'display_name': 'Alexanderplatz, Alexanderstraße, Mitte, Berlin, 10178, Deutschland',
  'class': 'highway',
  'type': 'bus_stop',
  'importance': 0.22100000000000003,
  'icon': 'https://nominatim.openstreetmap.org/images/mapicons/transport_bus_stop2.p.20.png'},
 {'place_id': 90037579,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'way',
  'osm_id': 23853138,
  'boundingbox': ['52.5214702', '52.5217276', '13.4037885', '13.4045026'],
  'lat': '52.5215991',
  'lon': '13.404112295159964',
  'display_name': 'Alexander Plaza, 1, Rosenstraße, Mitte, Berlin, 10178, Deutschland',
  'class': 'tourism',
  'type': 'hotel',
  'importance': 0.11100000000000002,
  'icon': 'https://nominatim.openstreetmap.org/images/mapicons/accommodation_hotel2.p.20.png'}]

Es werden drei verschiedene Orte gefunden, der Platz, eine Bushaltestelle und ein Hotel. Um nun weiter filtern zu können, können wir uns nur den bedeutendsten Ort anzeigen lassen:

[4]:
params = {
    'q': 'Alexanderplatz, Berlin',
    'format':'json',
    'limit':'1'
}
r = requests.get(base_url,params=params)
r.json()
[4]:
[{'place_id': 234847916,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'relation',
  'osm_id': 131761,
  'boundingbox': ['52.5200695', '52.5232601', '13.4103097', '13.4160798'],
  'lat': '52.521670650000004',
  'lon': '13.413278026558228',
  'display_name': 'Alexanderplatz, Mitte, Berlin, 10178, Deutschland',
  'class': 'highway',
  'type': 'pedestrian',
  'importance': 0.6914982526373583}]

Clean Code

Nachdem wir nun wissen, dass der Code funktioniert, wollen wir alles in eine saubere und flexible Funktion umwandeln.

Um sicherzustellen, dass die Interaktion erfolgreich war, verwenden wir die Methode raise_for_status von requests, die eine Exception auslöst, wenn der HTTP-Statuscode nicht 200 OK ist:

[5]:
r.raise_for_status()

Da wir die Lastgrenzen der Nomination-API nicht überschreiten möchten, werden wir unsere Anforderungen mit der Funktion time.sleep verzögern:

[6]:
from time import sleep

sleep(1)
r.json()
[6]:
[{'place_id': 234847916,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'relation',
  'osm_id': 131761,
  'boundingbox': ['52.5200695', '52.5232601', '13.4103097', '13.4160798'],
  'lat': '52.521670650000004',
  'lon': '13.413278026558228',
  'display_name': 'Alexanderplatz, Mitte, Berlin, 10178, Deutschland',
  'class': 'highway',
  'type': 'pedestrian',
  'importance': 0.6914982526373583}]

Als nächstes deklarieren wir die Funktion selbst. Als Argumente benötigen wir die Adresse, das Format, das Limit der zurückzugebenden Objekte mit dem Standardwert 1 und weitere kwargs (keyword arguments), die als Parameter übergeben werden:

[7]:
def nominatim_search(address, format="json", limit=1, **kwargs):
    """Thin wrapper around the Nominatim search API.
    For the list of parameters see
    https://nominatim.org/release-docs/develop/api/Search/#parameters
    """
    search_url = "https://nominatim.openstreetmap.org/search?"
    params = {"q": address, "format": format, "limit": limit, **kwargs}
    r = requests.get(search_url, params=params)
    # Raise an exception if the status is unsuccessful
    r.raise_for_status()

    sleep(1)
    return r.json()

Nun können wir die Funktion ausprobieren, z.B. mit

[8]:
nominatim_search('Alexanderplatz, Berlin')
[8]:
[{'place_id': 234847916,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'relation',
  'osm_id': 131761,
  'boundingbox': ['52.5200695', '52.5232601', '13.4103097', '13.4160798'],
  'lat': '52.521670650000004',
  'lon': '13.413278026558228',
  'display_name': 'Alexanderplatz, Mitte, Berlin, 10178, Deutschland',
  'class': 'highway',
  'type': 'pedestrian',
  'importance': 0.6914982526373583}]

Ihr könnt jedoch neben address noch weitere Parameter verwenden. Eine Übersicht erhaltet ihr in den Nominatim Docs.

[9]:
nominatim_search(address=None, street='8, Marienburger Straße', city='Berlin',
    country='Germany')
[9]:
[{'place_id': 22277694,
  'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
  'osm_type': 'node',
  'osm_id': 2270572699,
  'boundingbox': ['52.5346778', '52.5347778', '13.4241714', '13.4242714'],
  'lat': '52.5347278',
  'lon': '13.4242214',
  'display_name': '8, Marienburger Straße, Kollwitzkiez, Prenzlauer Berg, Pankow, Berlin, 10405, Deutschland',
  'class': 'place',
  'type': 'house',
  'importance': 0.42099999999999993}]

Caching

Falls innerhalb einer Session immer wieder dieselben Abfragen gestellt werden sollen,ist es sinnvoll, diese Daten nur einmal abzurufen und wiederzuverwenden. In Python können wir lru_cache aus der functools-Standardbibliothek von Python verwenden. lru_cache speichert die N letzten Anfragen (Least Recent Used) und sobald das Limit überschritten wird, werden die ältesten Werte verworfen. Um dies für die Methode nominatim_search zu verwenden, müsst ihr lediglich einen Import und einen Decorator defnieren:

[10]:
from functools import lru_cache

@lru_cache(maxsize=1000)
def nominatim_search(address, format='json', limit=1, **kwargs):
     '''…
     '''

lru_cache speichert die Ergebnisse jedoch nur während einer Session. Wenn ein Skript wegen einem Timeout oder einer Exception beendet wird, sind die Ergebnisse verloren. Sollen die Daten dauerhafter gespeichert werden, können Tools wie joblib oder python-diskcache verwendet werden.