Comment scraper les offres d'emploi Google Jobs avec Python

Entreprise, lieu et liens de candidature depuis Google Jobs en JSON, et la forme de réponse qui casse un parsing naïf sur un simple HTTP 200. Testé en août 2026.

Vous voulez des offres d’emploi pour un poste et un lieu donnés, nom de l’entreprise, localisation, lien pour postuler, à une échelle qui dépasse le clic manuel carte par carte sur Google : un job board de niche, un flux de veille marché, ou un script qui suit qui recrute pour un intitulé donné au fil des semaines.

La requête

Un appel à /v1/jobs prend une requête de recherche exactement comme vous la taperiez dans Google, plus des paramètres optionnels country, language et domain si vous avez besoin de résultats ciblés sur un marché particulier.

La forme de réponse qui cassera votre code si vous l’ignorez

Google ne renvoie pas un module d’offres pour chaque requête, et quand ce n’est pas le cas, /v1/jobs répond quand même avec un HTTP 200. Le corps vaut {"jobs_results": ["There are no jobs for this query"]}, une liste contenant une seule chaîne au lieu d’objets d’offre. Nous avons reproduit ça le 27 août 2026 : la requête « product manager » a renvoyé ce message à chaque tentative, tandis que « product manager jobs » a renvoyé dix vraies offres pour une recherche pourtant fonctionnellement identique. Une boucle qui suppose que jobs_results est une liste de dicts, for job in results: print(job["title"]), lève un TypeError dès qu’elle atteint la chaîne, parce qu’on ne peut pas indexer une chaîne avec une autre chaîne. La correction consiste à vérifier que chaque entrée est un dict avec un title avant de la traiter comme une offre, pas juste que la liste est non vide :

import requests

API_KEY = "faf_live_..."  # votre clé FastAF

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 n'a pas toujours de module d'offres pour une requête. Quand ce n'est
# pas le cas, jobs_results vaut ["There are no jobs for this query"], une
# chaîne, pas une offre.
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"]}')

Nous avons testé ça sur une vraie recherche « data engineer » en écrivant l’article (août 2026) : les dix entrées de jobs_results étaient toutes de vrais objets d’offre, donc listings est ressorti avec la même longueur que la liste brute. Le garde-fou ne coûte rien quand le module est présent ; il évite juste le plantage quand il ne l’est pas.

Les liens de candidature vivent dans apply_options, pas sur l’offre elle-même

Chaque offre de notre échantillon portait une liste apply_options, des paires {title, link} comme {"title": "LinkedIn", "link": "/goto?..."}, une entrée par site qui liste l’annonce : la page carrières de l’entreprise elle-même, puis celui ou ceux parmi Indeed, LinkedIn, ZipRecruiter, Glassdoor et sites similaires qui l’ont aussi repris. Le link de chacune est un chemin relatif, pas une URL prête à ouvrir, donc stockez-le à côté de son title plutôt que de supposer qu’il marche comme href autonome. Chaque offre portait aussi un share_link séparé, une URL absolue complète qui renvoie vers cette même annonce sur la page de recherche d’emploi de Google. C’est le seul champ de la réponse qui soit une URL complète en soi, ce qui en fait le choix naturel pour un lien “voir en ligne” ou une clé stable pour dédupliquer une offre entre deux recherches.

Ce qu’il y a d’autre sur une offre

Au-delà du titre, de l’entreprise et du lieu, chaque entrée porte via (le site auquel Google attribue l’offre), extensions (des tags courts comme “Full-time” ou “20 days ago”), une description complète, et job_highlights, une liste de puces groupées sous des titres comme “Qualifications”, “Responsibilities” et “Benefits” quand le parseur de Google les a trouvés dans l’annonce. Toutes les offres n’ont pas tous les groupes : dans notre échantillon, la moitié des annonces n’avait pas du tout de “Benefits” et une n’avait que “Responsibilities”, donc traitez job_highlights comme une liste à parcourir par titre, pas comme un ensemble fixe de champs nommés.

Ce que ça coûte

/v1/jobs coûte 5 crédits par appel, qu’il renvoie dix offres ou la chaîne d’absence de résultat. Les 500 crédits de l’offre gratuite couvrent 100 appels. Sur le pack Scale (99 $ pour 300 000 crédits), ça fait 60 000 appels, 1,65 $ pour 1 000.

En bref

Intégrez la vérification isinstance et title dans votre parsing dès le départ, parce que la réponse sans offre est un 200 normal et bien formé, pas une erreur que votre try/except verra jamais. À part ça, apply_options vous amène à l’annonce et share_link vous donne une URL stable pour l’offre elle-même. Récupérez 500 crédits gratuits, sans carte et testez vos propres combinaisons de titre et de lieu.