保哥笔记

手把手教你用Python+SerpApi搭建实时Google关键词排名查询工具

做 SEO 的朋友都知道,掌握自己网站在 Google 搜索中的关键词排名是日常工作的核心。市面上的排名追踪工具(如 Ahrefs、SEMrush)动辄上百美元/月,并且批量实时排名查询很难用,而实际上,我们完全可以借助 SerpApi 提供的搜索结果 API,用 Python 自己动手搭建一个轻量、灵活且免费(有额度)的排名查询工具。

本文将从零开始,手把手带你完成一个支持批量关键词查询实时排名展示CSV 导出的完整工具,并提供命令行和 Web 可视化两种使用方式。


1. 背景与工具选型

为什么要自己造轮子?

商业化的 SEO 排名追踪工具功能强大,但存在几个痛点:

而 SerpApi 提供了一种"原子级"的方案——它直接返回 Google 搜索的结构化结果(JSON 格式),我们拿到数据后可以自由加工处理。

为什么选 SerpApi?


2. SerpApi 账号准备

2.1 注册并获取 API Key

  1. 访问 serpapi.com 并注册账号。
  2. 注册成功后进入 Dashboard,在页面中可以看到你的 API Key,复制备用。
  3. 免费套餐每月提供 100 次搜索,付费套餐起步 $50/月,提供 5000 次搜索。

2.2 API 基本用法

SerpApi 的调用非常简单,只需向 https://serpapi.com/search.json 发送一个 GET 请求,带上参数即可:

GET https://serpapi.com/search.json?api_key=YOUR_KEY&engine=google&q=best+crm+software&gl=us&hl=en&num=100

核心参数说明:

参数说明示例值
api_key你的 API Keyabc123...
engine搜索引擎google
q搜索关键词best crm software
gl地理位置(国家代码)us(美国)
hl界面语言en(英语)
num返回结果数量100(最多100)
device设备类型desktop / mobile

2.3 返回数据结构

API 返回的 JSON 中,我们最关心的是 organic_results 字段,它是一个数组,每个元素代表一条自然搜索结果:

{
  "organic_results": [
    {
      "position": 1,
      "title": "Best CRM Software 2025 - Forbes Advisor",
      "link": "https://www.forbes.com/advisor/business/best-crm-software/",
      "displayed_link": "https://www.forbes.com › advisor › business",
      "snippet": "Compare the best CRM software of 2025..."
    },
    {
      "position": 2,
      "title": "...",
      "link": "...",
      ...
    }
  ]
}

我们的核心逻辑就是:遍历 organic_results,检查每条结果的 link 字段是否包含我们的目标域名,如果匹配,则该条目的 position 就是我们的排名。


3. 核心原理:如何通过 API 获取排名

整个查询流程可以用三步概括:

1. 发送请求 → SerpApi(关键词 + 地区 + 语言)
2. 解析返回 → 遍历 organic_results
3. 域名匹配 → 找到目标域名的 position

域名匹配的关键逻辑

域名匹配看似简单,但有几个细节需要处理:

from urllib.parse import urlparse

def normalize_domain(domain: str) -> str:
    """标准化域名:去掉协议前缀、www 和尾部斜杠"""
    domain = domain.lower().strip()
    if domain.startswith(("http://", "https://")):
        domain = urlparse(domain).hostname or domain
    domain = domain.removeprefix("www.")
    return domain.rstrip("/")

def extract_domain(url: str) -> str:
    """从完整 URL 中提取裸域名"""
    try:
        return urlparse(url).hostname.removeprefix("www.").lower()
    except Exception:
        return ""

为什么需要标准化? 因为用户输入的域名可能是 example.comwww.example.comhttps://example.com/ 等各种形式,而 Google 返回的链接也可能带或不带 www。统一标准化后才能准确匹配。

子域名匹配:我们还支持子域名匹配——如果目标是 example.com,那么 blog.example.comdocs.example.com 也会被识别为匹配结果。实现方式是检查 item_domain.endswith("." + target)


4. 第一部分:命令行版本

命令行版本适合批量查询、定时任务和脚本集成。

4.1 环境准备

pip install requests rich

4.2 核心代码实现

单个关键词查询函数

import requests

SERPAPI_URL = "https://serpapi.com/search.json"

def query_keyword(api_key: str, keyword: str, num: int = 100) -> dict:
    """查询单个关键词,返回 SerpApi 完整响应"""
    params = {
        "api_key": api_key,
        "engine": "google",
        "q": keyword,
        "gl": "us",         # 固定美国地区
        "hl": "en",          # 英文界面
        "num": num,           # 返回结果数(查询深度)
        "device": "desktop",
    }
    resp = requests.get(SERPAPI_URL, params=params, timeout=30)
    resp.raise_for_status()
    return resp.json()

在结果中查找目标域名

def find_domain_in_results(data: dict, target_domain: str) -> dict | None:
    """遍历自然搜索结果,查找目标域名的排名"""
    target = normalize_domain(target_domain)
    organic = data.get("organic_results", [])

    for item in organic:
        link = item.get("link", "")
        item_domain = extract_domain(link)
        # 精确匹配 + 子域名匹配
        if item_domain == target or item_domain.endswith("." + target):
            return {
                "position": item.get("position"),
                "title": item.get("title", ""),
                "link": link,
                "snippet": item.get("snippet", ""),
            }
    return None  # 在前 N 条结果中未找到

批量查询主循环

import time

def run_check(api_key, domain, keywords, num=100, delay=1.0):
    """逐个查询关键词,收集排名结果"""
    results = []
    for i, kw in enumerate(keywords):
        record = {"keyword": kw, "status": "error", "position": None}

        try:
            data = query_keyword(api_key, kw, num)
            match = find_domain_in_results(data, domain)

            if match:
                record.update({"status": "found", **match})
            else:
                record["status"] = "notfound"

        except requests.exceptions.HTTPError as e:
            record["error"] = str(e)
        except Exception as e:
            record["error"] = str(e)

        results.append(record)

        # 请求间隔,避免触发 API 限流
        if i < len(keywords) - 1:
            time.sleep(delay)

    return results

注意 time.sleep(delay):每次请求之间加入 1 秒延迟。虽然 SerpApi 本身允许较高频率的请求,但加入间隔是一个好习惯,既保护 API 配额,也降低被限流的风险。

4.3 美化终端输出

使用 rich 库渲染彩色表格和实时进度条:

from rich.console import Console
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich import box

console = Console()

def display_results(results, domain):
    """渲染结果表格"""
    found = [r for r in results if r["status"] == "found"]
    top10 = [r for r in found if r["position"] <= 10]

    table = Table(box=box.ROUNDED, header_style="bold cyan")
    table.add_column("#", width=4, justify="right")
    table.add_column("关键词", min_width=20)
    table.add_column("排名", width=6, justify="center")
    table.add_column("状态", width=8, justify="center")

    for i, r in enumerate(results, 1):
        if r["status"] == "found":
            pos = r["position"]
            style = "bold green" if pos <= 3 else "cyan" if pos <= 10 else "yellow"
            table.add_row(str(i), r["keyword"], f"[{style}]{pos}[/]", "[green]✓[/]")
        elif r["status"] == "notfound":
            table.add_row(str(i), r["keyword"], "—", "[yellow]✗[/]")
        else:
            table.add_row(str(i), r["keyword"], "!", "[red]ERR[/]")

    console.print(table)

4.4 导出功能

支持 CSV 和 JSON 两种导出格式:

import csv
from datetime import datetime

def export_csv(results, domain, filepath=None):
    """导出为 CSV 文件,支持 Excel 直接打开(UTF-8 BOM)"""
    if not filepath:
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        filepath = f"rank_{normalize_domain(domain)}_{ts}.csv"

    with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["关键词", "排名", "状态", "标题", "链接", "摘要"])
        for r in results:
            writer.writerow([
                r["keyword"],
                r.get("position", "—"),
                {"found": "已找到", "notfound": "未收录"}.get(r["status"], "出错"),
                r.get("title", ""),
                r.get("link", ""),
                r.get("snippet", ""),
            ])
    return filepath

这里使用 utf-8-sig 编码写入 BOM(字节顺序标记),这样用 Excel 打开 CSV 文件时中文不会乱码。

4.5 使用方式

# 从文件读取关键词(每行一个)
python rank_checker.py \
  --api-key YOUR_SERPAPI_KEY \
  --domain example.com \
  --keywords keywords.txt

# 直接指定关键词
python rank_checker.py \
  --api-key YOUR_SERPAPI_KEY \
  --domain example.com \
  -k "best crm software" \
  -k "free crm tools" \
  -k "crm for small business"

# 指定查询深度和导出格式
python rank_checker.py \
  --api-key YOUR_SERPAPI_KEY \
  --domain example.com \
  --keywords keywords.txt \
  --num 50 \
  --export csv

终端输出效果示例:

╭──────── 查询配置 ────────╮
│ 域名: example.com        │
│ 地区: Google US           │
│ 深度: 前 100 条结果       │
│ 关键词: 5 个              │
╰──────────────────────────╯

查询中 ━━━━━━━━━━━━━━━━━ 5/5 · best crm software

╭──────── 查询统计 ────────╮
│ 已收录: 3/5  |  Top 10: 2 │
╰──────────────────────────╯

╭────┬─────────────────────┬──────┬──────╮
│  # │ 关键词              │ 排名 │ 状态 │
├────┼─────────────────────┼──────┼──────┤
│  1 │ best crm software   │   7  │  ✓   │
│  2 │ free crm tools      │   3  │  ✓   │
│  3 │ crm for small biz   │  23  │  ✓   │
│  4 │ crm pricing         │   —  │  ✗   │
│  5 │ top crm 2025        │  12  │  ✓   │
╰────┴─────────────────────┴──────┴──────╯

5. 第二部分:Web 可视化版本

命令行虽好,但视觉体验有限。我们再来做一个 Web 版本,提供更直观的操作界面。

5.1 为什么不能直接在浏览器中调 SerpApi?

这是一个很常见的坑:SerpApi 的接口不支持浏览器直接调用。原因是浏览器的同源策略(CORS)——从你的网页域名(如 localhost:3000)向 serpapi.com 发 AJAX 请求时,浏览器会检查响应头中是否包含 Access-Control-Allow-Origin,而 SerpApi 并未设置这个头,所以请求会被浏览器拦截。

解决方案:在本地搭建一个代理服务器。前端请求发到本地服务器,由服务器转发给 SerpApi,再将结果返回给前端。Python 的服务端请求不受 CORS 限制。

浏览器 → localhost:8765/api/search → Python 服务器 → serpapi.com → 返回 JSON

5.2 架构设计

整个 Web 版本只有一个 Python 文件,它同时充当:

  1. 静态文件服务器:返回 HTML/CSS/JS 页面(内嵌在 Python 代码中)。
  2. API 代理:将前端的查询请求转发到 SerpApi。
import http.server
import urllib.request
import urllib.parse

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/api/search"):
            self.handle_search()   # 代理转发
        else:
            self.serve_html()      # 返回页面

    def handle_search(self):
        """将请求代理到 SerpApi"""
        query_string = self.path.split("?", 1)[1]
        params = urllib.parse.parse_qs(query_string)

        serpapi_params = {
            "api_key": params["api_key"][0],
            "engine": "google",
            "q": params["q"][0],
            "gl": "us",
            "hl": "en",
            "num": params.get("num", ["100"])[0],
            "device": "desktop",
        }

        url = f"https://serpapi.com/search.json?{urllib.parse.urlencode(serpapi_params)}"
        req = urllib.request.Request(url)

        with urllib.request.urlopen(req, timeout=30) as resp:
            data = resp.read()

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(data)

5.3 前端核心逻辑

前端使用原生 JavaScript(零依赖),通过 fetch API 逐个查询关键词,每完成一个就立即刷新界面:

async function startQuery() {
  const kwList = keywords.split('\n').filter(k => k.trim());

  for (let i = 0; i < kwList.length; i++) {
    results[i].status = 'loading';
    renderResults();  // 立即刷新:显示"查询中"

    const params = new URLSearchParams({
      api_key: apiKey,
      q: kwList[i],
      num: String(num)
    });

    const resp = await fetch('/api/search?' + params);
    const data = await resp.json();
    const match = findDomain(data, domain);

    results[i] = match
      ? { ...results[i], status: 'found', ...match }
      : { ...results[i], status: 'notfound' };

    renderResults();  // 再次刷新:显示结果
    await sleep(800); // 间隔 800ms
  }
}

5.4 前端展示模块

页面顶部有 5 个统计卡片:

排名数字使用颜色编码来直观区分:

5.5 启动和使用

python rank_checker_web.py

终端会显示:

╔══════════════════════════════════════════════╗
║   Google 排名追踪器 - Web 版                ║
║   SerpApi · US Region · Local Server         ║
╠══════════════════════════════════════════════╣
║   🌐  http://localhost:8765                  ║
║   按 Ctrl+C 停止服务器                       ║
╚══════════════════════════════════════════════╝

在浏览器中打开 http://localhost:8765,即可看到完整的可视化界面。


6. 关键技术细节解析

6.1 查询深度(num 参数)

num 参数控制 Google 返回的结果数量。设为 100 意味着查看前 100 条结果中是否有你的域名。

建议默认用 100,反正都是一次 API 调用的费用。

6.2 地区与语言

本工具固定查询 Google 美国地区(gl=us, hl=en),如果需要查询其他地区,只需修改参数:

地区glhl
美国usen
英国uken
中国(Google)cnzh-cn
日本jpja
德国dede

6.3 请求间隔与限流

代码中在每次请求之间加入了间隔(命令行版 1 秒,Web 版 0.8 秒)。这么做有两个原因:

  1. 保护 API 配额:避免短时间内大量请求被 SerpApi 判定为滥用。
  2. 稳定性:网络波动时给足缓冲时间。

如果你使用付费套餐且需要更快速度,可以将 delay 调低到 0.3-0.5 秒。

6.4 错误处理

工具对以下异常做了处理:

6.5 CSV 导出编码

导出 CSV 时使用 utf-8-sig 编码(带 BOM 的 UTF-8)。这是因为 Excel 默认用系统编码打开 CSV 文件,不带 BOM 的 UTF-8 文件中的中文会显示乱码。加上 BOM 后 Excel 会自动识别为 UTF-8 编码。


7. 进阶优化思路

如果你想在此基础上进一步扩展,以下是一些实用的优化方向:

7.1 定时自动查询

使用 cron(Linux/macOS)或 Task Scheduler(Windows)定时执行命令行版本,自动追踪排名变化:

# 每天早上 8 点执行查询并导出
0 8 * * * cd /path/to/tool && python rank_checker.py \
  --api-key YOUR_KEY --domain example.com \
  --keywords keywords.txt --export csv

7.2 历史趋势记录

将每次查询结果追加到数据库(如 SQLite)或追加写入 CSV,然后用 matplotlib 或 Plotly 绘制排名趋势图:

import sqlite3

def save_to_db(results, domain):
    conn = sqlite3.connect("rankings.db")
    c = conn.cursor()
    c.execute("""CREATE TABLE IF NOT EXISTS rankings (
        date TEXT, domain TEXT, keyword TEXT,
        position INTEGER, status TEXT
    )""")
    for r in results:
        c.execute("INSERT INTO rankings VALUES (?, ?, ?, ?, ?)",
            (datetime.now().isoformat(), domain,
             r["keyword"], r.get("position"), r["status"]))
    conn.commit()
    conn.close()

7.3 多地区对比

同一个关键词在不同地区的排名可能差异巨大。可以扩展为同时查询多个地区(us、uk、de 等),对比排名差异。

7.4 移动端排名

Google 的移动端和桌面端搜索结果排名不同。将 device 参数从 desktop 改为 mobile,即可查询移动端排名。可以同时查询两者进行对比。

7.5 竞品监控

不仅查自己的域名,还可以同时查竞争对手的域名。修改代码使其接受多个域名参数,一次查询就能看到你和竞品的排名对比。


8. 完整源码

本项目包含以下文件:

文件说明
rank_checker.py命令行版本,支持批量查询、结果表格展示、CSV/JSON 导出
rank_checker_web.pyWeb 可视化版本,内置本地服务器和前端页面
keywords_example.txt示例关键词文件

快速开始

# 安装依赖(Web 版无需额外依赖)
pip install requests rich

# 命令行版
python rank_checker.py --api-key YOUR_KEY --domain example.com --keywords keywords.txt --export csv

# Web 版
python rank_checker_web.py
# 浏览器打开 http://localhost:8765

完整代码已在文末附上。你也可以基于这些代码,根据自己的需求做二次开发。


9. 总结

通过这篇教程,我们从零搭建了一个完整的 Google 关键词排名查询工具。回顾一下核心知识点:

这个工具虽然简单,但它的核心逻辑和商业 SEO 工具的排名查询模块是一致的。掌握了这些基础,你可以根据自己的需求不断扩展——无论是多地区对比、竞品监控还是自动化报告,都是在这个框架上的自然延伸。

希望这篇教程对你有帮助,祝你的网站排名节节攀升!