Build a Multi-Source Job Search Tool

Automate job searches across Google Jobs and career pages with Python.

Introduction

30 Second Summary

Job searching is exhausting. You check one board, then another, then another, and still miss listings posted directly on company websites that never appear on the big aggregators.

In this project, you will build a Python script that reads your resume, extracts relevant keywords, and automatically searches for matching jobs across Google Jobs and company career pages.

What You'll Build

A Python-based job search tool that uses SerpAPI to find local, remote, and company-direct job listings personalized to your resume.

By the end of this project, you'll have:

  • A working Python script that reads your resume and automatically searches for matching jobs across Google Jobs and company career pages.
  • A deduplicated Excel spreadsheet with job listings including salary, benefits, degree requirements, and direct apply links.
  • A reusable tool you can re-run weekly to refresh results without manually searching job boards.
  • Secret Mission: Add a resume match scoring system that ranks every job by how closely it matches your skills.

Are there any prerequisites?

None. This project includes full setup instructions for Python, VS Code, and your SerpAPI account. You do not need any prior programming experience to follow along.

Install Python and VS Code

To build a job search tool, you need two foundational pieces: a programming language to write your script in, and an editor to write it with.

In this step, you will install Python (the language your script runs on) and VS Code (the editor where you will write and run your code). You will also create a dedicated project folder and set up an isolated environment for your packages.

In this step, get ready to:
  • Install Python 3.14 on your Windows machine.
  • Set up VS Code with the Python extension.
  • Create your project folder and virtual environment.
Download and install Python 3.14

Python is the programming language your job search script is written in. You need it installed on your machine before you can run any code.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check if Python is already installed by running this command:
python --version

Look at what appears in your terminal and pick the tab that matches.

✔️ I see Python 3.14

You should see something like Python 3.14.6. Python is ready to go. Skip ahead to the next substep.

ⓧ I see an older version

If you see a version like Python 3.12.x or Python 3.13.x, you need to install Python 3.14 alongside it. The new installer will update your PATH to point to the newer version.

  • Open your browser and go to python.org/downloads.
  • Click the large Download Python 3.14 button.
  • Run the downloaded installer.
  • On the first page of the installer, check the box that says Add python.exe to PATH before clicking anything else.
  • Click Install Now.
  • Wait for the installation to complete, then click Close.

Why check the PATH box?

Adding Python to PATH lets you run python from any terminal window. Without this, Windows will not know where to find the Python program when you type the command.

  • Close your PowerShell window completely.
  • Open a new PowerShell window (press Windows key, type PowerShell, press Enter).
  • Verify the new version by running:
python --version

You should now see Python 3.14.6 or similar.

ⓧ Command not found or Microsoft Store opens

If you see an error or the Microsoft Store opens, Python is not installed yet.

  • Close the Microsoft Store if it opened.
  • Open your browser and go to python.org/downloads.
  • Click the large Download Python 3.14 button.
  • Run the downloaded installer.
  • On the first page of the installer, check the box that says Add python.exe to PATH before clicking anything else.
  • Click Install Now.
  • Wait for the installation to complete, then click Close.

Why check the PATH box?

Adding Python to PATH lets you run python from any terminal window. Without this, Windows will not know where to find the Python program when you type the command.

  • Close your PowerShell window completely.
  • Open a new PowerShell window (press Windows key, type PowerShell, press Enter).
  • Verify the installation by running:
python --version

You should see Python 3.14.6 or similar.

Still not seeing Python 3.14?

Make sure you closed and reopened PowerShell after installing. PATH changes only take effect in new terminal windows.

If you forgot to check the Add python.exe to PATH box, re-run the installer, select Modify, and enable the PATH option.

Help me fix my Python PATH

Download and install VS Code

VS Code is a free code editor that gives you syntax highlighting, a built-in terminal, and extensions that make Python development smoother.

  • Open your browser and go to code.visualstudio.com.
  • Click the Download for Windows button.
  • Run the downloaded installer and accept the defaults.
  • Once the installation finishes, open VS Code by pressing the Windows key, typing VS Code, and pressing Enter.

Now install the Python extension so VS Code understands your Python files.

  • Open the Extensions panel by pressing Ctrl+Shift+X.
  • Type Python in the search bar.
  • Find the extension named Python by Microsoft (it should be the first result).
  • Click Install.

What does the Python extension do?

The Python extension adds code completion, error highlighting, and debugging support for Python files. It also helps VS Code detect your virtual environments automatically.

Don't see the Python extension?

Make sure you typed Python (capital P) in the Extensions search bar. The correct one has the Microsoft logo and millions of installs listed.

Help me find the Python extension

Create your project folder and virtual environment

A virtual environment is an isolated space where your project's packages live. It keeps this project's dependencies separate from anything else on your machine.

  • In VS Code, open the built-in terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir job-search-tool
cd job-search-tool
  • Confirm the folder was created by running:
ls

You should see an empty folder (no files listed yet). That is expected.

Now open this folder as your VS Code workspace so the editor is focused on it.

  • Click File in the top menu bar of VS Code.
  • Click Open Folder.
  • Navigate to your Desktop, select the job-search-tool folder, and click Select Folder.
  • If VS Code asks whether you trust the authors of the files in this folder, click Yes, I trust the authors.

VS Code now shows the job-search-tool folder name in the Explorer sidebar on the left.

  • Open a fresh terminal in this workspace by pressing Ctrl+`.
  • Create a virtual environment by running this command:
python -m venv .venv

What does this command do?

The python -m venv .venv command creates a new folder called .venv inside your project. This folder contains a copy of the Python interpreter and a clean space for installing packages.

Anything you install here stays isolated from the rest of your system.

  • Activate the virtual environment by running:
.venv\Scripts\activate
  • Look at your terminal prompt. You should see (.venv) appear at the beginning of the line.

How do I know it worked?

The (.venv) prefix in your terminal prompt means the virtual environment is active. Any packages you install with pip from now on will go into this isolated environment instead of your system-wide Python.

Seeing a permissions error when activating?

Windows PowerShell sometimes blocks scripts by default. If you see an error about execution policies, run this command first:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Then try the activation command again. This tells Windows to allow local scripts (like your virtual environment's activate script) to run.

Help me fix this permissions error

Your environment is ready. Next up, you will install the packages your job search script needs and connect to the SerpAPI service that powers the job searches.

Install Packages and Set Up SerpAPI

Your development environment is ready to go. Now you need the building blocks that make your job search tool work.

In this step, you will install four Python packages that handle different parts of the pipeline. You will also create a free SerpAPI account and wire up your project files so the script can talk to the search API securely.

In this step, get ready to:
  • Install the four Python packages your project needs.
  • Create a free SerpAPI account and store your API key safely.
  • Set up the starter Python file and your resume text file.
Install the required packages

Your project relies on four packages. Each one handles a specific job in the pipeline.

  • Install all four packages in one command by running this in your terminal:
pip install serpapi==1.0.2 pandas==3.0.4 openpyxl==3.1.5 python-dotenv==1.2.2

What do these packages do?

  • serpapi connects your script to SerpAPI's search engines (Google Jobs, Google Search).
  • pandas organizes your job results into a table structure for easy filtering and export.
  • openpyxl lets pandas write Excel spreadsheets (.xlsx files).
  • python-dotenv loads your API key from a .env file so you never hard-code secrets into your script.
  • Wait for the install to finish. You should see a line that says Successfully installed followed by the four package names.

Seeing a 'pip not found' error?

Make sure your virtual environment is still active. You should see (.venv) at the start of your terminal prompt. If it disappeared, reactivate it by running:

.venv\Scripts\activate

Then try the pip install command again.

Help me fix my pip install error.

Create a SerpAPI account and configure your project files

SerpAPI is the service that gives your Python script access to Google's job search results. The free tier includes 250 searches per month, which is more than enough for this project.

  • Open your browser and go to serpapi.com/users/sign_up to create a free account.
  • Complete the signup form with your email and a password.
  • After signing in, go to serpapi.com/manage-api-key to view your API key.
  • You will see a long string labeled Your Private API Key. Click the copy icon next to it.

What is an API key?

An API key is a unique password that identifies your account when your script sends requests to SerpAPI. It lets SerpAPI track your usage against the free tier limit of 250 searches per month.

Now you will store the key in a safe place where your Python script can read it without exposing the secret in your code.

  • In VS Code, click the new file icon at the top of the Explorer sidebar.
  • Name the file .env and press Enter.
  • Paste the following line into the file, replacing your-api-key-here with the API key you just copied:
SERPAPI_KEY=your-api-key-here

Why use a .env file?

Storing secrets in a .env file keeps them separate from your code. If you ever share your script or push it to GitHub, you can exclude .env so your key stays private.

  • Save .env by pressing Ctrl+S.

Next, create the main Python file that will hold your job search code.

  • Click the new file icon in the Explorer sidebar again.
  • Name the file job_search.py and press Enter.
  • Add the initial imports and API client setup by pasting this code into job_search.py:
import os
import serpapi
import pandas as pd
from dotenv import load_dotenv

# Load API key from .env file
load_dotenv()
API_KEY = os.getenv("SERPAPI_KEY")

What does this code do?

  • load_dotenv() reads your .env file and makes the values inside it available to your script.
  • os.getenv("SERPAPI_KEY") grabs the API key value so your script can authenticate with SerpAPI without you typing the key directly into the code.
  • The import serpapi and import pandas as pd lines load the packages you just installed so you can use them later.
  • Save job_search.py by pressing Ctrl+S.

VS Code showing a yellow underline on imports?

If VS Code underlines import serpapi or import pandas, it may not be using your virtual environment's Python interpreter. Press Ctrl+Shift+P, type Python: Select Interpreter, and choose the one that includes .venv in its path.

Help me fix import errors in VS Code.

Create your resume text file

Your job search tool reads a plain text version of your resume to extract skills and job titles. It uses those keywords to build targeted search queries.

  • Click the new file icon in the Explorer sidebar.
  • Name the file resume.txt and press Enter.
  • Open your actual resume (Word doc, PDF, or Google Doc) and select all the text.
  • Paste your resume content into resume.txt as plain text. Include your skills, job titles, experience, and education.

No resume handy?

You can use placeholder text for now and swap in your real resume later. Paste this starter content into resume.txt:

Paste your full resume content here as plain text.
Include your skills, job titles, experience, and education.
The script will scan this file for matching keywords.
  • Save resume.txt by pressing Ctrl+S.
  • Check the Explorer sidebar in VS Code. You should now see four items in your job-search-tool folder: .env, .venv, job_search.py, and resume.txt.

Don't see all four items?

Make sure you created each file inside the job-search-tool folder, not somewhere else on your computer. If a file ended up in the wrong place, right-click it in the Explorer and select Delete, then create it again inside the correct folder.

Help me find my missing project files.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the complete content for each file at this point:

SERPAPI_KEY=your-api-key-here
import os
import serpapi
import pandas as pd
from dotenv import load_dotenv

# Load API key from .env file
load_dotenv()
API_KEY = os.getenv("SERPAPI_KEY")
Paste your full resume content here as plain text.
Include your skills, job titles, experience, and education.
The script will scan this file for matching keywords.

Your project is wired up and ready to go. In the next step, you will write the code that reads your resume and automatically extracts the keywords it will use to search for jobs.

Build the Resume Keyword Extractor

Your packages are installed, your SerpAPI key is stored, and your resume is ready to scan. Now you need to teach your script which words to look for.

In this step, you will build a keyword extractor that reads your resume, finds relevant job titles and skills, and turns them into search queries. This is the foundation that personalizes every job search to your background.

In this step, get ready to:
  • Define keyword lists for data/IT job titles and skills.
  • Write a function that scans your resume for matching keywords.
  • Build search queries from the matched titles and verify the output.
Define the search configuration and keyword lists

Before your script can scan a resume, it needs to know what to look for. You will define lists of common data analytics and IT job titles, plus a list of technical skills.

  • In VS Code, open job_search.py.
  • Find the line that says API_KEY = os.getenv("SERPAPI_KEY"), then add the following code below it:
# Search configuration
LOCATION = "San Antonio, Texas, United States"
RADIUS_KM = "25"
MAX_QUERIES = 2

# Job titles to look for in resume
DATA_TITLES = [
    "data analyst", "data entry", "business analyst",
    "data engineer", "business intelligence", "database administrator",
    "data scientist", "reporting analyst"
]
IT_TITLES = [
    "it support", "help desk", "systems administrator",
    "network administrator", "it analyst", "technical support",
    "desktop support", "it technician"
]

# Skills to look for in resume
SKILLS_KEYWORDS = [
    "sql", "python", "excel", "tableau", "power bi",
    "javascript", "networking", "windows", "linux",
    "cloud", "aws", "azure", "google analytics",
    "r", "sas", "jira", "servicenow", "active directory"
]

What does this code do?

LOCATION and RADIUS_KM tell the API where to search for local jobs. The radius of 25 km covers roughly a 15-mile commute.

MAX_QUERIES limits the script to your top 2 matched job titles per run. This conserves your 250 monthly free API credits.

DATA_TITLES, IT_TITLES, and SKILLS_KEYWORDS are the lookup tables the extractor will scan your resume against.

  • Save job_search.py.

You won't see output yet. The function you write next uses these lists to scan your resume.

Write the resume keyword extractor function

The extractor reads your resume file, converts it to lowercase, and checks which job titles and skills from your lists appear in the text. If nothing matches, it falls back to sensible defaults so the script still works.

  • In job_search.py, add this function below the SKILLS_KEYWORDS list (leave one blank line between them):
def extract_resume_keywords(resume_path):
    """Read resume and extract relevant skills and job titles."""
    with open(resume_path, "r", encoding="utf-8") as f:
        resume_text = f.read().lower()

    matched_titles = []
    for title in DATA_TITLES + IT_TITLES:
        if title in resume_text:
            matched_titles.append(title)

    matched_skills = []
    for skill in SKILLS_KEYWORDS:
        if skill in resume_text:
            matched_skills.append(skill)

    if not matched_titles:
        matched_titles = ["data analyst", "it support"]
        print("  No specific titles found in resume. Using defaults.")

    if not matched_skills:
        matched_skills = ["excel", "sql"]
        print("  No specific skills found in resume. Using defaults.")

    return matched_titles, matched_skills

What does this code do?

  • resume_text = f.read().lower() reads the entire file and converts it to lowercase so that matching is case-insensitive.
  • The first loop checks each job title from both DATA_TITLES and IT_TITLES. If a title appears anywhere in your resume text, it gets added to matched_titles.
  • The second loop does the same for skills.
  • The fallback defaults ensure the script always has something to search for, even if your resume uses different phrasing.
  • Save job_search.py.

You won't see output yet. You will test this function at the end of the step.

Write the search query builder

Matched job titles on their own are too broad for a job search. You need to turn them into targeted search queries that include "entry level" so the results focus on positions appropriate for your experience.

  • In job_search.py, add this function below extract_resume_keywords() (leave one blank line between them):
def build_search_queries(titles):
    """Build search queries from extracted job titles."""
    queries = []
    for title in titles[:MAX_QUERIES]:
        queries.append(f"entry level {title}")
    return queries

What does this code do?

titles[:MAX_QUERIES] takes only the first 2 titles from your matched list. Each query uses about 2 API credits (one for local, one for remote), so limiting queries keeps you within the free tier.

The f-string prepends "entry level" to each title, producing queries like entry level data analyst.

  • Save job_search.py.

You won't see output yet. The test block you add next runs both functions together.

Add a test block and verify the extractor works

Before connecting to the API, you should confirm that the keyword extractor finds titles and skills in your resume. A test block at the bottom of the file lets you run the script and see what it matched.

  • At the very bottom of job_search.py, add this test block:
# Test block
if __name__ == "__main__":
    titles, skills = extract_resume_keywords("resume.txt")
    print(f"Matched titles: {titles}")
    print(f"Matched skills: {skills}")
    queries = build_search_queries(titles)
    print(f"Search queries: {queries}")

What does this code do?

if __name__ == "__main__": ensures this block only runs when you execute the file directly. It will not run when you import functions from this file later.

The three print() statements show you exactly what the extractor found in your resume, so you can verify it picked up the right keywords before burning API credits.

  • Save job_search.py.
  • Run the script by entering this command in your terminal:
python job_search.py

You should see output showing your matched titles, skills, and search queries. For example:

Matched titles: ['data analyst', 'business analyst']
Matched skills: ['sql', 'python', 'excel', 'tableau']
Search queries: ['entry level data analyst', 'entry level business analyst']

What if I see different results?

Your output depends on your resume content. If you see default values like ['data analyst', 'it support'], that means none of the predefined titles appeared in your resume.txt file.

The script still works with defaults. You can also edit the DATA_TITLES or IT_TITLES lists to include titles from your own resume.

Seeing a FileNotFoundError?

  • Make sure resume.txt exists in the same folder as job_search.py. Both files should be inside your job-search-tool folder on your Desktop.
  • Check that your terminal is in the right directory. Run cd ~/Desktop/job-search-tool to make sure.
  • Verify your virtual environment is still activated. You should see (.venv) at the start of your terminal prompt.

Still stuck? Help me debug a FileNotFoundError when running my job search script.

✔️ Awesome, I've got everything!

Great. Double check you have saved job_search.py and confirmed the output looks right.

ⓧ I'd like to double check the full code

Here is the full job_search.py up to this point:

import os
import serpapi
import pandas as pd
from dotenv import load_dotenv

# Load API key from .env file
load_dotenv()
API_KEY = os.getenv("SERPAPI_KEY")

# Search configuration
LOCATION = "San Antonio, Texas, United States"
RADIUS_KM = "25"
MAX_QUERIES = 2

# Job titles to look for in resume
DATA_TITLES = [
    "data analyst", "data entry", "business analyst",
    "data engineer", "business intelligence", "database administrator",
    "data scientist", "reporting analyst"
]
IT_TITLES = [
    "it support", "help desk", "systems administrator",
    "network administrator", "it analyst", "technical support",
    "desktop support", "it technician"
]

# Skills to look for in resume
SKILLS_KEYWORDS = [
    "sql", "python", "excel", "tableau", "power bi",
    "javascript", "networking", "windows", "linux",
    "cloud", "aws", "azure", "google analytics",
    "r", "sas", "jira", "servicenow", "active directory"
]


def extract_resume_keywords(resume_path):
    """Read resume and extract relevant skills and job titles."""
    with open(resume_path, "r", encoding="utf-8") as f:
        resume_text = f.read().lower()

    matched_titles = []
    for title in DATA_TITLES + IT_TITLES:
        if title in resume_text:
            matched_titles.append(title)

    matched_skills = []
    for skill in SKILLS_KEYWORDS:
        if skill in resume_text:
            matched_skills.append(skill)

    if not matched_titles:
        matched_titles = ["data analyst", "it support"]
        print("  No specific titles found in resume. Using defaults.")

    if not matched_skills:
        matched_skills = ["excel", "sql"]
        print("  No specific skills found in resume. Using defaults.")

    return matched_titles, matched_skills


def build_search_queries(titles):
    """Build search queries from extracted job titles."""
    queries = []
    for title in titles[:MAX_QUERIES]:
        queries.append(f"entry level {title}")
    return queries


# Test block
if __name__ == "__main__":
    titles, skills = extract_resume_keywords("resume.txt")
    print(f"Matched titles: {titles}")
    print(f"Matched skills: {skills}")
    queries = build_search_queries(titles)
    print(f"Search queries: {queries}")

Your keyword extractor is working. Next up, you will use these queries to search for real job listings through the SerpAPI.

Write the Multi-Source Search Functions

Your keyword extractor is working and pulling relevant job titles from your resume. Now it's time to put those keywords to use by actually searching for jobs.

In this step, you'll write three search functions that each call SerpAPI from a different angle. One searches for local/hybrid positions near San Antonio, one searches for remote jobs, and one searches Google directly for company career pages that never appear on traditional job boards.

In this step, get ready to:
  • Write a local jobs search function that targets positions within 25 km of San Antonio.
  • Write a remote jobs search function that finds work-from-home positions.
  • Write a company careers search function that finds jobs posted directly on employer websites.
Write the local jobs search function

The first search function uses the Google Jobs engine to find hybrid and on-site positions within driving distance of San Antonio. It passes a location and radius so results stay geographically relevant.

  • In job_search.py, scroll to the bottom of the file and find the test block that starts with:
# Test block
if __name__ == "__main__":
    titles, skills = extract_resume_keywords("resume.txt")
    print(f"Matched titles: {titles}")
    print(f"Matched skills: {skills}")
    queries = build_search_queries(titles)
    print(f"Search queries: {queries}")
  • Select the entire test block and delete it.
  • In its place, add the following search_local_jobs() function:
def search_local_jobs(query):
    """Search Google Jobs for hybrid/local positions near San Antonio."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": query,
        "location": LOCATION,
        "lrad": RADIUS_KM,
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])

What does this code do?

This function creates a SerpAPI client using your API key, then sends a search request to the Google Jobs engine.

  • engine: "google_jobs" tells SerpAPI to search the Google Jobs aggregator (which pulls listings from Indeed, LinkedIn, Glassdoor, and other sources).
  • location and lrad restrict results to positions within 25 km of San Antonio. The radius is approximate, not a hard boundary.
  • results.get("jobs_results", []) safely returns an empty list if no jobs are found, preventing crashes.

You won't see output yet. The next function handles remote positions.

Write the remote jobs search function

Not every good opportunity is within driving distance. This function searches for remote positions by appending "remote" to the query string instead of filtering by location.

  • Below the search_local_jobs() function you just wrote, add this search_remote_jobs() function:
def search_remote_jobs(query):
    """Search Google Jobs for remote positions."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": f"{query} remote",
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])

What does this code do?

This function uses the same Google Jobs engine but takes a different approach to finding remote work.

  • Instead of using a deprecated ltype filter parameter, it appends the word "remote" directly to the query string. This reliably surfaces work-from-home listings.
  • No location or lrad parameter is needed here because remote jobs are location-independent.

You won't see output yet. One more search function covers company career pages.

Write the company careers search function

Many companies post jobs only on their own career pages and never syndicate them to job boards. This function uses regular Google Search (not the Jobs engine) to find those hidden listings by targeting pages that contain hiring-related keywords.

Why search company career pages separately?

Google Jobs aggregates listings from major job boards like Indeed and LinkedIn. But smaller companies, local businesses, and government agencies often post openings only on their own websites.

By searching Google directly for pages containing words like "careers" or "hiring" alongside your job title, you surface opportunities that the big job boards miss entirely.

  • Below the search_remote_jobs() function, add this search_company_careers() function:
def search_company_careers(query):
    """Search Google for company career pages with job postings."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google",
        "q": f'"careers" OR "hiring" OR "apply now" "{query}" San Antonio TX',
        "gl": "us",
        "hl": "en",
        "num": "10"
    })
    return results.get("organic_results", [])

What does this code do?

This function switches to the regular Google Search engine instead of Google Jobs.

  • engine: "google" searches the regular web, not the Google Jobs aggregator.
  • The query uses quoted phrases ("careers" OR "hiring" OR "apply now") to target actual career pages rather than generic search results.
  • num: "10" limits results to 10 per search, conserving your free API credits.
  • The results come back as organic_results (standard Google links) rather than jobs_results (structured job data). You'll handle this difference in the next step.
  • Save job_search.py.
  • Verify there are no syntax errors by running this command in your terminal:
python job_search.py

You should see no output and no errors. The script loads your API key and defines the functions, but nothing calls them yet.

Seeing an error?

  • If you see ModuleNotFoundError: No module named 'serpapi', your virtual environment is not activated. Run .venv\Scripts\activate in your terminal, then try again.
  • If you see a SyntaxError, check that you didn't leave any leftover lines from the old test block. The file should end with the search_company_careers() function.
  • If you see IndentationError, make sure each function body is indented with 4 spaces, not tabs.

help me fix an error when running job_search.py

✔️ Awesome, I've got everything!

Great. Double check you've saved job_search.py and confirm the script runs without errors before moving on.

ⓧ I'd like to double check the full code

Here is the complete job_search.py file at this point:

import os
import serpapi
import pandas as pd
from dotenv import load_dotenv

# Load API key from .env file
load_dotenv()
API_KEY = os.getenv("SERPAPI_KEY")

# Search configuration
LOCATION = "San Antonio, Texas, United States"
RADIUS_KM = "25"
MAX_QUERIES = 2

# Job titles to look for in resume
DATA_TITLES = [
    "data analyst", "data entry", "business analyst",
    "data engineer", "business intelligence", "database administrator",
    "data scientist", "reporting analyst"
]
IT_TITLES = [
    "it support", "help desk", "systems administrator",
    "network administrator", "it analyst", "technical support",
    "desktop support", "it technician"
]

# Skills to look for in resume
SKILLS_KEYWORDS = [
    "sql", "python", "excel", "tableau", "power bi",
    "javascript", "networking", "windows", "linux",
    "cloud", "aws", "azure", "google analytics",
    "r", "sas", "jira", "servicenow", "active directory"
]


def extract_resume_keywords(resume_path):
    """Read resume and extract relevant skills and job titles."""
    with open(resume_path, "r", encoding="utf-8") as f:
        resume_text = f.read().lower()

    matched_titles = []
    for title in DATA_TITLES + IT_TITLES:
        if title in resume_text:
            matched_titles.append(title)

    matched_skills = []
    for skill in SKILLS_KEYWORDS:
        if skill in resume_text:
            matched_skills.append(skill)

    if not matched_titles:
        matched_titles = ["data analyst", "it support"]
        print("  No specific titles found in resume. Using defaults.")

    if not matched_skills:
        matched_skills = ["excel", "sql"]
        print("  No specific skills found in resume. Using defaults.")

    return matched_titles, matched_skills


def build_search_queries(titles):
    """Build search queries from extracted job titles."""
    queries = []
    for title in titles[:MAX_QUERIES]:
        queries.append(f"entry level {title}")
    return queries


def search_local_jobs(query):
    """Search Google Jobs for hybrid/local positions near San Antonio."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": query,
        "location": LOCATION,
        "lrad": RADIUS_KM,
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])


def search_remote_jobs(query):
    """Search Google Jobs for remote positions."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": f"{query} remote",
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])


def search_company_careers(query):
    """Search Google for company career pages with job postings."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google",
        "q": f'"careers" OR "hiring" OR "apply now" "{query}" San Antonio TX',
        "gl": "us",
        "hl": "en",
        "num": "10"
    })
    return results.get("organic_results", [])

Your search functions are locked and loaded. Next up, you'll process the raw results from these functions and export everything to a clean Excel spreadsheet.

Combine Results and Export to Excel

Your search functions are ready to pull job listings from three different sources. Now you need to process those raw results into a consistent format and export them somewhere useful.

Raw API responses contain nested data structures with inconsistent fields across sources. In this step, you will write processing functions that extract the fields you care about, remove duplicates, and export everything to a clean Excel spreadsheet.

In this step, get ready to:
  • Process raw Google Jobs and Google Search results into a consistent row format.
  • Remove duplicate job listings based on title and company.
  • Export all results to a formatted Excel spreadsheet and run the full pipeline.
Process the raw search results

The Google Jobs API and Google Search API return results in different formats. You need a separate processing function for each source that extracts the same columns into a flat dictionary.

  • In job_search.py, scroll to the bottom of the file (below the search_company_careers() function).
  • Add the process_jobs_results() function by copying this code below the last function:
def process_jobs_results(raw_jobs, source_type):
    """Extract relevant fields from Google Jobs results."""
    processed = []
    for job in raw_jobs:
        extensions = job.get("detected_extensions", {})

        apply_link = "N/A"
        if job.get("apply_options"):
            apply_link = job["apply_options"][0].get("link", "N/A")
        elif job.get("share_link"):
            apply_link = job["share_link"]

        # Build benefits list from detected_extensions
        benefits = []
        if extensions.get("health_insurance"):
            benefits.append("Health")
        if extensions.get("dental_coverage"):
            benefits.append("Dental")
        if extensions.get("paid_time_off"):
            benefits.append("PTO")

        processed.append({
            "Job Title": job.get("title", "N/A"),
            "Company": job.get("company_name", "N/A"),
            "Location": job.get("location", "N/A"),
            "Salary": extensions.get("salary", "Not listed"),
            "Benefits": ", ".join(benefits) if benefits else "Not listed",
            "No Degree Required": "Yes" if extensions.get("no_degree_mentioned") else "",
            "Type": "Remote" if extensions.get("work_from_home") else "Hybrid/On-site",
            "Schedule": extensions.get("schedule_type", "N/A"),
            "Date Posted": extensions.get("posted_at", "N/A"),
            "Posted Via": job.get("via", "N/A"),
            "Source": source_type,
            "Apply Link": apply_link
        })
    return processed

What does this code do?

This function loops through every job from a Google Jobs API response and extracts the fields you care about into a flat dictionary.

  • detected_extensions is where the API stores structured data like salary, schedule, and benefits (health insurance, dental, PTO).
  • The function checks apply_options first for a direct apply link, then falls back to share_link (Google's job detail page) if no direct link exists.
  • no_degree_mentioned and work_from_home are boolean fields the API sets to true when detected in the listing.

Google Search organic results have a different structure than Google Jobs results. You need a separate function to handle them.

  • Add the process_career_results() function directly below process_jobs_results():
def process_career_results(organic_results):
    """Extract relevant fields from Google Search career page results."""
    processed = []
    for result in organic_results:
        processed.append({
            "Job Title": result.get("title", "N/A"),
            "Company": result.get("source", "N/A"),
            "Location": "San Antonio, TX (career page)",
            "Salary": "Check listing",
            "Benefits": "Check listing",
            "No Degree Required": "",
            "Type": "Check listing",
            "Schedule": "N/A",
            "Date Posted": result.get("date", "N/A"),
            "Posted Via": "Company Website",
            "Source": "Company Career Page",
            "Apply Link": result.get("link", "N/A")
        })
    return processed

Why are career page fields set to "Check listing"?

Google Search organic results are links to career pages, not structured job data. The API gives you a page title, snippet, and URL, but no salary or benefits breakdown. You set those fields to "Check listing" so the learner knows to click the apply link for full details.

  • Save job_search.py.

Seeing a syntax error?

  • Check that your curly braces and square brackets inside the processed.append({...}) call are balanced. Each dictionary key-value pair ends with a comma except the last one.
  • Make sure both functions are at the same indentation level as your existing functions (no extra spaces before def).

Help me fix a syntax error in my process_jobs_results or process_career_results function.

Remove duplicate listings

When you search for the same job title across multiple sources, you often get the same listing twice. A simple deduplication step prevents your spreadsheet from being cluttered with repeats.

  • Add the deduplicate_jobs() function below process_career_results():
def deduplicate_jobs(all_jobs):
    """Remove duplicate listings based on title + company."""
    seen = set()
    unique = []
    for job in all_jobs:
        key = f"{job['Job Title'].lower().strip()}|{job['Company'].lower().strip()}"
        if key not in seen:
            seen.add(key)
            unique.append(job)
    return unique

What does this code do?

The function creates a unique key for each job by combining the lowercase, trimmed title and company name with a pipe separator. If a job with the same key already appeared, it gets skipped. This catches cases where the same position shows up in both local and remote results.

  • Save job_search.py.
Export to Excel and run the full pipeline

The final piece is a function that takes the processed job list and writes it to an Excel file. Then you wire everything together in a main() function that runs the complete pipeline from resume reading to Excel export.

  • Add the export_to_excel() function below deduplicate_jobs():
def export_to_excel(jobs):
    """Export job listings to an Excel spreadsheet."""
    df = pd.DataFrame(jobs)

    with pd.ExcelWriter("job_search_results.xlsx", engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="Jobs")

        worksheet = writer.sheets["Jobs"]
        for column in worksheet.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                if cell.value:
                    max_length = max(max_length, len(str(cell.value)))
            adjusted_width = min(max_length + 2, 50)
            worksheet.column_dimensions[column_letter].width = adjusted_width

    print(f"Exported {len(df)} jobs to job_search_results.xlsx")

What does this code do?

  • pd.DataFrame(jobs) converts the list of dictionaries into a table. Each dictionary key becomes a column header.
  • pd.ExcelWriter with the openpyxl engine writes a real .xlsx file that opens in Excel or Google Sheets.
  • The column-width loop measures the longest value in each column and sets the width accordingly, capped at 50 characters to keep things readable.

Now add the main() function that orchestrates the entire pipeline.

  • Add the main() function and the entry point below export_to_excel():
def main():
    print("=" * 50)
    print("  Multi-Source Job Search Tool")
    print("=" * 50)

    # Read resume and extract keywords
    print("\nReading resume and extracting keywords...")
    titles, skills = extract_resume_keywords("resume.txt")
    print(f"  Matched titles: {titles}")
    print(f"  Matched skills: {skills}")

    # Build search queries
    queries = build_search_queries(titles)
    print(f"  Search queries: {queries}")

    # Search for jobs
    all_jobs = []

    for query in queries:
        print(f"\nSearching local jobs: '{query}'...")
        local_results = search_local_jobs(query)
        local_processed = process_jobs_results(local_results, "Google Jobs (Local)")
        all_jobs.extend(local_processed)
        print(f"  Found {len(local_processed)} local listings")

        print(f"Searching remote jobs: '{query} remote'...")
        remote_results = search_remote_jobs(query)
        remote_processed = process_jobs_results(remote_results, "Google Jobs (Remote)")
        all_jobs.extend(remote_processed)
        print(f"  Found {len(remote_processed)} remote listings")

    # Search company career pages with first query
    if queries:
        print(f"\nSearching company career pages...")
        career_results = search_company_careers(titles[0])
        career_processed = process_career_results(career_results)
        all_jobs.extend(career_processed)
        print(f"  Found {len(career_processed)} career page results")

    # Deduplicate and export
    print(f"\nTotal results before deduplication: {len(all_jobs)}")
    unique_jobs = deduplicate_jobs(all_jobs)
    print(f"Unique results after deduplication: {len(unique_jobs)}")

    if unique_jobs:
        export_to_excel(unique_jobs)
        print("\nDone! Open job_search_results.xlsx to see your results.")
    else:
        print("\nNo jobs found. Check your API key and resume keywords.")

    # Print credit usage estimate
    credits_used = (len(queries) * 2) + 1
    print(f"\nAPI credits used this run: ~{credits_used} of 250 monthly")


if __name__ == "__main__":
    main()

What does this code do?

  • main() runs the full pipeline in order: read resume, build queries, search all three sources, deduplicate, and export.
  • The loop runs each query against both local and remote Google Jobs searches. Career page search runs once using the first matched title.
  • if __name__ == "__main__" ensures main() only runs when you execute the script directly (not when importing it as a module).
  • The credit estimate at the end shows approximately how many of your 250 monthly free searches were used. With 2 queries, each run uses about 5 credits.
  • Save job_search.py.
  • Run the complete script by entering this command in your terminal:
python job_search.py

You should see output showing each search stage, the number of results found, and a final message confirming the Excel export.

Seeing an authentication error?

  • A 401 error means your API key is invalid. Open .env and confirm that SERPAPI_KEY is set to the key you copied from your SerpAPI dashboard (no quotes around the value).
  • A 429 error means you hit the rate limit. The free tier allows 50 searches per hour. Wait a few minutes and try again.
  • If you see "No jobs found", make sure resume.txt contains your actual resume content with recognizable job titles and skills.

Help me debug my job search script. I'm getting an error when I run it.

  • Open the job_search_results.xlsx file in your job-search-tool folder. You should see columns for Job Title, Company, Location, Salary, Benefits, and more.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete job_search.py file at this point:

import os
import serpapi
import pandas as pd
from dotenv import load_dotenv

# Load API key from .env file
load_dotenv()
API_KEY = os.getenv("SERPAPI_KEY")

# Search configuration
LOCATION = "San Antonio, Texas, United States"
RADIUS_KM = "25"
MAX_QUERIES = 2

# Job titles to look for in resume
DATA_TITLES = [
    "data analyst", "data entry", "business analyst",
    "data engineer", "business intelligence", "database administrator",
    "data scientist", "reporting analyst"
]
IT_TITLES = [
    "it support", "help desk", "systems administrator",
    "network administrator", "it analyst", "technical support",
    "desktop support", "it technician"
]

# Skills to look for in resume
SKILLS_KEYWORDS = [
    "sql", "python", "excel", "tableau", "power bi",
    "javascript", "networking", "windows", "linux",
    "cloud", "aws", "azure", "google analytics",
    "r", "sas", "jira", "servicenow", "active directory"
]


def extract_resume_keywords(resume_path):
    """Read resume and extract relevant skills and job titles."""
    with open(resume_path, "r", encoding="utf-8") as f:
        resume_text = f.read().lower()

    matched_titles = []
    for title in DATA_TITLES + IT_TITLES:
        if title in resume_text:
            matched_titles.append(title)

    matched_skills = []
    for skill in SKILLS_KEYWORDS:
        if skill in resume_text:
            matched_skills.append(skill)

    if not matched_titles:
        matched_titles = ["data analyst", "it support"]
        print("  No specific titles found in resume. Using defaults.")

    if not matched_skills:
        matched_skills = ["excel", "sql"]
        print("  No specific skills found in resume. Using defaults.")

    return matched_titles, matched_skills


def build_search_queries(titles):
    """Build search queries from extracted job titles."""
    queries = []
    for title in titles[:MAX_QUERIES]:
        queries.append(f"entry level {title}")
    return queries


def search_local_jobs(query):
    """Search Google Jobs for hybrid/local positions near San Antonio."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": query,
        "location": LOCATION,
        "lrad": RADIUS_KM,
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])


def search_remote_jobs(query):
    """Search Google Jobs for remote positions."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google_jobs",
        "q": f"{query} remote",
        "gl": "us",
        "hl": "en"
    })
    return results.get("jobs_results", [])


def search_company_careers(query):
    """Search Google for company career pages with job postings."""
    client = serpapi.Client(api_key=API_KEY)
    results = client.search({
        "engine": "google",
        "q": f'"careers" OR "hiring" OR "apply now" "{query}" San Antonio TX',
        "gl": "us",
        "hl": "en",
        "num": "10"
    })
    return results.get("organic_results", [])


def process_jobs_results(raw_jobs, source_type):
    """Extract relevant fields from Google Jobs results."""
    processed = []
    for job in raw_jobs:
        extensions = job.get("detected_extensions", {})

        apply_link = "N/A"
        if job.get("apply_options"):
            apply_link = job["apply_options"][0].get("link", "N/A")
        elif job.get("share_link"):
            apply_link = job["share_link"]

        benefits = []
        if extensions.get("health_insurance"):
            benefits.append("Health")
        if extensions.get("dental_coverage"):
            benefits.append("Dental")
        if extensions.get("paid_time_off"):
            benefits.append("PTO")

        processed.append({
            "Job Title": job.get("title", "N/A"),
            "Company": job.get("company_name", "N/A"),
            "Location": job.get("location", "N/A"),
            "Salary": extensions.get("salary", "Not listed"),
            "Benefits": ", ".join(benefits) if benefits else "Not listed",
            "No Degree Required": "Yes" if extensions.get("no_degree_mentioned") else "",
            "Type": "Remote" if extensions.get("work_from_home") else "Hybrid/On-site",
            "Schedule": extensions.get("schedule_type", "N/A"),
            "Date Posted": extensions.get("posted_at", "N/A"),
            "Posted Via": job.get("via", "N/A"),
            "Source": source_type,
            "Apply Link": apply_link
        })
    return processed


def process_career_results(organic_results):
    """Extract relevant fields from Google Search career page results."""
    processed = []
    for result in organic_results:
        processed.append({
            "Job Title": result.get("title", "N/A"),
            "Company": result.get("source", "N/A"),
            "Location": "San Antonio, TX (career page)",
            "Salary": "Check listing",
            "Benefits": "Check listing",
            "No Degree Required": "",
            "Type": "Check listing",
            "Schedule": "N/A",
            "Date Posted": result.get("date", "N/A"),
            "Posted Via": "Company Website",
            "Source": "Company Career Page",
            "Apply Link": result.get("link", "N/A")
        })
    return processed


def deduplicate_jobs(all_jobs):
    """Remove duplicate listings based on title + company."""
    seen = set()
    unique = []
    for job in all_jobs:
        key = f"{job['Job Title'].lower().strip()}|{job['Company'].lower().strip()}"
        if key not in seen:
            seen.add(key)
            unique.append(job)
    return unique


def export_to_excel(jobs):
    """Export job listings to an Excel spreadsheet."""
    df = pd.DataFrame(jobs)

    with pd.ExcelWriter("job_search_results.xlsx", engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="Jobs")

        worksheet = writer.sheets["Jobs"]
        for column in worksheet.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                if cell.value:
                    max_length = max(max_length, len(str(cell.value)))
            adjusted_width = min(max_length + 2, 50)
            worksheet.column_dimensions[column_letter].width = adjusted_width

    print(f"Exported {len(df)} jobs to job_search_results.xlsx")


def main():
    print("=" * 50)
    print("  Multi-Source Job Search Tool")
    print("=" * 50)

    print("\nReading resume and extracting keywords...")
    titles, skills = extract_resume_keywords("resume.txt")
    print(f"  Matched titles: {titles}")
    print(f"  Matched skills: {skills}")

    queries = build_search_queries(titles)
    print(f"  Search queries: {queries}")

    all_jobs = []

    for query in queries:
        print(f"\nSearching local jobs: '{query}'...")
        local_results = search_local_jobs(query)
        local_processed = process_jobs_results(local_results, "Google Jobs (Local)")
        all_jobs.extend(local_processed)
        print(f"  Found {len(local_processed)} local listings")

        print(f"Searching remote jobs: '{query} remote'...")
        remote_results = search_remote_jobs(query)
        remote_processed = process_jobs_results(remote_results, "Google Jobs (Remote)")
        all_jobs.extend(remote_processed)
        print(f"  Found {len(remote_processed)} remote listings")

    if queries:
        print(f"\nSearching company career pages...")
        career_results = search_company_careers(titles[0])
        career_processed = process_career_results(career_results)
        all_jobs.extend(career_processed)
        print(f"  Found {len(career_processed)} career page results")

    print(f"\nTotal results before deduplication: {len(all_jobs)}")
    unique_jobs = deduplicate_jobs(all_jobs)
    print(f"Unique results after deduplication: {len(unique_jobs)}")

    if unique_jobs:
        export_to_excel(unique_jobs)
        print("\nDone! Open job_search_results.xlsx to see your results.")
    else:
        print("\nNo jobs found. Check your API key and resume keywords.")

    credits_used = (len(queries) * 2) + 1
    print(f"\nAPI credits used this run: ~{credits_used} of 250 monthly")


if __name__ == "__main__":
    main()

Secret mission

Add Resume Match Scoring

Your spreadsheet finds jobs, but which ones are actually the best fit for YOUR skills? Add a scoring system that rates every job by how closely it matches your resume, so the best opportunities rise to the top automatically.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine with a free-tier API. There are no ongoing costs to worry about.

Decide whether to keep your project files for future job searches, pause your environment, or delete everything entirely.

Resources you used:

  • Local project folder job-search-tool on your Desktop (job_search.py, resume.txt, .env, job_search_results.xlsx, .venv folder).
  • SerpAPI account (free plan, no credit card required, 250 searches/month renews automatically).

Keep everything running

No action needed. Choose this if you plan to re-run your job search weekly to get fresh listings.

  • Your script and files sit on your local machine at no cost.
  • Re-run python job_search.py anytime to get updated results.
  • Your SerpAPI free tier (250 searches/month) renews automatically each month.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files stay untouched until you return.

  • Deactivate your virtual environment by running this command in the VS Code terminal:
deactivate

Your project files remain on your Desktop. When you come back, reactivate with .venv\Scripts\activate and you are ready to search again.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Deactivate your virtual environment first by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rmdir /s /q job-search-tool
  • Confirm the folder is gone by running:
ls

You should no longer see job-search-tool in the output.

  • To delete your SerpAPI account, sign in at serpapi.com and contact support at support@serpapi.com to request account deletion.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully automated job search tool that reads your resume, searches multiple sources, and delivers a clean spreadsheet of matching positions.

You've learned how to:

  • Built a Python script that reads your resume and automatically extracts relevant job titles and skills as search keywords.
  • Connected to SerpAPI to search Google Jobs for local/hybrid and remote positions, plus Google Search to find jobs posted directly on company career pages that never appear on job boards.
  • Combined, deduplicated, and exported all results to a clean Excel spreadsheet with salary, benefits, and degree requirements ready for your job hunt.
  • Secret Mission: Added a resume match scoring system that ranks every job by skill overlap so the best-fit positions rise to the top.

Ready to quiz yourself?