网页一改版爬虫就全崩?LLM自愈爬虫实战:选择器自动修复,运维量直降80%

发布时间:2026/10/31 22:25:34
网页一改版爬虫就全崩?LLM自愈爬虫实战:选择器自动修复,运维量直降80%

做工业数据采集这几年,最头疼的事永远不是反爬,是前端改版。
很多站点反爬没把你搞死,前端开发随手改个类名、调一下DOM层级、换个标签顺序,你写的CSS选择器、XPath就全失效了,数据抓空,告警炸群。小改还好,挨个修选择器要花大半天;碰到全站重构,等于爬虫重写一遍。

去年我们团队维护的二十几个采集站点,每月光花在修复选择器上的时间就有三四十个小时,一半的运维精力都耗在这上面。后来尝试把LLM加进去做自愈,选择器失效了自动识别新的路径,不用人工介入。前后调了两个多月,踩了幻觉、准确率、成本控制一堆坑,最终普通改版的自愈成功率稳定在92%以上,每月的人工运维时间降到了6小时以内,效果比预期好很多。

今天就把整套方案完整分享出来,从设计思路、代码实现到踩坑优化全讲透,看完就能嵌到自己的爬虫框架里。

一、先想清楚:传统的「耐造爬虫」方案差在哪?

在LLM之前,大家也想了很多办法提升爬虫的鲁棒性,也就是常说的Resilient Scraping,但都有自己的天花板。

  1. 绝对路径换相对路径:不用层级过深的绝对XPath,改用id、class、文本特征定位,比绝对路径稳一点,但类名一改还是直接挂。
  2. 文本+属性定位:不靠DOM层级,靠元素的文本、href、data-*属性定位,稳定性更高,但文本改了、属性删了照样失效。
  3. 多规则兜底:同一个元素写3-5个不同的选择器,一个失效了试下一个。能应付小改动,但改版幅度大一点还是全军覆没,而且维护成本翻倍。

这些方案本质都是「提前预判变化」,靠人把可能的情况都想到。但前端改版是不可预测的,你永远猜不到开发会改成什么样,所以兜底规则再多,也总有覆盖不到的情况。

而LLM的思路完全不一样:它不是靠预设规则,是像人一样去「理解」页面结构,根据旧元素的业务特征,在新页面里找到对应的新元素。只要页面的业务逻辑没变,哪怕DOM结构改得面目全非,也能找对位置。

二、LLM自愈爬虫的核心设计:只做一件事,修复选择器

很多人做LLM爬虫一上来就让大模型写整个爬虫代码,结果幻觉满天飞,数据错了都不知道。我们的设计原则很明确:LLM只负责选择器修复这一件事,其余逻辑全靠原有爬虫框架。

整个自愈流程触发时机非常明确:当原有选择器匹配不到元素时,才启动自愈流程,正常情况下完全不调用LLM,不增加额外成本。

完整的执行链路是五步:

  1. 触发:原有选择器匹配失败,捕获异常,进入自愈流程
  2. 切片:提取目标元素周边的DOM片段,而不是整页HTML,减少token消耗和干扰
  3. 推理:把旧选择器、旧元素特征、新页面DOM片段传给LLM,生成新的选择器
  4. 校验:用新选择器去页面里匹配,检查结果是否符合预期(数量、文本、属性特征)
  5. 落库:校验通过就更新选择器缓存,下次直接用;校验失败就触发人工告警

核心是把LLM当成一个智能修复工具,嵌在原有爬虫的异常处理链路里,不侵入原有业务逻辑,上线成本极低。

三、手把手实现:可直接落地的自愈模块

接下来上完整代码,基于Python实现,兼容所有OpenAI协议的大模型,本地部署的开源模型也能用。

3.1 前置准备

只需要两个基础依赖:

pipinstallbeautifulsoup4 openai

用BeautifulSoup做DOM切片和结果校验,openai库调用大模型接口,通用度很高。

3.2 第一步:DOM切片,别把整页HTML丢给LLM

这是第一个大坑。一开始我直接把整页HTML传进去,token又多、干扰又大,LLM经常被无关元素带偏,准确率很低。
正确的做法是:根据旧选择器的特征,在新页面里定位大致的区域,只截取目标周边的DOM片段,再传给LLM。既省token,又减少干扰,准确率直接上一个台阶。

frombs4importBeautifulSoupdefextract_related_dom(html,old_selector,context_range=5):"""提取目标周边的DOM片段,减少无关内容干扰"""soup=BeautifulSoup(html,'html.parser')# 尝试用旧选择器找近似区域,找不到就取主体内容区target=soup.select_one(old_selector)iftarget:# 向上找5层父元素作为上下文parent=targetfor_inrange(context_range):ifparent.parent:parent=parent.parentelse:breakreturnstr(parent)# 旧选择器完全失效,取页面主内容区兜底main_content=soup.select_one('main, .content, #app')ifmain_content:returnstr(main_content)returnhtml[:5000]# 最后兜底,只取前5000字符

3.3 第二步:核心Prompt设计,精准控制LLM输出

Prompt是自愈准确率的关键。要明确告诉LLM任务是什么、输入是什么、输出格式是什么、要遵守哪些规则,绝对不能让它自由发挥。

我们的Prompt经过十几轮优化,核心包含四个部分:角色定义、输入信息、输出要求、约束规则。

defbuild_prompt(old_selector,element_desc,dom_snippet):returnf""" 你是专业的网页元素定位专家。请根据旧的选择器和元素描述,在新的HTML片段中找到对应的目标元素,生成新的CSS选择器。 【旧选择器】:{old_selector}【元素业务含义】:{element_desc}【新页面HTML片段】: ```html{dom_snippet}

要求:

  1. 找到和旧元素业务含义一致的目标元素,生成最稳定的CSS选择器
  2. 优先使用id、data-*属性、文本特征、稳定的类名,避免使用层级过深的绝对路径
  3. 不要使用动态生成的随机类名,优先选择语义化的、不随版本变化的特征
  4. 严格按JSON格式输出,不要额外解释,格式如下:
    {{
    “new_selector”: “新的CSS选择器”,
    “confidence”: 0.95,
    “reason”: “选择理由”
    }}
    置信度0-1,越高表示越准确。如果完全找不到对应元素,置信度填0。
    “”"
### 3.4 第三步:调用LLM获取新选择器 用标准的OpenAI接口调用,支持所有兼容协议的模型,包括本地开源模型。 ```python from openai import OpenAI client = OpenAI( api_key="你的API_KEY", base_url="你的接口地址" ) def llm_fix_selector(old_selector, element_desc, html): dom_snippet = extract_related_dom(html, old_selector) prompt = build_prompt(old_selector, element_desc, dom_snippet) try: resp = client.chat.completions.create( model="qwen3-7b", # 轻量模型足够用,成本极低 messages=[{"role": "user", "content": prompt}], temperature=0.1, # 温度调低,减少随机性 response_format={"type": "json_object"} ) import json result = json.loads(resp.choices[0].message.content) return result except Exception as e: print(f"LLM调用失败: {e}") return {"confidence": 0, "new_selector": ""}

实战建议:用7B参数的轻量模型完全足够,成本只有千亿大模型的几十分之一,准确率也差不了多少。我们用本地部署的Qwen3-7B,和千亿模型的准确率差不到3%,单次调用成本可以忽略不计。

3.5 第四步:结果校验,杜绝幻觉

这是最重要的一步,没有校验的自愈就是埋雷。LLM生成的选择器可能是错的,直接用会导致脏数据入库,比爬虫挂了更可怕。
我们做了三层校验,全部通过才认为修复成功:

defvalidate_selector(html,new_selector,old_features):"""三层校验,确保新选择器正确"""soup=BeautifulSoup(html,'html.parser')elements=soup.select(new_selector)# 1. 数量校验:匹配到的元素数量和预期是否一致iflen(elements)!=old_features.get('expect_count',1):returnFalse# 2. 特征校验:元素的文本、属性是否符合旧特征foreleinelements:# 检查文本是否包含预期关键词ifold_features.get('keywords'):text=ele.get_text()ifnotany(kintextforkinold_features['keywords']):returnFalse# 检查关键属性是否存在ifold_features.get('required_attrs'):forattrinold_features['required_attrs']:ifnotele.has_attr(attr):returnFalse# 3. 置信度校验:LLM给出的置信度高于阈值才通过returnTrue

只有三层校验都通过,才更新选择器;任何一层不通过,都判定为自愈失败,触发人工告警。

四、嵌入现有爬虫:零侵入式改造

不用重写现有爬虫,只要在元素匹配的异常捕获里加一行自愈逻辑就行。
比如用DrissionPage的采集场景,改造后是这样:

defsafe_get_element(page,selector,element_desc,old_features):"""带自愈能力的元素获取函数"""try:ele=page.ele(selector,timeout=2)returneleexceptException:# 匹配失败,触发自愈流程html=page.html fix_result=llm_fix_selector(selector,element_desc,html)iffix_result['confidence']>0.8andvalidate_selector(html,fix_result['new_selector'],old_features):# 修复成功,更新缓存print(f"选择器自愈成功:{selector}->{fix_result['new_selector']}")update_selector_cache(selector,fix_result['new_selector'])returnpage.ele(fix_result['new_selector'])else:# 修复失败,触发告警send_alert(f"选择器失效且自愈失败:{selector}")raise

原有业务代码完全不用改,只是把原来的元素获取函数换成带自愈的版本,半天就能完成全项目改造。

五、踩了两个月的坑,这些经验一定要避开

坑1:整页HTML丢给LLM,费钱还不准

整页HTML动辄几万字符,token消耗大,无关内容多,LLM很容易被干扰找错元素。一定要做DOM切片,只保留目标周边的上下文,准确率能提升20%以上,token消耗还能降到原来的十分之一。

坑2:温度太高,结果五花八门

温度参数一定要调低,建议0.1以下。选择器修复是确定性任务,不需要创意,温度越低,输出越稳定,幻觉越少。一开始用默认0.7,每次生成的选择器都不一样,准确率忽高忽低,调到0.1之后稳定多了。

坑3:不加校验直接用,脏数据比挂了更可怕

千万别相信LLM的输出,一定要加多层校验。我们早期踩过这个坑,LLM把别的元素当成了目标,结果抓了一周的错误数据,返工花了好几天。校验再严都不为过,宁可自愈失败告警,也不能让错误数据入库。

坑4:什么都让LLM做,准确率反而低

不要让LLM负责整个爬虫逻辑,就聚焦在选择器修复这一件事上。任务越单一,提示词越好写,准确率越高,越容易校验。贪多求全,最后哪样都做不好。

坑5:用大模型跑,成本太高

完全没必要用千亿参数的大模型,7B甚至3B的轻量模型就足够胜任这种结构化任务。本地部署开源模型的话,成本几乎可以忽略,比人工修划算太多。

六、实测效果与适用边界

我们在二十几个采集站点上跑了三个月,核心数据非常直观:

  • 普通小改版(改类名、调层级):自愈成功率92.6%,基本不用人工介入
  • 中等改版(模块重构、布局调整):自愈成功率68.3%,剩下的人工处理
  • 全站大重构:成功率不到30%,还是需要人工重写
  • 整体人工运维时间:从每月30-40小时降到5-7小时,下降80%以上

不是万能的,这些场景不适合

  1. 页面完全重构,业务逻辑都变了,LLM也没法无中生有
  2. 对准确率要求100%的场景,还是人工审核更稳妥
  3. 页面结构极其简单,改版频率极低的站点,没必要加LLM增加复杂度

最适合的场景

  • 站点多、改版频繁,人工维护成本高
  • 对可用性要求高,不能动不动就挂
  • 数据允许少量容错,有校验机制兜底

总结

LLM不是银弹,不能解决爬虫的所有问题,但在选择器自愈这件事上,确实能大幅降低运维成本。
它的价值从来不是100%替代人工,而是把80%的简单、重复的修复工作自动化,让人只去处理那20%的复杂改版。把工程师从重复的修选择器的体力活里解放出来,去做更有价值的事。

技术落地永远是这样:不用追求高大上的方案,找对具体的场景,用合适的技术解决具体的痛点,就是最好的方案。

合规提示:本文仅讨论爬虫技术的运维优化方案,采集数据请严格遵守《网络安全法》及目标网站的robots协议,不得用于非法用途。