Mit Binder oder Colab kann das Jupyter-Notebook interaktiv im Browser gestartet werden:

Binder

Open In Colab

Python-Beispiele für das Zürich Tourismus API

Inhaltsverzeichnis

  1. Restaurants vom Zürich Tourismus API auf einer Karte darstellen
  2. CSV Download
  3. Bilder zu einem Thema
  4. Kategorien der Elemente im API
In [ ]:
%pip install requests pandas folium branca anytree
In [ ]:
import requests
import pandas as pd
import folium
import branca
import anytree
In [ ]:
SSL_VERIFY = False
# evtl. SSL_VERIFY auf False setzen wenn die Verbindung zu https://www.zuerich.com nicht klappt (z.B. wegen Proxy)
# Um die SSL Verifikation auszustellen, bitte die nächste Zeile einkommentieren ("#" entfernen)
# SSL_VERIFY = False
if not SSL_VERIFY:
    import urllib3
    urllib3.disable_warnings()
    
def get_de(field):
    try:
        return field['de']
    except (KeyError, TypeError):
        try:
            return field['en']
        except (KeyError, TypeError):
            return field

Restaurants vom Zürich Tourismus API auf einer Karte darstellen

Daten von der API laden

Alle Elemente mit dem Tag "gastronomy" vom API abrufen:

In [ ]:
headers = {'Accept': 'application/json'}
r = requests.get('https://www.zuerich.com/de/data?id=101', headers=headers, verify=SSL_VERIFY)

Die JSON Daten vom API in ein Python dictionary umwandeln:

In [ ]:
data = r.json()
data

Die Daten haben viele mehrsprachige Felder, der folgende Code holt sich jeweils die deutschen Inhalte:

In [ ]:
de_data = [{k: get_de(v) for (k,v) in f.items()} for f in data]
de_data

Die Daten in einem pandas DataFrame ablegen:

In [ ]:
df = pd.DataFrame(de_data)
df

Daten zur Karte hinzufügen

folium ist ein Python Wrapper für OpenLayers. Der nachfolgende Code erstellt eine neue Karte und verwendet den Übersichtsplan als Hintergrund (eingebunden als WMS).

In [ ]:
m = folium.Map(location=[47.36, 8.53], zoom_start=13, tiles=None)
folium.raster_layers.WmsTileLayer(
    url='https://www.ogd.stadt-zuerich.ch/wms/geoportal/Basiskarte_Zuerich_Raster',
    layers='Basiskarte Zürich Raster',
    name='Zürich - Basiskarte',
    fmt='image/png',
    overlay=False,
    control=False,
    autoZindex=False,
).add_to(m)

Nun iterieren wir über das pandas DataFrame und erstellen einen Marker für jedes Restaurant. Falls vorhanden wird ein Photo in den Beschreibungstext eingefügt.

In [ ]:
gastro = folium.FeatureGroup("Restaurants")
isna = df.isna()
for i, row in df.iterrows():
    print(row['geoCoordinates'])
    print(row['name'])
    geo = row['geoCoordinates']
    if not isna.geoCoordinates[i]:
        print("%s, %s, %s" % (float(geo['latitude']), float(geo['longitude']), row['name']))
        
        try:
            photo = row['photo'][0]['url']
            photo_html = f'<img src="{photo}" style="width:300px">'
        except (IndexError, KeyError, TypeError):
            photo_html = ''
        html = (
            f'<h2>{row["name"]}</h2>'
            f'{photo_html}'
            f'<p>{row["disambiguatingDescription"]}</p>'
        )
        #popup = folium.Popup(branca.element.IFrame(html=html, width=420))
        gastro.add_child(folium.Marker(location=[float(geo['latitude']), float(geo['longitude'])], popup=html)) 
m.add_child(gastro)

Hier ist die fertige Karte:

In [ ]:
folium.LayerControl().add_to(m)
m

CSV Download

Für die weitere Verarbeitung kann es nützlich sein, die Daten aus dem API in tabellarischer Form zu haben. Der nachfolgende Code wandelt das JSON vom API in ein CSV um (ohne jedoch alle Attribute zu flatten).

In [ ]:
# download CSV
import base64
from IPython.display import HTML

def create_download_link( df, title = "Download CSV file", filename = "data.csv"):  
    csv = df.to_csv(None, index=False)
    b64 = base64.b64encode(csv.encode())
    payload = b64.decode()
    html = '<h3><a download="{filename}" href="data:text/csv;base64,{payload}" target="_blank">{title}</a><h3>'
    html = html.format(payload=payload,title=title,filename=filename)
    return HTML(html)

def generate_csv_download(endpoint, name):
    headers = {'Accept': 'application/json'}
    r = requests.get(endpoint, headers=headers, verify=SSL_VERIFY)
    data = r.json()
    df = pd.DataFrame(data)
    return create_download_link(df, f'Download {name}_data.csv', f'data_{name}.csv')
In [ ]:
generate_csv_download('https://www.zuerich.com/de/data?id=72', 'attractions')

Bilder zu einem Thema

Das Zürich Tourismus API bietet sehr hochwertige Bilder an, die sich zur Illustration eigenen.

Lade alle Einträge zum Thema "Sehenswürdigkeiten" (engl. attractions):

In [ ]:
headers = {'Accept': 'application/json'}
r = requests.get('https://www.zuerich.com/de/data?id=72', headers=headers, verify=SSL_VERIFY)
data = r.json()

Alle Bilder zum Thema "einsammeln", diese sind in den Attributen image und photo hinterlegt:

In [ ]:
images = []
images.extend([d['image']['url'] for d in data])
images.extend([p['url'] or '' for y in [d['photo'] or '' for d in data] for p in y])
images

Anzeige von zufälligen Bildern zum Thema:

In [ ]:
from IPython.display import HTML, display
import random

# wähle zufällig 8 Einträge aus der Liste aus
sample = random.sample(images, k=8)

def img_html(url):
     return '<img src="{}" style="display:inline;margin:1px;width:200px"/>'.format(url)

display(HTML(''.join([img_html(url) for url in sample])))

Kategorien der Elemente im API

Dieses Beispiel zeigt, wie man sich durch API "hangeln" kann, d.h. den Links zu folgen und zu sehen, welche Elemente es gibt.

In [ ]:
from anytree import Node, RenderTree
from urllib.parse import urljoin
In [ ]:
headers = {'Accept': 'application/json'}
base_url = 'https://www.zuerich.com'
data_url = urljoin(base_url, '/data')
r = requests.get(data_url, headers=headers, verify=SSL_VERIFY)
data = r.json()
data

Baum der Kategorien

Mit dem Aufruf von https://www.zuerich.com/de/data bekommt man das oberste Level des Baums (d.h. eine Liste aller Kategorien inkl. ihrer Hierarchie). Damit können wir uns einen Python-Baum basteln:

In [ ]:
# Finde die direkten Kind-Nodes des angegebenem Eltern-Nodes (rekursiv)
def find_children(data, parent):
    children = [e for e in data if e['parent'] == parent.id]
    for c in children:
        node = Node(id=c['id'], name=c['name'].get('de', c['name']), urlpath=c['path'], parent=parent)
        find_children(data, node)

root = Node(id='0', name="Root", urlpath="/data")
find_children(data, root)

# Zeige den Baum an
for pre, _, node in RenderTree(root):
    print("%s%s" % (pre, node.name))

Anzahl Elemente pro Kategorie

In [ ]:
# Jetzt holen wir via API die Anzahl Elemente für jede Kategorie
# ACHTUNG: es wird für jede Kategorie ein Request gemacht, das dauert einige Minuten!
de_data = [{k: get_de(v) for (k,v) in f.items()} for f in data]
categories = pd.DataFrame(de_data)

def get_category_count(path):
    headers = {'Accept': 'application/json'}
    base_url = 'https://www.zuerich.com'
    data_url = urljoin(base_url, path)
    print(f"Request data from {data_url}")
    r = requests.get(data_url, headers=headers, verify=SSL_VERIFY)
    return len(r.json())

categories['count'] = categories['path'].apply(get_category_count)
categories
In [ ]:
# Liste aller Kategorien mit keinen Elementen (count == 0)
categories[categories['count'] == 0].sort_values(by=['id'])
In [ ]:
# Aggregation auf "Eltern-Elemente" und der zugehörigen Anzahl
categories_with_parents = categories.merge(categories, how='right', left_on='id', right_on='parent', suffixes=('_parent', ''))
categories_with_parents.name_parent.fillna(categories_with_parents.name, inplace=True)
categories_with_parents = categories_with_parents[['name_parent', 'count']]
aggregated_categories = categories_with_parents.groupby('name_parent').sum().sort_values(by=['count'], ascending=False)
aggregated_categories
In [ ]:
ax = aggregated_categories.plot.bar(figsize=(20,15))
for p in ax.patches:
    ax.annotate(str(p.get_height()), (p.get_x() + 0.01, p.get_height() + 5))
ax
In [ ]: