Google-Jobs-Listings mit Python scrapen

Unternehmen, Standort und Bewerbungslinks von Google Jobs als JSON, und die eine Antwortform, die naiven Parsing-Code bei einem ganz normalen HTTP 200 zum Absturz bringt. Getestet im August 2026.

Du willst Job-Listings für eine Rolle und einen Ort, Firmenname, Standort, einen Bewerbungslink, in einem Umfang, der über das Durchklicken von Googles Job-Karten eine nach der anderen hinausgeht: ein Nischen-Jobboard, ein Market-Intelligence-Feed, oder ein Skript, das über die folgenden Wochen verfolgt, wer für einen Titel einstellt.

Der Request

Ein Call an /v1/jobs nimmt eine Suchquery genauso, wie du sie in Google eintippen würdest, plus optionale country, language und domain, wenn du Ergebnisse brauchst, die auf einen bestimmten Markt eingegrenzt sind.

Die Antwortform, die deinen Code bricht, wenn du sie ignorierst

Google liefert nicht für jede Query ein Jobs-Modul, und wenn nicht, antwortet /v1/jobs trotzdem mit einem HTTP 200. Der Body ist {"jobs_results": ["There are no jobs for this query"]}, eine Liste mit einem einzelnen String statt Job-Objekten. Wir haben das am 27. August 2026 reproduziert: Die Query “product manager” lieferte bei jedem Versuch diese Nachricht, während “product manager jobs” zehn echte Listings für eine funktional identische Suche lieferte. Eine Schleife, die annimmt, jobs_results sei eine Liste von Dicts, for job in results: print(job["title"]), wirft einen TypeError, sobald sie den String erreicht, weil man einen String nicht mit einem anderen String indizieren kann. Der Fix ist, zu prüfen, dass jeder Eintrag ein Dict mit einem title ist, bevor du ihn als Job behandelst, nicht nur, dass die Liste nicht leer ist:

import requests

API_KEY = "faf_live_..."  # dein FastAF-Key

resp = requests.get(
    "https://api.fastaf.co/v1/jobs",
    params={"query": "product manager jobs"},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=60,
)
resp.raise_for_status()
raw_results = resp.json()["jobs_results"]

# Google hat nicht immer ein Jobs-Modul für eine Query. Wenn nicht,
# ist jobs_results ["There are no jobs for this query"], ein String, kein Job.
listings = [r for r in raw_results if isinstance(r, dict) and "title" in r]

if not listings:
    print("No jobs module for this query.")

for job in listings:
    apply_options = job.get("apply_options", [])
    apply_via = apply_options[0]["title"] if apply_options else None
    print(f'{job["title"]} @ {job["company_name"]} ({job["location"]})')
    print(f'  apply via {apply_via}, share link: {job["share_link"]}')

Wir haben das beim Schreiben des Posts gegen eine echte “data engineer”-Suche laufen lassen (August 2026): Alle zehn Einträge in jobs_results waren echte Job-Objekte, listings kam also mit derselben Länge wie die Rohliste heraus. Die Absicherung kostet nichts, wenn das Modul vorhanden ist, sie stoppt nur den Absturz, wenn nicht.

Jedes Listing in unserem Sample hatte eine apply_options-Liste, Paare aus {title, link} wie {"title": "LinkedIn", "link": "/goto?..."}, ein Eintrag pro Seite, die die Stelle listet: zuerst die eigene Karriereseite des Unternehmens, dann welche von Indeed, LinkedIn, ZipRecruiter, Glassdoor und ähnlichen Boards sie auch aufgegriffen haben. Der link in jedem Eintrag ist ein relativer Pfad, keine fertige URL zum Öffnen, speichere ihn also zusammen mit seinem title, statt anzunehmen, dass er als eigenständiger Href funktioniert. Jedes Listing hatte außerdem einen separaten share_link, eine vollständige absolute URL zurück zu derselben Stelle auf Googles eigener Jobsuche-Seite. Das ist das einzige Feld im Payload, das für sich allein eine komplette URL ist, was es zur natürlichen Wahl für einen „Online ansehen”-Link oder einen stabilen Key macht, um einen Job über zwei Suchläufe hinweg zu deduplizieren.

Was ein Listing sonst noch enthält

Über Titel, Unternehmen und Standort hinaus trägt jeder Eintrag via (die Seite, der Google das Listing zuschreibt), extensions (kurze Tags wie “Full-time” oder “20 days ago”), eine vollständige description, und job_highlights, eine Liste gruppierter Bullet Points unter Überschriften wie “Qualifications”, “Responsibilities” und “Benefits”, wenn Googles Parser sie in der Stellenanzeige gefunden hat. Nicht jedes Listing hat jede Gruppe: In unserem Sample ließ die Hälfte der Anzeigen “Benefits” komplett aus, und eine hatte nur “Responsibilities”, lies job_highlights also als Liste, die du nach Überschrift durchsuchst, nicht als festes Set benannter Felder.

Was es kostet

/v1/jobs kostet 5 Credits pro Call, egal ob er zehn Listings oder den No-Results-String liefert. Die 500 Credits des Free Tiers decken 100 Calls ab. Mit dem Scale-Paket ($99 für 300.000 Credits) sind das 60.000 Calls, $1,65 pro 1.000.

Kurzversion

Bau die isinstance- und title-Prüfung von Anfang an in dein Parsing ein, denn die No-Jobs-Antwort ist ein normaler, wohlgeformter 200er, kein Fehler, den dein try/except je zu sehen bekommt. Davon abgesehen bringt dich apply_options zur Stellenanzeige, und share_link gibt dir eine stabile URL für das Listing selbst. Hol dir 500 kostenlose Credits, keine Karte und lass deine eigenen Titel-und-Standort-Kombinationen durchlaufen.