开发技巧

做一个关键词排名跟踪工具:每天用 Bright Data SERP API 查询 Google 搜索结果

做一个关键词排名跟踪工具:每天用 Bright Data SERP API 查询 Google 搜索结果,取回结构化 JSON,记录你的网站在每个关键词下排第几、第一名是谁,追加到 CSV,用 Excel 就能画出排名变化曲线。

BINBHO · 图文教程

做一个关键词排名跟踪工具:每天用 Bright Data SERP API 查询 Google 搜索结果,取回结构化 JSON,记录你的网站在每个关键词下排第几、第一名是谁,追加到 CSV,用 Excel 就能画出排名变化曲线。

先看清成品

成品是 rank.py 加一个 keywords.json 配置。运行后终端显示每个关键词的排名,ranks.csv 每天追加一行。本文代码已用一份按官方 Light JSON 格式编写的样例响应实际测试,4 个测试全部通过:查询参数顺序正确、子域名计入但相似域名不计入、正确找出排名、CSV 表头正确。本文环境没有 Bright Data 账号,真实的 SERP API 请求没有发送。

注意:采集前先读目标网站的服务条款和 robots.txt,只采集公开、非个人的数据,控制频率,不绕过登录和付费墙,不采集个人信息。本文示例使用专门供练习的网站 toscrape.com。用于商业用途前请咨询法律意见,并遵守所在地关于数据和个人信息保护的法律。

做完之后你会得到什么?

成果 说明
关键词配置 域名、地区、语言、关键词列表
SERP 查询 brd_json=parsed_light 返回结构化结果
排名判断 主域名和子域名都算
历史记录 ranks.csv,带 BOM,Excel 直接打开

需要准备哪些环境?

项目 要求 说明
Bright Data 账号 新账号每月有 5,000 免费额度 以官方免费额度页面为准,额度用完会停止,不会自动扣费
Python 3.10 或更高 建议使用虚拟环境
依赖 requests、pytest

第一步:怎样准备 SERP API?

1. 创建区域

控制台添加一个 SERP API 区域,记下区域名(如 serp_api1)和 API 密钥。

2. 先用 curl 测试

curl -X POST https://api.brightdata.com/request \
  -H "Content-Type: application/json" -H "Authorization: Bearer 你的API密钥" \
  -d '{"zone":"serp_api1","url":"https://www.google.com/search?q=pomodoro+timer&gl=us&hl=en&brd_json=parsed_light","format":"raw"}'
参数 作用
gl 搜索地区
hl 界面语言
brd_json=parsed_light 只返回自然结果等主要部分,官方说明比完整 JSON 快约一倍

注意:官方文档特别说明 q 要放在最前面,gl 写在 q 之前属于错误写法。

第二步:代码怎么写?

1. keywords.json

{
  "domain": "example.com",
  "gl": "us",
  "hl": "en",
  "keywords": ["pomodoro timer online", "focus timer for studying"]
}

2. rank.py

"""关键词排名跟踪:用 Bright Data SERP API 查询 Google,记录你的网站在前 N 条自然结果中的排名。
用法:python rank.py            # 查询 keywords.json 里的所有关键词,追加到 ranks.csv
"""
import csv
import json
import os
import sys
import time
from datetime import date
from pathlib import Path
from urllib.parse import quote_plus, urlparse

import requests

API = "https://api.brightdata.com/request"
HERE = Path(__file__).parent


def serp_url(keyword: str, gl: str, hl: str) -> str:
    # 注意顺序:q 必须放在最前面,gl、hl 跟在后面
    return f"https://www.google.com/search?q={quote_plus(keyword)}&gl={gl}&hl={hl}&brd_json=parsed_light"


def search(keyword: str, gl: str = "us", hl: str = "en") -> dict:
    r = requests.post(
        API,
        headers={"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"},
        json={"zone": os.environ["BRIGHTDATA_SERP_ZONE"], "url": serp_url(keyword, gl, hl), "format": "raw"},
        timeout=90,
    )
    r.raise_for_status()
    return r.json()


def same_site(link: str, domain: str) -> bool:
    host = (urlparse(link).hostname or "").lower()
    return host == domain or host.endswith("." + domain)


def find_rank(result: dict, domain: str):
    """返回 (排名, 链接);不在结果里返回 (None, None)。"""
    for item in result.get("organic", []):
        if same_site(item.get("link", ""), domain):
            return item.get("global_rank") or item.get("rank"), item["link"]
    return None, None


def run(config: dict, searcher=search, out: Path = HERE / "ranks.csv", delay: float = 2.0):
    rows = []
    new = not out.exists()
    with out.open("a", newline="", encoding="utf-8-sig") as f:
        w = csv.writer(f)
        if new:
            w.writerow(["日期", "关键词", "地区", "排名", "链接", "第一名"])
        for i, kw in enumerate(config["keywords"]):
            if i:
                time.sleep(delay)
            try:
                res = searcher(kw, config.get("gl", "us"), config.get("hl", "en"))
            except Exception as e:
                print(f"✗ {kw}:{e}")
                continue
            rank, link = find_rank(res, config["domain"])
            top = (res.get("organic") or [{}])[0].get("link", "")
            row = [date.today().isoformat(), kw, config.get("gl", "us"), rank or "未进入", link or "", top]
            w.writerow(row)
            rows.append(row)
            print(f"{kw}:{rank if rank else '未进入前列'}")
    return rows


if __name__ == "__main__":
    cfg = json.loads((HERE / "keywords.json").read_text(encoding="utf-8"))
    run(cfg)
    sys.exit(0)
要点 说明
organic 数组 官方说明这是唯一保证存在的字段
global_rank 结果在页面上的位置
same_site blog.example.com 算你的,notexample.com 不算
searcher 参数 测试时替换成假函数,不消耗额度

3. fixtures/serp_light.json(测试样例)

{
  "general": { "query": "pomodoro timer online" },
  "organic": [
    { "global_rank": 1, "title": "Competitor timer", "link": "https://pomofocus.example.net/" },
    { "global_rank": 2, "title": "Another one", "link": "https://www.othertimer.example.org/app" },
    { "global_rank": 3, "title": "My Focus Timer", "link": "https://blog.example.com/focus-timer" }
  ]
}

第三步:怎样测试和运行?

1. 测试

import json
from pathlib import Path

from rank import find_rank, run, same_site, serp_url

FIX = json.loads((Path(__file__).parent / "fixtures/serp_light.json").read_text())


def test_query_param_order():
    u = serp_url("pomodoro timer", "us", "en")
    assert u.startswith("https://www.google.com/search?q=pomodoro+timer&gl=us")
    assert u.endswith("brd_json=parsed_light")


def test_subdomain_counts_but_lookalike_does_not():
    assert same_site("https://blog.example.com/x", "example.com")
    assert not same_site("https://notexample.com/x", "example.com")


def test_find_rank():
    assert find_rank(FIX, "example.com") == (3, "https://blog.example.com/focus-timer")
    assert find_rank(FIX, "absent.com") == (None, None)


def test_run_writes_csv(tmp_path):
    out = tmp_path / "r.csv"
    rows = run({"domain": "example.com", "keywords": ["a", "b"]}, searcher=lambda *a: FIX, out=out, delay=0)
    assert [r[3] for r in rows] == [3, 3]
    assert out.read_text(encoding="utf-8-sig").splitlines()[0] == "日期,关键词,地区,排名,链接,第一名"
pip install requests pytest
python -m pytest -q

本文实际输出 4 passed。

2. 真实运行

export BRIGHTDATA_API_KEY=你的API密钥
export BRIGHTDATA_SERP_ZONE=serp_api1
python rank.py

期望输出每个关键词一行,例如 pomodoro timer online:7,数字取决于真实搜索结果。

3. 每天运行并查看趋势

用 cron 每天运行一次,一段时间后用 Excel 打开 ranks.csv,以日期为横轴、排名为纵轴插入折线图。排名数字越小越好,可以把纵轴设为逆序。

出问题时该怎么调?

1. 排名一直“未进入”

原因 解决方法
确实不在前 10 Light JSON 只含 10 条自然结果,需要更深时改用完整 JSON 并研究翻页参数
域名写成了带 www domain 填主域名,代码会匹配子域名
地区不对 检查 gl、hl

2. 返回的是 HTML 不是 JSON

URL 里没有带 brd_json 参数,或者写错了值。

3. 不同天排名波动很大

搜索结果本身会因地区、时间、个性化而变化。看一周以上的趋势,不要只看单日。

常见问题怎么处理?

现象 原因 解决方法
KeyError: BRIGHTDATA_API_KEY 没设置环境变量 先 export
401 密钥错误 重新复制
结果地区不对 gl 放在了 q 前面 按文档顺序
CSV 中文乱码 编码 文件已用 utf-8-sig 写入
请求太慢 完整 JSON 体积大 用 parsed_light
关键词很多 逐个查询 官方提供异步模式,可按文档改造

项目资源

用途 地址
SERP API 第一个请求 https://docs.brightdata.com/products/serp-api/send-your-first-request
Google SERP API 入门 https://docs.brightdata.com/products/serp-api/get-started-google-serp-api
解析后的 JSON 结构 https://docs.brightdata.com/products/serp-api/parsed-json-results
响应格式参数 https://docs.brightdata.com/products/serp-api/features
Google 查询参数 https://docs.brightdata.com/products/serp-api/query-parameters/google
免费额度 https://docs.brightdata.com/general/account/billing-and-pricing/free-tier

建议先跑 pytest 理解排名判断逻辑,再用 curl 手动查一个关键词,确认返回的 JSON 里有 organic。

之后只放两三个关键词运行一周,观察额度消耗,再增加关键词。