
做SEO的人迟早会遇到一个问题:你想知道自己的页面在某个关键词下排第几。手动搜一次两次没问题,但如果你要追踪几百个关键词、每周更新一次、还要跟竞品对比,手动就不现实了。排名追踪工具市面上一大堆,Semrush、Ahrefs、爱站、5118都能做。但这些工具的更新频率、覆盖的搜索引擎、数据粒度不一定满足你的需求。而且如果你只想追踪一批长尾词的排名变化,买一个几百美元一个月的套餐有点浪费。所以自己写脚本抓SERP(Search Engine Results Page)是个合理的选择。问题是技术选型。大部分人第一个想到的就是requests加BeautifulSoup,这两个库简单、文档全、上手快。但真的够用吗?先说结论看你抓什么、抓多大量、用什么搜索引擎。如果只是抓百度前10页的自然结果,关键词不超过200个,更新频率一天一次,requests加BeautifulSoup基本够用。加上合理的请求间隔和代理轮换,能跑得很稳。但如果你要抓Google,或者要处理大规模关键词(上千个),或者目标搜索引擎有JS渲染依赖,这两个库就会开始吃力。这时候你需要考虑Playwright或者Selenium做浏览器自动化,或者干脆用商业API。下面具体说。requests加BeautifulSoup的基础方案先看一个能跑的代码框架。以百度为例,抓取某个关键词的搜索结果页面,提取自然结果的标题和URL。python
import requests
from requests.auth import HTTPProxyAuth
# 亿牛云代理配置
proxy_host = "proxy.16yun.cn"
proxy_port = "端口"
proxy_user = "用户名"
proxy_pass = "密码"
proxy_url = f"http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}"
proxies = {
"http": proxy_url,
"https": proxy_url,
}
# 带代理请求百度SERP
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}
resp = requests.get(
"https://www.baidu.com/s",
params={"wd": "Python爬虫教程"},
headers=headers,
proxies=proxies,
timeout=15,
)这段代码能跑,但有几个问题需要注意。第一个问题是百度的搜索结果URL不是真实URL,而是一个跳转链接(https://www.baidu.com/link?url=...)。你要拿到真实URL,得再请求一次这个跳转链接,从响应头的Location字段里取。这一步会额外增加请求量,也会增加被风控的概率。python
第二个问题是CSS选择器。百度的HTML结构经常变,今天能抓到的div.result,下个月可能就变了。你得定期维护选择器。这也是requests加BeautifulSoup方案的一个固有缺陷:它依赖固定HTML结构,搜索引擎一改版你的解析逻辑就得跟着改。requests加BeautifulSoup不够用的场景上面说的是理想情况。实际跑起来你会遇到几个坎。JS渲染依赖Google的搜索结果页有大量JS渲染的内容。有些结果(比如People Also Ask、知识图谱、轮播卡片)是JS动态加载的,requests拿到的HTML里根本没有这些内容。百度的部分新版结果页也有类似问题。如果你只关心前10条自然结果的排名,这个问题不大,因为自然结果的链接在初始HTML里就有。但如果你想抓Rich Snippet、Featured Snippet或者更完整的结果类型,requests就力不从心了。这种场景下,Playwright是更合适的选择:python
Playwright会执行JS,拿到的是渲染后的完整DOM。代价是速度慢、资源消耗大。跑一个关键词可能要3到5秒,而requests只要几百毫秒。反爬机制这是最头疼的问题。不管是百度还是Google,对自动化请求都有检测机制。短时间大量请求同一个IP,很快就会触发验证码或者直接封IP。requests本身没有绕过反爬的能力。你能做的是:轮换User-Agent控制请求频率用代理IP池模拟人类浏览行为(加Referer、Cookie、随机延迟)前三条requests能搞定,最后一条比较难。真正的反爬检测不只是看User-Agent,还会看TLS指纹、鼠标轨迹、JS执行能力。requests在这些维度上一眼就露馅。这就是代理IP服务发挥作用的地方。你自己维护一个代理池成本不低,要找代理源、验证可用性、定时清理失效IP。用商业代理服务省事很多。亿牛云提供的就是这类代理IP服务。它的代理池覆盖国内多个城市,支持HTTP和SOCKS5协议,可以按地区选择出口IP。做SERP抓取的时候,按地区选代理很重要,因为搜索引擎的排名结果本身就跟地理位置有关。你用北京IP搜出来的排名,跟用广州IP搜出来的可能不一样。如果你的客户在成都,你想追踪成都地区的排名表现,就得用成都的代理IP去请求。接入也不复杂,requests直接配proxies参数就行:python
import sqlite3
def init_db(db_path="serp_tracker.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS keywords (
id INTEGER PRIMARY KEY,
keyword TEXT NOT NULL,
domain TEXT NOT NULL,
engine TEXT DEFAULT 'baidu',
region TEXT DEFAULT '',
last_rank INTEGER,
last_checked TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS rank_history (
id INTEGER PRIMARY KEY,
keyword_id INTEGER,
rank INTEGER,
url TEXT,
checked_at TEXT,
FOREIGN KEY (keyword_id) REFERENCES keywords(id)
)
""")
conn.commit()
return conn用代理之后,每个请求可以走不同的出口IP,搜索引擎看到的是分散的访问来源,不容易触发频率限制。亿牛云的隧道代理模式更省心,每次请求自动换IP,不用你自己维护IP轮换逻辑。请求规模200个关键词,每个抓3页,就是600个请求。加上解析跳转URL的二次请求,总共1200个请求。如果每个请求间隔5秒,跑完需要100分钟。这个速度对于一周更新一次的场景完全够用。但如果你的关键词库有5000个,每个关键词抓5页,那就是25000个请求。加上二次请求就是50000个。同样的间隔需要69小时。这就不是requests加BeautifulSoup能轻松搞定的了。大规模场景下,你需要并发。requests本身是同步的,可以用concurrent.futures或者aiohttp做并发。但并发越高,反爬风险越大。你得在并发数和封禁风险之间找平衡点。Scrapy在这个场景下更合适,它内置了并发控制、重试机制和中间件系统,可以挂代理中间件、User-Agent轮换中间件,管理起来比手写requests清晰得多。完整的排名追踪流程不管用什么技术栈,一个完整的排名追踪系统包含这几个部分:关键词管理。 维护一个关键词列表,包括关键词本身、目标域名、目标搜索引擎、目标地区。存在CSV或者数据库里都行,关键词多的时候建议用SQLite或者PostgreSQL。python
from urllib.parse import urlparse
def normalize_url(url):
parsed = urlparse(url)
host = parsed.netloc.lower()
if host.startswith("www."):
host = host[4:]
return host
def find_rank(results, target_domain, max_results=100):
target = normalize_url(target_domain)
for rank, item in enumerate(results[:max_results], 1):
real_url = resolve_real_url(item["url"])
if not real_url:
continue
if normalize_url(real_url) == target:
return rank, real_url
return None, None抓取。 请求SERP页面,拿到HTML。这一步的技术选型就是前面讨论的核心。解析。 从HTML里提取结果列表,包括排名位置、标题、URL、是否是广告等。解析逻辑跟搜索引擎和页面结构强相关,需要针对性编写。排名匹配。 拿到结果列表后,找到目标域名在结果中的排名位置。这里有个坑:百度的跳转URL需要二次请求才能拿到真实URL,而真实URL可能跟目标域名有差异(比如http和https、有没有www、有没有跟踪参数)。做匹配的时候要先做URL规范化。python
存储。 把排名结果存下来,包括时间戳。这样你才能看到排名变化趋势。通知。 排名变化超过一定阈值时发通知(邮件、钉钉、飞书都行),这样你不用每次主动去看。一个实际能跑的脚本把上面的部分拼起来,做一个能定时运行的排名追踪脚本:python
import sqlite3
return None
def parse_results(self, html, engine="baidu"):
soup = BeautifulSoup(html, "html.parser")
results = []
if engine == "baidu":
for item in soup.select("div.result, div.c-container"):
a = item.select_one("h3 a") or item.select_one("a")
if a:
results.append({"url": a.get("href", ""), "title": a.get_text(strip=True)})
else:
for item in soup.select("div.g"):
a = item.select_one("a")
if a:
results.append({"url": a.get("href", ""), "title": a.get_text(strip=True)})
return results
def resolve_url(self, redirect_url, engine="baidu"):
if engine == "google":
return redirect_url
try:
resp = requests.get(redirect_url, allow_redirects=False, timeout=10, proxies={"http": self.proxy, "https": self.proxy} if self.proxy else None)
return resp.headers.get("Location", redirect_url)
except:
return redirect_url
def track(self):
rows = self.conn.execute("SELECT id, keyword, domain, engine FROM keywords").fetchall()
for kw_id, keyword, domain, engine in rows:
html = self.fetch_serp(keyword, engine)
if not html:
continue
results = self.parse_results(html, engine)
target = domain.replace("www.", "").replace("https://", "").replace("http://", "").rstrip("/")
found_rank = None
found_url = None
for rank, item in enumerate(results, 1):
real_url = self.resolve_url(item["url"], engine)
url_host = urlparse(real_url).netloc.lower().replace("www.", "")
if url_host == target:
found_rank = rank
found_url = real_url
break
now = datetime.now().isoformat()
self.conn.execute(
"INSERT INTO ranks (keyword_id, rank, url, checked_at) VALUES (?, ?, ?, ?)",
(kw_id, found_rank, found_url, now)
)
self.conn.commit()
status = f"第{found_rank}名" if found_rank else "未进入前50"
print(f"[{keyword}] {status}")
time.sleep(random.uniform(5, 12))
def get_history(self, keyword_id):
return self.conn.execute(
"SELECT rank, checked_at FROM ranks WHERE keyword_id = ? ORDER BY checked_at",
(keyword_id,)
).fetchall()
if __name__ == "__main__":
tracker = RankTracker()
tracker.add_keyword("Python教程", "docs.python.org")
tracker.add_keyword("爬虫框架", "scrapy.org")
tracker.track()这个脚本能跑,但不要直接拿去生产用。它缺少错误重试、并发控制、验证码检测、结果可视化。它是一个起点,不是一个终点。requests加BeautifulSoup到底够不够用回到最初的问题。如果你的需求是:追踪几十到两三百个关键词在百度的排名,每周或每天更新一次,不需要抓JS渲染的内容,预算有限不想买商业API。requests加BeautifulSoup加上一个代理服务(比如亿牛云)就够用。这套方案简单、可控、维护成本低。出了问题你能快速定位是选择器变了还是被反爬了,修改起来也快。如果你要抓Google,关键词规模上千,需要追踪不同地区的排名,或者需要抓取Rich Snippet等JS渲染内容。requests加BeautifulSoup不够用。你需要Playwright做JS渲染,需要更完善的代理管理,可能还需要Scrapy做并发调度。或者直接用SerpAPI、Bright Data这类商业SERP API,它们帮你处理了反爬、代理、解析这一堆麻烦事,你只需要调接口拿数据。还有一个中间方案值得考虑:用requests抓初始HTML,对于JS渲染的部分用Playwright按需补充。不是每个关键词都需要浏览器渲染,大部分自然结果在初始HTML里就有。你可以先用requests抓,如果解析不到结果再fallback到Playwright。这样能在速度和完整性之间取得平衡。最后说一个实际经验。排名追踪的数据精度不需要做到100%准确。搜索引擎本身的排名就有个性化因素,同一个关键词同一时间不同账号搜出来的结果都可能不一样。你的脚本追踪到的是一个相对值,看趋势变化比看绝对数值更有意义。如果上周排第5这周排第8,你可能需要关注一下。但如果一直在第5到第7之间波动,大概率是正常浮动。理解了这一点,你在技术选型上会更务实。requests加BeautifulSoup的精度对趋势追踪来说足够了,不需要为了那点精度提升去上Playwright,增加十倍的资源消耗和复杂度。
文章到这里。核心观点是技术选型要看具体场景,不存在一套方案打天下。requests加BeautifulSoup在中小规模百度排名追踪里够用,加上代理服务能处理大部分反爬问题。规模上去之后或者换搜索引擎,就需要更重的方案。亿牛云这类代理服务在这个流程里解决的是IP层面的访问限制问题,是整个技术栈里比较底层但很关键的一环。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。