DEV Community

Plain Signal
Plain Signal

Posted on

How to Scrape Google Jobs with Python (Salaries, Apply Links and Daily Alerts)

Google Jobs is the job search built into Google. Search for "data analyst Chicago" and Google shows a box of listings pulled from LinkedIn, Indeed, Glassdoor, ZipRecruiter, company career pages and hundreds of smaller job boards. For job market research, salary benchmarks, a niche job board or lead generation ("who's hiring right now?"), it's one of the best sources there is.

There's no official API for it. Google's Cloud Talent Solution is for searching your own job postings, not Google's index. And scraping it yourself is annoying: Google blocks plain HTTP clients quickly, shows only 10 jobs per page and loads the rest with JavaScript.

This tutorial shows how to get Google Jobs results as clean JSON or CSV in three ways: without code, with a short Python script, and as a daily alert to Slack. It uses the Google Jobs Scraper on Apify, which I built for exactly this. All code is in the examples repo.

What you get per job

{
  "title": "Software Engineer I",
  "companyName": "Indeed",
  "location": "Austin, TX",
  "postedAt": "2026-09-15",
  "employmentTypes": ["FULLTIME"],
  "salary": "94K–142K a year",
  "salaryMin": 94000,
  "salaryMax": 142000,
  "salaryPeriod": "YEAR",
  "applyOptions": [
    { "publisher": "Indeed", "url": "https://www.indeed.com/viewjob?jk=177a2f345faeafd5" },
    { "publisher": "Built In", "url": "https://builtin.com/job/software-engineer-i/..." }
  ],
  "qualifications": ["Bachelor's degree in Computer Science or related field", "..."],
  "description": "About the role ..."
}
Enter fullscreen mode Exit fullscreen mode

The parts that are hardest to get yourself:

  • Direct apply links to every board that lists the job, not Google redirect URLs. One job usually has several (LinkedIn, Indeed, the company's own site).
  • Parsed salaries: salaryMin, salaryMax, salaryCurrency and salaryPeriod next to the raw text.
  • A real posting date: Google only says "3 days ago"; postedAt turns that into a date.
  • Highlights and the full description: qualifications, responsibilities and benefits as lists.

Option 1: no code

  1. Open the Google Jobs Scraper and click Try for free. A free Apify account is enough.
  2. Enter one or more search queries (data analyst) and locations (Chicago, IL).
  3. Optionally pick a date range (last 3 days, last week…) and an employment type.
  4. Click Start, then download the results as CSV, Excel or JSON.

Option 2: Python

Install the Apify client and set your API token (Console → Settings → Integrations):

pip install apify-client
export APIFY_TOKEN=...
Enter fullscreen mode Exit fullscreen mode

This script searches for data analyst jobs in Chicago posted in the last week, saves them as CSV and prints the median advertised salary:

import csv
import os
from decimal import Decimal
from statistics import median

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("plain-signal/google-jobs-scraper").call(
    run_input={
        "queries": ["data analyst"],
        "locations": ["Chicago, IL"],
        "country": "us",
        "datePosted": "week",
        "maxJobsPerSearch": 30,
        "includeDescription": False,
    },
    max_total_charge_usd=Decimal("0.50"),  # hard cap on what this run can cost
)
jobs = list(client.dataset(run.default_dataset_id).iterate_items())

with open("jobs.csv", "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["title", "company", "location", "posted", "salary", "apply_url"])
    for j in jobs:
        apply_url = j["applyOptions"][0]["url"] if j["applyOptions"] else j["googleJobsUrl"]
        w.writerow([j["title"], j["companyName"], j["location"], j["postedAt"], j["salary"], apply_url])

yearly = [(j["salaryMin"] + j["salaryMax"]) / 2 for j in jobs if j["salaryPeriod"] == "YEAR" and j["salaryMin"]]
print(f"{len(jobs)} jobs saved to jobs.csv")
if yearly:
    print(f"{len(yearly)} list a yearly salary, median midpoint ${median(yearly):,.0f}")
Enter fullscreen mode Exit fullscreen mode

Output from a run on 2026-10-02:

30 jobs saved to jobs.csv
6 list a yearly salary, median midpoint $78,750
Enter fullscreen mode Exit fullscreen mode
title,company,location,posted,salary,apply_url
"Data Analyst, Marketing (Hybrid)",Rewards Network,"Chicago, IL",2026-10-01,,https://www.linkedin.com/jobs/view/...
"Data Analyst, Intern",Stripe,"Chicago, IL",2026-10-01,,https://www.linkedin.com/jobs/view/...
Enter fullscreen mode Exit fullscreen mode

Two things worth knowing:

  • max_total_charge_usd caps the cost of the run. The scraper stops once it's reached, so a typo in maxJobsPerSearch can't cost you more than you planned.
  • Only about a third of US jobs show a salary on Google. If salary analysis is the goal, run more queries or locations to get a bigger sample.

Option 3: a daily job alert in Slack

The scraper has a monitor mode: give it a monitorName, and each run returns only jobs that earlier runs with the same name haven't returned. Run it once a day and you have a job alert:

import json
import os
import urllib.request
from decimal import Decimal

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("plain-signal/google-jobs-scraper").call(
    run_input={
        "queries": ["product designer"],
        "locations": ["Remote"],
        "country": "us",
        "datePosted": "3days",
        "maxJobsPerSearch": 50,
        "includeDescription": False,
        "monitorName": "remote-product-design",
    },
    max_total_charge_usd=Decimal("0.50"),
)
jobs = list(client.dataset(run.default_dataset_id).iterate_items())

lines = [f"*{j['title']}* at {j['companyName']} ({j['location']}) {j['salary'] or ''}\n"
         f"{j['applyOptions'][0]['url'] if j['applyOptions'] else j['googleJobsUrl']}" for j in jobs]
text = f"{len(jobs)} new jobs\n\n" + "\n\n".join(lines) if jobs else "No new jobs today."

webhook = os.environ.get("SLACK_WEBHOOK_URL")
if webhook:
    req = urllib.request.Request(webhook, data=json.dumps({"text": text}).encode(),
                                 headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req)
else:
    print(text)
Enter fullscreen mode Exit fullscreen mode

The first run returns everything that matches (50 jobs in my test). Later runs return only what's new. Schedule it with cron or GitHub Actions. If you'd rather not run code at all, create a schedule in the Apify Console and connect the Slack, Google Sheets or email integration to the actor.

What does it cost?

The scraper charges $0.005 per job ($5 per 1,000), with no monthly fee. The 30-job Chicago search above cost $0.15, and a daily alert with about 30 new jobs a day comes to around $4.50 a month. Duplicates and jobs removed by your filters aren't charged. Apify's free plan includes monthly platform credit, which is enough to try all of the above.

How it gets more than 10 jobs per search

Google renders only the first 10 jobs of a search on the server. The rest come from infinite scrolling in the browser. Instead of driving a headless browser, the scraper runs your search again with Google's own filter phrases appended ("full time", "remote", "entry level", "in the last 3 days"…). Google Jobs treats these as filters, so every result still matches your query, but each variant surfaces a different set of 10. Merged and deduplicated by Google's job ID, that usually gives 50 to 150 unique jobs per search.

For broad coverage, several specific searches beat one broad one: "nurse" in five cities returns far more distinct jobs than "nurse" in one state.

Tips

  • Other countries: set country (gb, de, in, ca, au…) and use local place names. Keep language on en for the most complete data. In some countries (for example Germany) Google shows only a preview of the description.
  • Company names: for some listings Google names the job board as the company (e.g. "Indeed"). The apply links still point to the real posting.
  • Speed: a 30-job search takes under a minute, a 50-job search about two. Searches run in parallel, so 10 searches don't take 10 times as long.

Wrapping up

All three examples are in the plain-signal/examples repo, along with an example for the Funding Rounds Tracker (US companies that just raised money, from SEC filings). Questions and feature requests are welcome in the comments or on the actor's Issues tab.

This article was written with AI assistance. All code was run against the live scraper before publishing, and the outputs shown are real.

Top comments (0)