干市场调研、SEO、竞品分析的朋友,应该都经历过这种场景:想搞清楚某个关键词在搜索结果里的表现,打开搜索引擎,输入关键词,翻页,然后一条一条复制标题、链接、摘要,粘到表格里。一个词还算轻松,十个词就开始头疼,要是每周重复一遍,整个人就是人形爬虫。
后来我换了个思路:不自己去浏览器里点,而是把“搜索结果页面”本身当成一个数据接口来调。也就是用一个 SERP API 服务,把搜索引擎结果页拆成 title、link、snippet、排名这些标准字段,直接通过 API 拿回来。这篇文章就记录我如何用 Ace Data Cloud 接入 Google 搜索结果,从注册账号、获取 API Key,到用 Python 批量拉取,再到搭出一个能定时跑的关键词排名监控小工具。想省时间的开发者、运营、产品同学,都可以照着这套流程上手。
1. 为什么要把搜索结果变成 API
1.1 手动采集的三座大山
试想一下:你要调研 50 个关键词在 Google 搜索结果里的自然排名,手动流程是什么?打开首页、输入关键词、回车、等页面加载完,看前 10 条结果的网址,记录下来;翻页再看;换下一个关键词。这么一圈下来,乐观估计两个多小时,而且中间你重复做的全是无差别劳动。
手动采集的问题主要体现在三个方面:
- 效率低:1 个关键词 5 分钟,50 个就是 4 小时起步,还不算中间被打断、看错行、复制漏了之类的低级错误。
- 不可复现:今天加了一个词、明天调整了排序,换了个人再采一遍,结果可能完全不一样,整个过程没有任何留痕,出了问题也说不出是哪个环节搞错了。
- 无法自动化:没法定时跑,更没法在排名掉下去的第一时间感知到变化,等你发现的时候,竞品可能已经抢走一个月的流量了。
这些痛点指向同一个需求:把搜索结果的采集从人工动作变成程序调用。人工只负责定规则、看结论,脏活累活交给脚本。
1.2 SERP 数据到底能用在哪些地方
SERP(Search Engine Results Page)数据,说白了就是搜索引擎结果页里的各种模块信息。很多人以为搜索结果页只是“10 条蓝色链接”,其实不是,它下面包含了很多更有价值的信息块:
- 自然搜索结果(organic):最核心的网页排名列表,这是 SEO 人员每天都盯的东西。
- 付费广告位(ads):竞品投了哪些词、展示文案是什么,想研究对手投放策略的人很需要。
- 本地结果包(local pack):地图、地址、电话等信息,做本地生意的人尤其关注。
- 相关搜索(related searches):用户还搜了什么,做题材挖掘非常好用。
- People also ask(相关提问):长尾问句,做内容规划是现成的选题库。
所以 SERP 数据的应用场景很广,我列一个速览表:
| 应用方向 | 核心数据 | 典型使用人 |
|---|---|---|
| SEO 排名追踪 | organic 结果的 title 和 link | SEO 人员、站长 |
| 竞品情报 | ads 数据、organic 排名变化 | 市场、投放人员 |
| 内容选题 | related searches、PAA 模块 | 内容运营、编辑 |
| 品牌舆情 | 关键词下的媒体、社区结果 | 公关、品牌团队 |
| 数据研究 | 批量搜索结果统计 | 分析师、产品经理 |
1.3 结构化 API 才是正解
为什么强调“结构化 API”?因为浏览器里看到的是给“人”看的渲染页面,不是给“程序”用的数据。同样的搜索结果,人工看很直观,程序想处理就麻烦了:需要做页面解析、处理动态加载、还要面对随时可能变化的前端结构。
而 API 返回的是 JSON,天然是结构化数据。字段就叫 title、link、snippet,不用说不用猜,不依赖 DOM 结构,拿到就能落库。这也意味着你可以非常方便地把后续的告警、报表、归档全部串起来。我自己的感受是:一旦把数据采集这层自动化了,真正花在“分析结果”上的时间才开始变多,而这才是这件事本身的价值所在。
2. 方案选型:为什么是 Ace Data Cloud
2.1 自建爬虫:看似省钱,实际很贵
很多人的第一反应是:写个爬虫自己抓搜索引擎结果页,不花钱。说实话,技术上确实可行,但要长期稳定跑,坑比想象中多:
- 搜索引擎有很强的反爬策略,请求频率稍微上去,就会遇到验证码、人机识别。
- IP 会被限流,需要维护代理资源,这部分成本并不低。
- 结果页前端经常变动,爬虫解析规则随时要跟着改,投入的维护时间并不比写业务代码少。
- 高并发下,抓回来的数据质量参差不齐,缺少统一校验,后续清洗很费劲。
还有合规层面的考虑:批量抓取公开页面,如果用来做商业化项目,需要特别注意对方条款和限制。相比之下,直接通过服务商提供的 API 获取,数据授权和稳定性都更有保障。
2.2 SERP API 服务商横向对比
市面上拿 SERP 数据的路子,主要有这两大类:
| 对比项 | 搜索引擎官方 Search API | 第三方 SERP API 平台 |
|---|---|---|
| 数据范围 | 通常是自定义索引,不等同于完整结果页 | 完整结果页,包含 ads、local pack 等模块 |
| 返回字段 | 相对少 | 字段全,带位置、摘要、站点信息 |
| 配额与价格 | 免费额度少,超出后单次成本偏高 | 按请求量计费,套餐灵活 |
| 上手门槛 | 要建云项目、开服务、配认证 | 注册、拿 Key、按文档请求即可 |
如果你只是需要“网页搜索功能”,官方 API 够用;但如果你想拿到和真实搜索结果比较接近的排名信息,第三方 SERP API 通常更贴合需求。这里我特意说“比较接近”,是因为不同服务商抓取到的数据源和渲染环境有差异,实际使用时还是需要小规模验证一下。
2.3 Ace Data Cloud 的几个加分点
我最后选择 Ace Data Cloud,不是因为它最便宜,而是几个点凑在一起比较顺:
- 接口设计直观:请求参数就是 q、location、language、num 这些,几乎没有学习成本。
- 返回结构标准:organic_results 里就带 position、title、link、snippet,省去大量字段映射工作。
- 生态扩展方便:同一套 Key 还能接它另外一些数据接口,后续要加新闻、购物数据时不用换供应商。
- 文档里的示例挺全,Python、Node.js、curl 都有,照着复制就能跑通。
这里要诚实说明:它不是唯一选择,市面上还有不少同类产品,但接入思路完全通用。你现在用哪个平台都不影响读懂这篇文章,核心方法论是一致的。
2.4 接入前要准备什么
开始写代码前,把下面这些准备好:
- 一个 Ace Data Cloud 账号(注册用邮箱即可)。
- 在控制台创建 API Key。
- Python 3.8 以上环境,推荐 3.10+。
- requests 库、python-dotenv,用 pip 安装即可。
pip install requests python-dotenv pandaspandas 不是必需的,但后面做统计分析、转表格比较方便,建议直接装上。
3. 实操全流程:注册、调用、解析
3.1 注册账号并创建 API Key
登录 Ace Data Cloud 官网,注册流程和大多数云服务差不多:邮箱加密码,去邮箱点一下验证链接就完成了。登录控制台后找到 API Keys 页面,按下面几步操作:
- 点击 Create API Key。
- 给这个 Key 起一个名字,比如 serp-monitor。
- 创建后复制 Key,一般只显示一次,记得立即保存。
- 把 Key 写入本地 .env 文件,不要硬编码在代码里。
我个人的习惯是先创建一个.env文件放在项目根目录:
ACE_DATA_CLOUD_API_KEY=your_api_key_here ACE_SERP_BASE_URL=https://api.ace.datacloud.com/v1/serp/google/search然后用 python-dotenv 加载。这样做的好处是代码提交到 Git 时不会把密钥带出去,以后换 Key 也只需要改配置文件,不用动代码。
3.2 请求参数:一张表讲清楚
SERP API 的请求参数,不同服务商会有细微差别,但主流字段基本一致。我把常用的几个列一下:
| 参数 | 含义 | 是否必填 | 示例 |
|---|---|---|---|
q | 搜索关键词 | 必填 | q=python web framework |
location | 地理区域,影响本地化结果 | 建议填 | location=United States |
language | 返回结果语言 | 选填 | language=en或zh-CN |
device | 设备形态,mobile/desktop | 选填 | device=mobile |
num | 返回网页结果条数 | 选填 | num=10 |
result_type | 抓取哪些板块 | 选填 | result_type=organic |
这里有个经验:如果你做的是全球业务的 SEO 监控,location 和 language 一定不要省。同一个关键词,在纽约和伦敦搜,结果可能差很多;不指定语言,可能混入非目标语言的内容,导致后续分析全偏。我自己刚开始跑的时候,就是因为默认参数吃了个亏,后面专门在 5.2 节讲。
3.3 先用 curl 验证连通性
拿到 Key 之后,不要急着写 Python,先用 curl 验证一遍网络和密钥是否正常。请求大概是这样的:
curl -G "https://api.ace.datacloud.com/v1/serp/google/search" \ -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "q=python web framework" \ --data-urlencode "language=en" \ --data-urlencode "location=United States" \ --data-urlencode "num=10"如果返回一段带 organic_results 字段的 JSON,说明整体链路已经通了。如果返回 401,检查 Key 是否正确;如果返回 400,看是不是参数名拼错了。这里用的接口地址,以你控制台里实际展示的 API 地址为准,不同服务商域名会不一样。
3.4 驱动代码:一次请求与批量封装
curl 通了之后,就可以用 Python 写正式代码了。我习惯先把单个搜索封装成一个函数,这样后面批量任务复用起来很方便:
import os import time from typing import Any import requests from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("ACE_DATA_CLOUD_API_KEY") BASE_URL = os.getenv( "ACE_SERP_BASE_URL", "https://api.ace.datacloud.com/v1/serp/google/search", ) def search_google( query: str, language: str = "en", location: str = "United States", num: int = 10, result_type: str = "organic", retries: int = 3, ) -> dict[str, Any]: headers = {"Authorization": f"Bearer {API_KEY}"} params = { "q": query, "language": language, "location": location, "num": num, "result_type": result_type, } for attempt in range(retries): try: response = requests.get( BASE_URL, params=params, headers=headers, timeout=30 ) response.raise_for_status() return response.json() except requests.HTTPError as exc: if ( response.status_code in (429, 500, 502, 503) and attempt < retries - 1 ): wait_time = 2**attempt print(f"请求失败 {response.status_code},{wait_time} 秒后重试") time.sleep(wait_time) else: raise exc return {}几个设计细节说明一下:
- Authorization 用 Bearer Token,这是 API 服务的常见习惯,照着写就行。
- timeout 设为 30 秒,避免网络抖动时脚本一直挂在那里,白白浪费时间。
- 重试逻辑只对 429、5xx 这类临时错误生效,参数错误 400、鉴权失败 401 不值得重试,重试只会浪费配额。
- 每次重试等待时间按指数递增,1 秒、2 秒、4 秒,给服务端足够的喘息空间。
接下来做批量查询:
keywords = [ "python web framework", "machine learning course", "data engineering tools", ] collected: dict[str, dict] = {} for keyword in keywords: data = search_google(keyword) collected[keyword] = data print(f"{keyword}: {len(data.get('organic_results', []))} 条结果") time.sleep(1)这个简单的time.sleep(1)很关键,它把请求节奏控制在每秒 1 次,避免因过快的并发请求触发限流。真上生产环境的话,建议再加一个带锁的限速器,不过小规模任务这样已经够了。
3.5 解析 organic_results:从 JSON 里拿关键信息
拿到响应后,先把整体结构打印出来看一遍,了解有哪些字段。这一步很重要,别跳过。
import json sample = collected["python web framework"] print(json.dumps(sample, indent=2, ensure_ascii=False))通常你会看到两类信息:search_metadata(本次请求的元信息,比如总结果数、耗时)和 organic_results(自然搜索结果数组)。写一个解析函数,把排名、标题、链接、摘要提取出来:
def extract_organic_results(data: dict) -> list[dict]: results = [] for item in data.get("organic_results", []): results.append( { "position": item.get("position"), "title": item.get("title"), "link": item.get("link"), "domain": item.get("domain", ""), "snippet": item.get("snippet", ""), } ) return results这里有一个细节:最好同时把 domain 字段单独取出来。因为做排名监控时,你关心的是“哪个网站排第几”,而不是一长串 URL。后面按域名分组统计时,单独的 domain 字段会方便很多,也方便做 URL 去重。
3.6 快速导出 CSV 备份
一次采集完,如果不想先做复杂的数据库,直接存 CSV 也是一个很实用的过渡方案:
import csv from pathlib import Path def save_to_csv( all_results: dict[str, list[dict]], output_path: str = "serp_results.csv" ): rows = [] for keyword, results in all_results.items(): for item in results: rows.append({"keyword": keyword, **item}) with Path(output_path).open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter( f, fieldnames=["keyword", "position", "title", "link", "domain", "snippet"], ) writer.writeheader() writer.writerows(rows) save_to_csv({kw: extract_organic_results(data) for kw, data in collected.items()})用utf-8-sig编码导出,主要是方便 Excel 直接打开不乱码。这个细节踩过坑的都懂,不做的话 CSV 里全是乱码,还得手动转码,很烦。
4. 进阶实践:搭一个关键词排名监控小工具
4.1 需求拆解:每天跑一遍,只看变化
基础的批量拉取跑通之后,就可以往真实场景延伸了。我给自己定的小目标是:每天自动跑 10 个核心关键词,记录每个关键词下指定域名的排名,并在排名波动超过 3 位时给出提示。
这样拆开看,需要的能力一共四块:
- 定时触发采集任务。
- 把每次的排名快照存下来。
- 和昨天的数据做对比。
- 变化超阈值时发通知。
每一块都不难,合起来就是一个能长期跑的小系统。
4.2 数据落库:用 SQLite 存快照
监控类任务的特点是:数据量不大,但需要反复查询和对比。SQLite 完全够用,零部署,Python 标准库自带,不需要额外起数据库服务。
建表逻辑很简单:
import sqlite3 from datetime import date def init_db(db_path: str = "serp_monitor.db"): conn = sqlite3.connect(db_path) conn.execute( """ CREATE TABLE IF NOT EXISTS serp_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, query TEXT NOT NULL, position INTEGER NOT NULL, title TEXT, link TEXT, domain TEXT, snapshot_date TEXT NOT NULL, UNIQUE(query, domain, snapshot_date) ) """ ) conn.commit() conn.close()加了 UNIQUE 约束,防止同一天对同一个关键词-域名组合重复插入。这个约束在脚本被 cron 重复触发时能起到兜底作用,避免数据翻倍。
4.3 写入本次采集结果
把前面封装好的函数组合起来,跑完一轮关键词后批量写入:
def snapshot_keywords(keywords: list[str], db_path: str = "serp_monitor.db"): conn = sqlite3.connect(db_path) today = date.today().isoformat() for keyword in keywords: data = search_google(keyword) results = extract_organic_results(data) for item in results: domain = item["domain"] link = item["link"] conn.execute( """ INSERT OR IGNORE INTO serp_snapshots (query, position, title, link, domain, snapshot_date) VALUES (?, ?, ?, ?, ?, ?) """, (keyword, item["position"], item["title"], link, domain, today), ) time.sleep(1) conn.commit() conn.close()这里我用INSERT OR IGNORE而不是INSERT OR REPLACE,原因是如果当天已经采集过,重复执行时应该保留第一次的快照,而不是覆盖。如果想要重新采集,最稳妥的方式是删除当天数据后重建,避免半覆盖状态。
4.4 对比逻辑:找出排名波动
对比逻辑用一条 SQL 就能完成:把今天的记录和昨天的记录按“关键词 + 域名”关联,然后计算排名差。
def find_ranking_changes( db_path: str = "serp_monitor.db", threshold: int = 3 ): conn = sqlite3.connect(db_path) rows = conn.execute( """ SELECT today.query, today.domain, yesterday.position AS old_position, today.position AS new_position, today.link FROM serp_snapshots today JOIN serp_snapshots yesterday ON today.query = yesterday.query AND today.domain = yesterday.domain AND yesterday.snapshot_date = date('now', '-1 day') WHERE today.snapshot_date = date('now') AND ABS(today.position - yesterday.position) >= ? ORDER BY ABS(today.position - yesterday.position) DESC """, (threshold,), ).fetchall() conn.close() return rows这里恰好体现了一点:如果之前只存纯 HTML 或者只存截图,这个对比基本没法做。数据落库为结构化字段,才能直接用 SQL 分析,这就是为什么我一直强调“结构化”。
4.5 通知与定时调度
变化结果可以先打印到终端,也可以推送到群机器人。推送逻辑不复杂,就是向 Webhook 地址 POST 一段 JSON:
import requests def notify_webhook(webhook_url: str, text: str): requests.post( webhook_url, json={"msg_type": "text", "content": {"text": text}}, timeout=10, )讲到定时调度,我推荐两种方式:
- 本地环境:crontab 每天固定时间执行脚本。
- 远程环境:用 GitHub Actions 的 schedule 触发器,每天自动跑一次,不依赖本地电脑。
GitHub Actions 的 workflow 大致长这样:
name: serp-monitor on: schedule: - cron: "0 0 * * *" workflow_dispatch: jobs: run: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install requests python-dotenv - run: python main.py env: ACE_DATA_CLOUD_API_KEY: ${{ secrets.ACE_DATA_CLOUD_API_KEY }}用 secrets 保存 API Key,而不是直接写进 workflow 文件,这个习惯千万别省。一旦 Key 泄漏到公开仓库,别人就可以拿你的配额去跑请求,账单上全是你的名字。
5. 常见问题与排查实录
5.1 高频错误速查表
按我自己踩过的坑和看过的群友反馈,把最高频的问题整理成一张表:
| 问题现象 | 大概率原因 | 处理建议 |
|---|---|---|
| 返回 401 Unauthorized | API Key 错误或已过期 | 回到控制台重新复制,检查是否有多余空格 |
| 返回 400 Bad Request | 参数名拼错或字段类型不对 | 对照官方文档逐项检查参数 |
| 返回 429 Too Many Requests | 请求频率超过配额 | 加退避重试、拉大请求间隔、升级套餐 |
| 结果和浏览器差别很大 | location / language / device 没设置 | 三个参数都显式传入 |
| organic_results 为空 | 关键词太生僻,或板块参数不对 | 换常见词测试,检查 result_type |
| 中文内容乱码 | 编码处理不当 | 写入文件时用 utf-8-sig,打印时加 ensure_ascii=False |
大多数问题其实都出在参数配置上,不是因为服务商不稳定。所以在动手写复杂逻辑之前,先把最简单的一次请求跑通,能省下大量排查时间。
5.2 实际接入过程中踩过的几个坑
第一个坑是参数没显式指定就开跑。我一开始图省事,只用 q 参数,结果同一个关键词在 API 返回里的结果和浏览器里明显不一致,后来才发现是 location 默认到了某个数据中心所在地,导致本地化结果完全错位。从那以后,凡是和地域相关的研究,我都会把 location、language 全部写死。
第二个坑是 num 大小没想清楚。为了“多拿点数据”,我试过一次性请求 100 条结果,实际跑下来响应时间明显变长,而且很多场景根本用不到这么多。大多数需求前 10 条就够了,先小请求跑通再放大,是更稳的做法。
第三个坑是 URL 归一化。存 link 字段时,同一个网页可能因为带了 utm_source、utm_campaign 之类的追踪参数,被当成两条记录。这会导致排名监测里同一个网站出现多条记录,对比时产生干扰。建议入库前先做一次 URL 清洗,去掉常见追踪参数,再统一存域名。
5.3 关于接入 SERP 数据这件事的一点体会
做 SERP 数据接入,本质上不是“怎么调接口”的问题,而是“拿到数据之后怎么用”的问题。API 只是把数据从服务端搬运到你的手里,真正有价值的是后续的分析链路:排名为什么变、竞品做了什么动作、哪些关键词在下滑趋势里。
我个人的建议是:先定义清楚要解决什么问题,再决定买哪种档位的套餐。很多平台的计费都是按成功请求量来的,如果只是每天十几个关键词的监控,成本完全可控。规模上去之后,再考虑在库里做增量去重、建历史趋势表、甚至接一个简单的看板。整个链路从“把 Google 搜索结果变成 API”开始,往后能延伸出很多可以长期复用的自动化工具。
最后再分享一个小技巧:拿到返回结果之后,先把原始 JSON 完整保存在本地留个底,再做解析。这样即使以后换服务商、换字段结构,你手上也有一份原始数据可以回溯比对。这个习惯帮我避免了不止一次“数据解析完才发现字段理解错了”的尴尬。