%pip install requests pandas folium branca anytree
import requests
import pandas as pd
import folium
import branca
import anytree
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
Alle Elemente mit dem Tag "gastronomy" vom API abrufen:
headers = {'Accept': 'application/json'}
r = requests.get('https://www.zuerich.com/en/api/v2/data?id=101', headers=headers, verify=SSL_VERIFY)
Die JSON Daten vom API in ein Python dictionary umwandeln:
data = r.json()
data
Die Daten haben viele mehrsprachige Felder, der folgende Code holt sich jeweils die deutschen Inhalte:
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:
df = pd.DataFrame(de_data)
df
folium
ist ein Python Wrapper für OpenLayers. Der nachfolgende Code erstellt eine neue Karte und verwendet den Übersichtsplan als Hintergrund (eingebunden als WMS).
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.
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:
folium.LayerControl().add_to(m)
m
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).
# 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')
generate_csv_download('https://www.zuerich.com/en/api/v2/data?id=72', 'attractions')
headers = {'Accept': 'application/json'}
r = requests.get('https://www.zuerich.com/en/api/v2/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:
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
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])))
Dieses Beispiel zeigt, wie man sich durch API "hangeln" kann, d.h. den Links zu folgen und zu sehen, welche Elemente es gibt.
from anytree import Node, RenderTree
from urllib.parse import urljoin
headers = {'Accept': 'application/json'}
base_url = 'https://www.zuerich.com'
data_url = urljoin(base_url, '/en/api/v2/data')
r = requests.get(data_url, headers=headers, verify=SSL_VERIFY)
data = r.json()
data
Mit dem Aufruf von https://www.zuerich.com/en/api/v2/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:
# 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))
# 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
# Liste aller Kategorien mit keinen Elementen (count == 0)
categories[categories['count'] == 0].sort_values(by=['id'])
# 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
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