用 Python 抓 Google SERP:真实失败日志与真正可行的方案
每个 SERP 爬虫的故事都一样:第一天能跑,两周后数据悄悄归零,因为请求依然返回 200。本文复盘一次真实的 Google 抓取失败——302 区域重定向、一个装着零条结果的 200 页面——再讲为什么 Bing 好抓但换个方式骗你,以及从裸请求到免费额度 SERP API 的三级阶梯。真实命令、真实输出、真实局限。
抓 Google SERP 失败得比你想象的安静:请求成功了(HTTP 200),返回的页面里却一条结果都没有。本文复盘真实日志——一次 302 区域重定向,落进一个 92 KB 的 JS 空壳——然后给出能用的三级阶梯:裸请求(脆)、无头浏览器(重)、免费额度 SERP API(结构化、按量计费)。附真实耗时、数据中心 IP 下 Bing 与 Google 的真实行为差异,以及可直接粘贴的限速 + 退避代码。
每个 SERP 爬虫的传记都一样。第一天能跑。你写好解析器,觉得自己很聪明。两周后关键词计数悄悄归零,没人发现——因为请求依然返回 200。
这篇文章是尸检报告。真实命令、真实响应、来自真实数据中心 IP,不是「Google 可能会封你」式的假设性警告。然后是真正能用的阶梯,连同成本。
实验:一次 curl 打向 Google
最小可行爬虫,连 Python 都不用——curl 加一个正常桌面 UA:
curl -s -o /tmp/google.html \
-w "HTTP %{http_code}, %{size_download} bytes, ip=%{remote_ip}\n" \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" \
"https://www.google.com/search?q=serp+scraping+python&num=10"
响应:
HTTP 302, 420 bytes, ip=198.18.0.63
没有 CAPTCHA。没有 429。没有「异常流量」页。一个 302 重定向——420 字节的「请去别处」。去哪?看一眼:
curl -s -o /dev/null -w "%{redirect_url}\n" \
-A "Mozilla/5.0 ..." \
"https://www.google.com/search?q=serp+scraping+python&num=10"
https://www.google.com.hk/url?sa=p&hl=zh-CN&pref=hkredirect&pval=yes&q=https://www.google.com.hk/search%3Fq%3Dserp%2Bscraping%2Bpython...
这次请求的出口 IP 落在香港,于是 Google 重定向到 google.com.hk,还硬塞了 hl=zh-CN。不跟重定向的爬虫看到 302 就放弃了。跟重定向的爬虫,会掉进真正的陷阱。
陷阱:装着空气的 200 OK
跟着重定向走:
curl -sL -o /tmp/google2.html -w "HTTP %{http_code}, %{size_download} bytes\n" \
-A "Mozilla/5.0 ..." \
"https://www.google.com/search?q=serp+scraping+python&num=10"
HTTP 200, 92366 bytes
成功了吧?92 KB 的 HTML。看看里面实际有什么:
grep -o '<title>[^<]*</title>' /tmp/google2.html
# <title>Google Search</title>
grep -o -c 'href="/url?' /tmp/google2.html
# 0
grep -o -iE 'enablejs|jserror' /tmp/google2.html | sort | uniq -c
# 2 enablejs
# 1 jserror
**零条自然结果链接。**页面标题是通用的「Google Search」,正文带 enablejs 标记——这是「需要 JavaScript」的空壳。Google 返回了一个尺寸正常、完全合法的 HTML 页面,里面没有结果,以后也不会有,因为它要的是真浏览器。
这是抓取里最坏的失败模式:静默成功。你的 requests.get() 返回 200。.status_code == 200 检查通过。解析器找到零个 h3,管线记下「该关键词无结果」,然后你的关键词数据悄悄腐烂。几周后,你正在基于空数据做决策。
修复不是更好的解析器,是断言:
resp = session.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
results = parse_organic(resp.text)
if len(results) < 3:
raise SerpShellError(
f"got 200 but only {len(results)} results — "
f"likely a JS shell or consent page, not a real SERP"
)
对结果条数断言,不对状态码断言。我们发布的每个 SERP 拉取器都这么做。
同一时刻,Bing 那边
同样的请求、同样的 IP、同样的 UA:
curl -s -o /tmp/bing.html \
-w "HTTP %{http_code}, %{size_download} bytes\n" \
-A "Mozilla/5.0 ..." \
"https://www.bing.com/search?q=serp+scraping+python"
HTTP 200, 124893 bytes
完整页面、真实结果、b_algo 结果块就在 HTML 里。从数据中心 IP 抓 Bing 比 Google 容易一个量级。
那直接抓 Bing?我们在自己的关键词难度工具里撞过的坑在这:**从数据中心 IP 抓到的排名,不是目标市场真实用户看到的排名。**我们拿 Bing 位置对过目标市场真实用户的结果——同关键词、不同顺序、有的域名整个消失。位置和 IP 信誉都是排名输入,而你的数据中心出口是个非常反常的「用户」。
好抓但悄悄错,比难抓更糟。至少 Google 的封锁是响的。
真正能用的阶梯
三级,可靠性递增:
**第一级:有礼貌的裸请求。**钉死 locale(gl、hl 参数)、复用 session、请求间 sleep、非 200 指数退避。每天几十个查询没问题。上量或 IP 信誉差的日子就死。做实验行,做管线不行。
**第二级:无头浏览器。**Playwright 用真实指纹能过 JS 空壳,但单查询成本乘 10-50 倍(浏览器启动、页面体积),而且你进了指纹军备竞赛——无头 Chrome 有破绽,Google 会找。要渲染后的页面内容时用它,要 SERP 结构时别用。
**第三级:免费额度 SERP API。**别人运营 IP 池、浏览器和解析,你拿结构化 JSON:
curl -s -X POST "https://google.serper.dev/search" \
-H "X-API-KEY: $SERPER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q":"serp scraping python","num":10}'
{
"searchParameters": {"q": "serp scraping python", "gl": "us", "hl": "en"},
"organic": [
{"title": "Web Scraping in Python (Complete Tutorial 2026)", "link": "https://serpapi.com/blog/python-web-scraping-tutorial/", "position": 1},
{"title": "How to Scrape Google Search Results: Python Tutorial", "link": "https://github.com/oxylabs/scrape-google-python", "position": 2}
],
"peopleAlsoAsk": [...],
"relatedSearches": [...],
"credits": 1
}
连续三次调用的真实耗时:**2.04s、2.34s、3.06s。**免费额度每月 2,500 次——$0 跑完一整条关键词研究管线。而且响应已解析,「静默空页」这个失败模式在结构上不可能存在:organic 数组为空是 API 在告诉你某件事,不是在藏。
我们整条关键词研究栈——难度评分、SERP 意图检查、竞品差距——都跑在这级。
非抓不可时的模式
有时免费额度不够,还是得裸抓。最低限度的纪律:
import time, random, requests
def fetch_with_backoff(url, session, max_retries=4):
for attempt in range(max_retries):
resp = session.get(url, timeout=15)
if resp.status_code == 200 and parse_organic(resp.text):
return resp
if resp.status_code in (429, 503) or resp.status_code == 200:
# 200 但零结果,按软封锁处理
sleep = (2 ** attempt) + random.uniform(0, 1)
time.sleep(sleep)
continue
resp.raise_for_status()
raise RuntimeError("soft-blocked after retries")
# 关键词之间也要限速,不只是重试之间
time.sleep(random.uniform(4, 9))
比代码更重要的三条规则:把 200-零结果当封锁处理(上面整课的重点)、在关键词之间限速而不只是重试之间、缓存每个响应绝不重复问同一个问题——本地缓存层能把 2,500 次免费额度撑出好几倍。
这套认知哪里不完美
-
**你的 IP 表现会不一样。**这些日志来自一条代理路由的一个数据中心出口。住宅 IP 从 Google 拿到的待遇更友好;别的数据中心段拿到的可能直接是 CAPTCHA 而不是礼貌重定向。你的失败模式会不同——但教训(对结果断言,不对状态码断言)无条件迁移。
-
**SERP API 是个依赖。**第三级意味着别人的可用性、别人的定价、一个硬计数器。管线长过 2,500/月的那天,你开始付费。依然比自建浏览器集群便宜,但不会永远免费。
-
**API 的 SERP 是归一化的 SERP。**SERP API 返回的是确定性的、钉死位置的快照——做评分正合适,但不等于某个具体真实用户看到的。同一个 API 内不同市场差多少,我们量过:见本地化排名差异测试。
放进工作流
抓取是管线的数据获取层,不是管线本身:
- 获取——本文的阶梯:裸请求、浏览器或 API
- 撑预算——花下一个 credit 之前先缓存每个 SERP 响应
- 评分——喂给难度评分和 SERP 结构分析
- 追踪——每周同一套钉死的
gl/hl重跑,配合知道自己边界的排名追踪
整条栈一行装好,跑在免费额度上:
pip install git+https://github.com/ZensInk/zens-ink-seo-package.git
export SERPER_API_KEY="your_key"
要点
SERP 抓取的难点从来不是拿到 HTML,而是知道你拿到的 HTML 是不是在撒谎。Google 最有效的反爬武器不是 CAPTCHA,是那个装着空气的 200 OK。对你解析出的东西断言,别对服务器说的话断言——剩下的阶梯自然会就位。
FAQ
为什么我的 Google 爬虫返回 HTTP 200 却抓不到结果?
因为 Google 经常不发封禁页,而是发一个「需要 JavaScript」的空壳页或区域重定向。本文复盘的测试里,google.com/search 返回 302 跳到 google.com.hk,跟过去之后拿到一个 200 OK、约 92 KB 的页面——标题只是「Google Search」,正文里零条自然结果链接。只检查状态码的爬虫会记录一次「成功」,然后解析出空气——除非你对结果条数做断言,否则这个失败是静默的。
抓取 Google 搜索结果合法吗?
本文不给法律意见,实操答案因司法辖区和规模而异。可以确定的是:Google 服务条款禁止自动化抓取,执法以限速为主——在有人找你谈话之前,你会先撞上 consent 墙、CAPTCHA 或 IP 封锁。SERP API 的存在正是为了把这块灰色地带做成服务:你付费(或用免费额度),不用自己做 IP 信誉生意。
Bing 比 Google 好抓吗?
从数据中心 IP 看,好抓得多。同一个被 Google 重定向清空的请求,打到 Bing 返回了完整的 124 KB 结果页,自然结果就在 HTML 里。但坑在于:Bing 的排名本身随 IP 和位置变化——你从数据中心抓到的,不是目标市场真实用户看到的结果。好抓但悄悄错,比难抓但响亮地失败更糟。
拿结构化 Google SERP 数据最便宜的方式是什么?
用免费额度的 SERP API。Serper.dev 每月免费 2,500 次查询,返回已解析的 JSON——自然结果、People Also Ask、相关搜索——单次约 2-3 秒。对一个每月几千关键词的研究管线来说,成本是 $0。再加一层本地缓存,同样的预算还能再翻几倍。