Der Onlineshop der Supermarktkette Rewe bietet einen Export der eigenen Einkaufsdaten – das ist praktisch für neugierige Datenanalysten: Mit Python und Werkzeugen wie Jupyter Notebooks und der Bibliothek Pandas kann man diese Daten untersuchen. Im vorigen Artikel haben wir gezeigt, wie man dabei einiges über die eigenen Einkaufsvorlieben im Speziellen und über Datenanalyse im Allgemeinen lernt.

Beidem sind allerdings Grenzen gesetzt, denn – so nerdfreundlich ein Datenexport im JSON-Format auch ist – letztlich steht in dem Datensatz wenig mehr als auf den Kassenzetteln. Insbesondere fehlen Produktgruppen oder -kategorien und Artikelnummern, die Artikel eindeutig und händlerübergreifend identifizieren. Erstere würden beispielsweise erlauben, Gemüseeinkäufe mit den Ausgaben für Snacks zu vergleichen. Über letztere könnte man Produkte auch in externen Datenbanken finden oder Preise mit denen der Mitbewerber vergleichen.

Grundsätzlich liegen diese Daten vor; wie praktisch jeder andere Onlineshop sortiert auch Rewes Website die eigenen Produkte in Kategorien. Einen eindeutigen Barcode mit der Global Trade Item Number (GTIN) findet man ohnehin auf fast jedem Produkt. Die GTIN wird umgangssprachlich oft noch als „EAN“ (für European Article Number) bezeichnet.

Eine Anfrage beim Kundendienst des Händlers brachte leider keinen Erfolg: Man könne den Datenexport nicht um Artikelnummern, EANs oder Kategorien erweitern, weil diese dynamisch seien und sich häufig änderten. So ganz leuchtet diese Sicht nicht ein: Bei neuer Zusammensetzung, Verpackung oder Herkunft von Produkten ändern sich Produktnummern durchaus. Trotzdem muss zum Zeitpunkt der Lieferung klar sein, welches Produkt mit welchen Kategoriezuordnungen und welcher EAN den Besitzer gewechselt hat.

Datenquellensuche

Dass dokumentierte Schnittstellen nicht alle verfügbaren Informationen liefern, ist eher die Regel als die Ausnahme. Man kann dann versuchen, auf bessere dokumentierte Schnittstellen hinzuwirken – oder man sucht nach undokumentierten Datenquellen. Denn die Rewe-Webseite und -App kennen ja weitergehende Produktdaten wie Kategorien oder Nährwerte. Und was eine App auf dem eigenen Gerät weiß, das weiß man auch bald selbst.

Nicht lokal berechnete Daten wie Suchergebnisse, Einkaufskörbe oder Kategorienlisten müssen Smartphone-Apps per API von einem Webserver erhalten. Auch bei modernen Webseiten interagiert häufig das JavaScript im Browser mit APIs, was ein Neuladen der kompletten Webseite vermeidet. Typischerweise stellen Webserver also nicht nur HTML-Seiten bereit, sondern auch APIs, die von den hauseigenen Apps und Webseiten genutzt werden (und häufig JSON ausliefern).

Solche APIs sind nicht immer gut dokumentiert, aber dennoch recht leicht auffindbar: Der Code, der sie anspricht, läuft schließlich auf den Geräten des Endkunden. Die Webentwickler-Tools moderner Browser erlauben nicht nur den Quellcode einer Website zu studieren, sondern auch live dabei zuzusehen, wenn ein Browser auf ein API zugreift – noch bevor die TLS-Verschlüsselung der Verbindung zum Server greift.

Die Entwickler-Tools öffnen Sie in den meisten Browsern mit einem Druck auf F12. Für die API-Analyse ist insbesondere der „Netzwerk“-Tab interessant. Dort protokolliert der Browser alle Anfragen, die er stellt, an wen er sie stellt und welche Antworten er bekommt. Siehe da: Wenn man sich bei Rewe im Webshop einloggt und sich über die Bestellübersicht zu einer konkreten Bestellung durchklickt, protokolliert der Browser dutzende Requests. Versteckt in dem Wust findet sich ein API-Aufruf, der JSON zurückliefert:

https://shop.rewe.de/api/orders/<BESTELLNUMMER>?constraint=rewe
Jackpot: Inmitten zahlreicher Aufrufe verbirgt sich einer für ein JSON-API, das detaillierte Produktinformationen liefert.
Jackpot: Inmitten zahlreicher Aufrufe verbirgt sich einer für ein JSON-API, das detaillierte Produktinformationen liefert.

Gib mir alles

Dieser Aufruf liefert zwar nur Daten zu einer Bestellung (die man über die Bestellnummer spezifiziert), bietet aber dafür wesentlich mehr Details, als im offiziellen Datenexport enthalten sind. Dazu gehören interne Artikelnummern, EANs (aha!), Links auf Produktbilder, Kategorieinformationen, Steuersätze und maximale Bestellmengen.

Ein schöner Datenschatz, aber wie kommt man an diese Details für alle eigenen Bestellungen? Das JSON-API von Rewe scheint hierarchisch aufgebaut zu sein, was typisch für APIs ist, die dem verbreiteten REST-Schema folgen. Vielleicht kann man die Bestellnummer einfach weglassen und bekommt dann mehr als eine Bestellung? Das klappt tatsächlich, eine Anfrage an https://shop.rewe.de/api/orders/ beantwortet der Server mit einer Liste von Bestellungen. Leider enthalten die einzelnen Punkte dieser Liste weniger Details, insbesondere fehlt die Liste der Artikel pro Bestellung.

Also muss man einen mühsameren Weg gehen und zuerst aus dem offiziellen Export sämtliche Bestellnummern extrahieren. Das erfordert nur eine kleine Ergänzung des Pandas-Codes:

oids = list(set(df['orderId']))

Eine aktualisierte Version des kompletten Jupyter-Notebooks finden Sie hier. Ein weiterer kleiner Codeschnipsel iteriert anschließend über die Bestellnummern und stellt mit jeder Nummer eine Anfrage an https://shop.rewe.de/api/orders/<BESTELLNUMMER>. Das klappt aber nicht einfach so, schließlich ist der Python-Code nicht im Rewe-Shop angemeldet.

Das Login-Verfahren in Python nachzubilden wäre sehr aufwendig, denn Rewe schützt es unter anderem mit CAPTCHAs externer Dienstleister. Einfacher ist es, aus dem Browser das relevante Cookie mit den Authentifizierungsinformationen zu extrahieren und an den Python-Code zu übergeben. Der Rewe-Shop setzt eine ganze Menge Cookies, aber nur das Cookie rstp dient der Authentifizierung – wie wir durch schlichtes Ausprobieren ermittelt haben. Um Ihren Cookie-Wert zu extrahieren, melden Sie sich im Rewe-Shop an, öffnen die Entwickler-Tools des Browsers und gehen zum Tab „Web-Speicher“ (Firefox) beziehungsweise „App“ (Chrome/Chromium). Dort wählen Sie in der Seitenleiste unter „Cookies“ die Domain https://shop.rewe.de aus. Der Browser zeigt nun alle Cookies der Domain an, nach einem Doppelklick auf den Wert von rstp können Sie selbigen in die Zwischenablage kopieren.

Nachdem Sie den Wert im Python-Schnipsel eingefügt haben, können Sie den Code ausführen und das Notebook fragt Details zu allen Ihren Einkäufen ab:

import httpx

rstp = "<IHR RSTP-WERT>"

def fetch(id):
  url = f"https://shop.rewe.de/api/orders/{id}?constraint=rewe"
  return httpx.get(url,
    cookies = {"rstp": rstp}).json()

orders = [fetch(id) for id in oids]

Der Code nutzt HTTPX, einen verbreiteten HTTP-Client für Python. Das resultierende JSON-Dokument ist sehr umfangreich und direkt im Notebook nur schlecht anzuzeigen. Um einen Blick darauf zu werfen, schreiben Sie das Dokument besser in eine Datei:

import json
with open("orders.json", 'w') as f:
  json.dump(orders, f)

Mit Firefox kann man die Datei gut betrachten, weil dieser Browser das JSON-Format schön darstellt. Für Konsolenfreunde gibt es stattdessen etwa jless.

Das JSON-Array in orders enthält mehr Informationen als der offizielle Datenexport, aber seine Struktur ist ähnlich. Es enthält Objekte mit subOrders, die wiederum unter lineItems ein Array mit Bestellpositionen enthalten. Mit dem Ergebnis können Sie ähnliche Analysen wie im vorherigen Artikel anstellen – nur eben auf einer reichhaltigeren Datenbasis. Über die jetzt verfügbaren EANs könnten Sie Ihre Einkaufsdaten auch mit anderen Quellen verknüpfen, etwa den Daten des Projekts Open Food Facts.

Allerdings hat der detailliertere Datensatz immer noch Lücken; zum Beispiel fehlen Produktbeschreibungen. Schmerzhafter für eine ausführliche Analyse ist, wie grob die enthaltenen Kategorien sind: Sie stellen nur die oberste Ebene der Produktgruppen dar.

App dekompilieren

Einen praktischen API-Endpunkt für feinere Kategorien konnten wir auf der Rewe-Website nicht ohne Weiteres finden und wandten uns daher der Rewe-App zu. Mobile Apps lassen sich nicht so leicht wie Browser unter die Haube gucken, was die Übung aber umso lehrreicher macht: Apps zu analysieren ist eine vielseitige Fähigkeit, die nicht nur bei der Suche nach undokumentierten Schnittstellen hilft.

Apps bei der Arbeit zu beobachten und live zu sehen, wie sie mit APIs reden, ist ziemlich kompliziert – zumindest, wenn sie ihre Transportverschlüsselung gut implementieren. Dann kann man im Betrieb nämlich höchstens sehen, mit welchem Server eine App redet. Welche Anfragen sie mit welchen Parametern stellt, bleibt verborgen, ebenso wie die Antworten des Servers. Dieses Problem lässt sich lösen, aber es ist oft einfacher, stattdessen den Programmcode statisch zu untersuchen. Dafür benötigt man zunächst die APK-Datei der App auf dem Computer.

Google will nicht, dass man APK-Dateien einfach so aus dem Play Store herunterlädt, aber es gibt eine Reihe von inoffiziellen Webseiten, die APKs zum Download anbieten. Solche inoffiziellen Quellen eignen sich gut für die App-Analyse, sollten aber mit Vorsicht gehandhabt werden. Mitunter findet man dort auch Malware. Eine Alternative – zumindest für Apps, die man selbst nutzt – sind Android-Dateimanager wie Total Commander, die lokal installierte Apps als APK-Dateien zugänglich machen.

Nachdem man sich die APK-Datei besorgt hat, gilt es, die Java-Dateien daraus zu extrahieren. Diese liegen im speziellen DEX-Format vor, das Konverter wie dex2jar in das bekannte JAR-Archivformat übersetzen. dex2jar kann man einfach das gesamte APK vorwerfen:

dex2jar rewe.apk

Der Konverter antwortet darauf mit dex2jar rewe.apk -> ./rewe-dex2jar.jar und legt die JAR-Datei an. Anschließend kann ein Decompiler aus dem JAR-Archiv wieder lesbare Java-Quelldateien erstellen. Diese entsprechen zwar nicht exakt dem Original (beispielsweise haben Variablen andere Namen), sind aber funktional identisch und gut lesbar – deutlich besser als der Bytecode im JAR-Archiv. „JD“, der Platzhirsch unter den Java-Dekompilern, verschluckt sich leider an der Rewe-App, wenn man ihn anweist, alle dekompilierten Quelldateien zu speichern. Daher wichen wir dafür auf den Decompiler „CFR“ aus, der anstandslos einen Korpus von etwa 100 MByte Quelldateien im Ordner src erzeugt:

java -jar cfr.jar --outputdir src rewe-dex2jar.jar

In src findet sich sowohl der eigentliche App-Code (in de/rewe) als auch der Code von zahlreichen genutzten Bibliotheken. Man erkennt zum Beispiel am Ordner kotlin, dass die gleichnamige Programmiersprache samt ihrer Standardbibliothek zum Einsatz kam. Der Ordner retrofit2 zeigt an, dass die App die Bibliothek Retrofit nutzt. Kotlin und Retrofit sind im Android-Umfeld durchaus üblich und letzteres ist ein vielversprechender Hinweis: Mit dieser Bibliothek lassen sich HTTP-APIs nutzen. Retrofit verfolgt dabei einen besonderen Ansatz: Statt im Java-Code explizit HTTP-Aufrufe über Methoden der Bibliothek zu tätigen, modelliert man das API als Java- oder Kotlin-Interface und annotiert die einzelnen Methoden mit den Details des API.

Decompiler wie JD zeigen die Inhalte einer App, inklusive einer Rekonstruktion des Quellcodes.
Decompiler wie JD zeigen die Inhalte einer App, inklusive einer Rekonstruktion des Quellcodes.

API-Analyse

Der Unterordner de/rewe/api ist eine naheliegende heiße Spur. Zum Beispiel findet sich in de/rewe/api/product/ProductApi.java das Interface ProductApi, unter anderem mit der Methode eanSearch():

@GET(value="products/ean/{ean}")
@Headers(value={"ruleVersion: 1"})
public
  Single<Response<ApiEanResponse>>
  eanSearch(
    @Path(value="ean") String var1
  );

Retrofit interpretiert Annotationen wie @GET und @Headers und implementiert diese Methode automatisch so, dass ein Aufruf mit einem Parameterwert wie "123" zu einem HTTP-GET-Request auf den Pfad products/ean/123 führt.

Zusätzlich setzt die App bei der eanSearch-Methode noch einen Request-Header, nämlich ruleVersion: 1. Womöglich handelt es sich hierbei um eine interne Versionierung, denn der Header ruleVersion ist nicht im HTTP-Standard spezifiziert. Übrigens können Sie die Metadata-Annotationen, die sich überall in den dekompilierten Dateien finden, getrost ignorieren; sie stammen vom Kotlin-Compiler und enthalten interne Daten für Reflection.

Um den gefundenen API-Aufruf auszuprobieren, fehlt noch ein kleines, aber wichtiges Detail: der Hostname. Um Hosts von HTTP-APIs im Quelltext von Apps zu finden – was oft interessante Ergebnisse liefert – sucht man am einfachsten nach „https://“. (Und nach „http://“, um zu sehen, wie genau es eine App mit der Transportverschlüsselung nimmt.) Die Rewe-App zeigt sich recht geschwätzig und verrät nicht nur den gesuchten Server (https://mobile-api.rewe.de), sondern auch die Hostnamen von Testumgebungen und eingebauten Trackern.

Nun kann man den EAN-Code eines Rewe-Produkts nutzen, um sich eine vollständige URL zusammenzubasteln. Die EAN-Codes von Produkten finden sich als gtin bereits in unserem JSON-Array orders. Für einen ersten Test tut es aber auch einfach der Strichcode von einem bei Rewe gekauften Produkt. 3-lagiges Toilettenpapier von Danke hat etwa die EAN 9011111035264. Eine komplette URL lautet also https://mobile-api.rewe.de/products/ean/9011111035264.

Ein schneller Test zeigt, dass Rewes Server tatsächlich mit einem JSON-Datensatz über das Klopapier antwortet. Die Daten enthalten nicht nur den Namen und die interne Produkt-ID, sondern auch eine ausführliche Beschreibung und eine ganze Liste von Produktkategorien:

{
  "items": [{
    "articleId": "9011111035264",
    "productId": "5001285",
    "description": "• Das DANKE WC …",
    "categoryIds": [
      "1066", "1067", "1072", "1969"
    ],
    "ean": "9011111035264",
    "name": "Danke Toilettenpapier …"
  }],
  "resultsSource": "REWE_GTIN"
}

Für den Test eignet sich jeder Browser, Firefox gefällt wieder durch seine schöne JSON-Darstellung. Konsolenfreunde nutzen stattdessen zum Beispiel curl, und wer richtig tief in die API-Analyse einsteigen will, kann spezialisierte Clients wie Insomnia oder Postman benutzen. Sie erlauben beispielsweise beliebige Header zu definieren und den Überblick über die verschiedenen Endpunkte zu bewahren.

Dass der Request ohne Weiteres im Browser funktioniert, ist übrigens interessant, schließlich schickt der Browser standardmäßig nicht den erwähnten ruleVersion-Header mit – was der Server offenbar ignoriert. Dass Rewe hier keine Authentifizierung verlangt, überrascht nicht, die Daten sind weder personenbezogen noch geheim.

Spezialisierte API-Clients wie Insomnia helfen bei weitergehenden API-Experimenten.
Spezialisierte API-Clients wie Insomnia helfen bei weitergehenden API-Experimenten.

Kategorisierung

Mehr als eine Kategorie ist zwar vielversprechend, allerdings liefert dieser API-Endpunkt bloße IDs, keine sinnvollen Namen. Aber vielleicht gibt es ja einen anderen Endpunkt, der Kategorienamen und -IDs ausliefert? Ein Blick in die Datei CategoriesApi.java im Ordner de/rewe/api/category ist naheliegend. Die Datei definiert ein Interface mit der Methode getCategories():

@GET(value="mobile/categories/{storeId}")
public
  Single<ApiCategorySearchResponse>
  getCategories(
    @Path(value="storeId") String var1
  );

Die Methode verlangt den Parameter storeID, den wir nicht kennen. Statt uns näher mit Store-IDs zu beschäftigen, vertrauen wir wieder auf die hierarchische Struktur typischer APIs und testen einfach eine Anfrage ohne ID an https://mobile-api.rewe.de/mobile/categories/. Das klappt, der Server antwortet mit einer umfänglichen Kategorienliste:

{
  "topLevelCategories": [{
    "id": "1",
    "name": "Obst & Gemüse",
    "slug": "obst-gemuese",
    "imageUrl": "https://…",
    "childCategories": [{
      "id": "2",
      "name": "Gemüse",
      "slug": "obst-gemuese-gemuese",
      "imageUrl": "https://…",
      "childCategories": [{
        "id": "30",
        "name": "Tomaten",
        "slug": "obst-gem[…]-tomaten",
        "imageUrl": "https://…"
      },
      ...

Auffälligerweise sind die Kategorienamen nicht eindeutig. Beispielsweise findet sich die Kategoriebezeichnung „Tomatensuppe“ einmal unter „Dosengerichte & Eintöpfe“ und einmal unter „Bechergerichte & -suppen“ – jeweils aber mit verschiedener ID. Der „Slug“ ist hingegen eindeutig und enthält mit Bindestrichen verknüpft die gesamte Hierarchie, wie zum Beispiel nahrungsmittel-fertiggerichte-konserven-bechergerichte-suppen-tomatensuppe.

Jedenfalls ordnen diese Informationen den Kategorien Namen zu. Nun steht einer ausführlichen Analyse nichts mehr im Wege: Zuerst reichert man Rewes Datenexport über die Bestellnummern und das Browser-API mit mehr Daten an. Anschließend liefert das ProductApi der App eine Beschreibung und sämtliche Kategorien für jedes gekaufte Produkt. Das CategoriesApi steuert die Kategorienamen und -hierarchie bei.

In unseren Experimenten konnten wir der großen Mehrheit der von uns gekauften Produkten detaillierte Kategorien mit bis zu vier Ebenen zuordnen. Nur manche, vor langer Zeit bestellte Produkte waren nicht mehr abrufbar und der Server antwortete mit einem Fehler 404.

Mit den jetzt umfänglich angereicherten Daten kann man alle möglichen Analysen anstellen. Nur als ein Beispiel haben wir die Python-Bibliothek „Plotly“ benutzt, um eine interaktive Treemap unserer Einkäufe nach Kategorien zu generieren. Den Code, der alle im Artikel beschriebenen Schritte abdeckt, von der Bestellnummern-Extraktion aus dem exportierten Datensatz bis zum Malen einer Treemap Ihrer Einkäufe, finden Sie hier.

Sehr gut, viel Obst und Gemüse: Mit ausreichend angereicherten Daten kann man die eigenen Einkäufe vielfältig untersuchen.
Sehr gut, viel Obst und Gemüse: Mit ausreichend angereicherten Daten kann man die eigenen Einkäufe vielfältig untersuchen.

Fazit

Wer mag, kann die Analyse noch viel weiter treiben, etwa indem er die erwähnten Daten von Open Food Facts einbindet. Nicht zuletzt lernt man durch solche Experimente allgemeine Fähigkeiten, von der Datenanalyse mit Python über die Inspektion von Web-Traffic im Browser bis zum Dekompilieren von Apps.