SERP抓取做排名追踪:requests加BeautifulSoup够不够用

发布时间:2026/7/23 8:30:34
SERP抓取做排名追踪:requests加BeautifulSoup够不够用 做SEO的人迟早会遇到一个问题你想知道自己的页面在某个关键词下排第几。手动搜一次两次没问题但如果你要追踪几百个关键词、每周更新一次、还要跟竞品对比手动就不现实了。排名追踪工具市面上一大堆Semrush、Ahrefs、爱站、5118都能做。但这些工具的更新频率、覆盖的搜索引擎、数据粒度不一定满足你的需求。而且如果你只想追踪一批长尾词的排名变化买一个几百美元一个月的套餐有点浪费。所以自己写脚本抓SERPSearch Engine Results Page是个合理的选择。问题是技术选型。大部分人第一个想到的就是requests加BeautifulSoup这两个库简单、文档全、上手快。但真的够用吗先说结论看你抓什么、抓多大量、用什么搜索引擎。如果只是抓百度前10页的自然结果关键词不超过200个更新频率一天一次requests加BeautifulSoup基本够用。加上合理的请求间隔和代理轮换能跑得很稳。但如果你要抓Google或者要处理大规模关键词上千个或者目标搜索引擎有JS渲染依赖这两个库就会开始吃力。这时候你需要考虑Playwright或者Selenium做浏览器自动化或者干脆用商业API。下面具体说。requests加BeautifulSoup的基础方案先看一个能跑的代码框架。以百度为例抓取某个关键词的搜索结果页面提取自然结果的标题和URL。pythonimportrequestsfrombs4importBeautifulSoupimporttimeimportrandomdeffetch_baidu_serp(keyword,page1,proxyNone):headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36,Accept:text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,en;q0.8,Referer:https://www.baidu.com,}params{wd:keyword,pn:(page-1)*10,rn:10,}proxies{http:proxy,https:proxy}ifproxyelseNonetry:resprequests.get(https://www.baidu.com/s,paramsparams,headersheaders,proxiesproxies,timeout15,)resp.encodingutf-8returnresp.textexceptrequests.RequestExceptionase:print(f请求失败:{keyword}第{page}页, 错误:{e})returnNonedefparse_baidu_results(html):soupBeautifulSoup(html,html.parser)results[]foriteminsoup.select(div.result):title_tagitem.select_one(h3 a)ifnottitle_tag:continuetitletitle_tag.get_text(stripTrue)urltitle_tag.get(href,)results.append({title:title,url:url})returnresultsdeftrack_rank(keyword,target_domain,max_pages3):forpageinrange(1,max_pages1):htmlfetch_baidu_serp(keyword,page)ifnothtml:continueresultsparse_baidu_results(html)forrank,iteminenumerate(results,1):actual_rank(page-1)*10rank# 百度的跳转链接需要二次请求才能拿到真实URLiftarget_domaininitem[url]:returnactual_rank,item time.sleep(random.uniform(3,7))returnNone,None这段代码能跑但有几个问题需要注意。第一个问题是百度的搜索结果URL不是真实URL而是一个跳转链接https://www.baidu.com/link?urlfont stylecolor:rgba(0, 0, 0, 0.9);background-color:rgba(0, 0, 0, 0.05);.../font。你要拿到真实URL得再请求一次这个跳转链接从响应头的Location字段里取。这一步会额外增加请求量也会增加被风控的概率。pythondefresolve_real_url(baidu_redirect_url):try:resprequests.get(baidu_redirect_url,allow_redirectsFalse,timeout10)returnresp.headers.get(Location,)except:return第二个问题是CSS选择器。百度的HTML结构经常变今天能抓到的font stylecolor:rgba(0, 0, 0, 0.9);background-color:rgba(0, 0, 0, 0.05);div.result/font下个月可能就变了。你得定期维护选择器。这也是requests加BeautifulSoup方案的一个固有缺陷它依赖固定HTML结构搜索引擎一改版你的解析逻辑就得跟着改。requests加BeautifulSoup不够用的场景上面说的是理想情况。实际跑起来你会遇到几个坎。JS渲染依赖Google的搜索结果页有大量JS渲染的内容。有些结果比如People Also Ask、知识图谱、轮播卡片是JS动态加载的requests拿到的HTML里根本没有这些内容。百度的部分新版结果页也有类似问题。如果你只关心前10条自然结果的排名这个问题不大因为自然结果的链接在初始HTML里就有。但如果你想抓Rich Snippet、Featured Snippet或者更完整的结果类型requests就力不从心了。这种场景下Playwright是更合适的选择pythonfromplaywright.sync_apiimportsync_playwrightdeffetch_google_serp_with_playwright(keyword):withsync_playwright()asp:browserp.chromium.launch(headlessTrue)pagebrowser.new_page()page.goto(fhttps://www.google.com/search?q{keyword})page.wait_for_selector(div.g)htmlpage.content()browser.close()returnhtmlPlaywright会执行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参数就行pythonimportrequestsfromrequests.authimportHTTPProxyAuth# 亿牛云代理配置proxy_hostproxy.16yun.cnproxy_port端口proxy_user用户名proxy_pass密码proxy_urlfhttp://{proxy_user}:{proxy_pass}{proxy_host}:{proxy_port}proxies{http:proxy_url,https:proxy_url,}# 带代理请求百度SERPheaders{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36,}resprequests.get(https://www.baidu.com/s,params{wd:Python爬虫教程},headersheaders,proxiesproxies,timeout15,)用代理之后每个请求可以走不同的出口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。pythonimportsqlite3definit_db(db_pathserp_tracker.db):connsqlite3.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()returnconn抓取。请求SERP页面拿到HTML。这一步的技术选型就是前面讨论的核心。解析。从HTML里提取结果列表包括排名位置、标题、URL、是否是广告等。解析逻辑跟搜索引擎和页面结构强相关需要针对性编写。排名匹配。拿到结果列表后找到目标域名在结果中的排名位置。这里有个坑百度的跳转URL需要二次请求才能拿到真实URL而真实URL可能跟目标域名有差异比如http和https、有没有www、有没有跟踪参数。做匹配的时候要先做URL规范化。pythonfromurllib.parseimporturlparsedefnormalize_url(url):parsedurlparse(url)hostparsed.netloc.lower()ifhost.startswith(www.):hosthost[4:]returnhostdeffind_rank(results,target_domain,max_results100):targetnormalize_url(target_domain)forrank,iteminenumerate(results[:max_results],1):real_urlresolve_real_url(item[url])ifnotreal_url:continueifnormalize_url(real_url)target:returnrank,real_urlreturnNone,None存储。把排名结果存下来包括时间戳。这样你才能看到排名变化趋势。通知。排名变化超过一定阈值时发通知邮件、钉钉、飞书都行这样你不用每次主动去看。一个实际能跑的脚本把上面的部分拼起来做一个能定时运行的排名追踪脚本pythonimportsqlite3importrequestsfrombs4importBeautifulSoupfromurllib.parseimporturlparsefromdatetimeimportdatetimeimporttimeimportrandomclassRankTracker:def__init__(self,db_pathserp.db,proxyNone):self.connsqlite3.connect(db_path)self.proxyproxy self.init_db()definit_db(self):self.conn.execute( CREATE TABLE IF NOT EXISTS keywords ( id INTEGER PRIMARY KEY, keyword TEXT, domain TEXT, engine TEXT DEFAULT baidu ) )self.conn.execute( CREATE TABLE IF NOT EXISTS ranks ( id INTEGER PRIMARY KEY, keyword_id INTEGER, rank INTEGER, url TEXT, checked_at TEXT ) )self.conn.commit()defadd_keyword(self,keyword,domain,enginebaidu):self.conn.execute(INSERT INTO keywords (keyword, domain, engine) VALUES (?, ?, ?),(keyword,domain,engine))self.conn.commit()deffetch_serp(self,keyword,enginebaidu):headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36,Accept:text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,}ifenginebaidu:urlhttps://www.baidu.com/sparams{wd:keyword,rn:50}else:urlhttps://www.google.com/searchparams{q:keyword,num:50}proxies{http:self.proxy,https:self.proxy}ifself.proxyelseNonetry:resprequests.get(url,paramsparams,headersheaders,proxiesproxies,timeout15)resp.encodingutf-8returnresp.textexceptExceptionase:print(f抓取失败{keyword}:{e})returnNonedefparse_results(self,html,enginebaidu):soupBeautifulSoup(html,html.parser)results[]ifenginebaidu:foriteminsoup.select(div.result, div.c-container):aitem.select_one(h3 a)oritem.select_one(a)ifa:results.append({url:a.get(href,),title:a.get_text(stripTrue)})else:foriteminsoup.select(div.g):aitem.select_one(a)ifa:results.append({url:a.get(href,),title:a.get_text(stripTrue)})returnresultsdefresolve_url(self,redirect_url,enginebaidu):ifenginegoogle:returnredirect_urltry:resprequests.get(redirect_url,allow_redirectsFalse,timeout10,proxies{http:self.proxy,https:self.proxy}ifself.proxyelseNone)returnresp.headers.get(Location,redirect_url)except:returnredirect_urldeftrack(self):rowsself.conn.execute(SELECT id, keyword, domain, engine FROM keywords).fetchall()forkw_id,keyword,domain,engineinrows:htmlself.fetch_serp(keyword,engine)ifnothtml:continueresultsself.parse_results(html,engine)targetdomain.replace(www.,).replace(https://,).replace(http://,).rstrip(/)found_rankNonefound_urlNoneforrank,iteminenumerate(results,1):real_urlself.resolve_url(item[url],engine)url_hosturlparse(real_url).netloc.lower().replace(www.,)ifurl_hosttarget:found_rankrank found_urlreal_urlbreaknowdatetime.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()statusf第{found_rank}名iffound_rankelse未进入前50print(f[{keyword}]{status})time.sleep(random.uniform(5,12))defget_history(self,keyword_id):returnself.conn.execute(SELECT rank, checked_at FROM ranks WHERE keyword_id ? ORDER BY checked_at,(keyword_id,)).fetchall()if__name____main__:trackerRankTracker()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层面的访问限制问题是整个技术栈里比较底层但很关键的一环。