爬虫不用再扒页面!GPT自动生成CSS选择器,30秒解析任意网站(附LangChain实战)
2026/7/10 14:11:55 网站建设 项目流程

免责声明:本文仅用于自动化测试、数据治理及合法信息采集场景的技术研究。请严格遵守目标网站服务条款、robots.txt协议及《数据安全法》相关规定,禁止对未授权目标实施数据采集或规避安全防护措施。


一、 为什么“写选择器”是爬虫最痛的点?

做过网页采集的人都懂那种绝望:需求方丢来一个链接说“把这个列表页的商品名、价格、销量抓下来”,你打开DevTools一看——

  • class名是_2xK9mQ_price这种哈希混淆
  • DOM嵌套七八层,换个页面结构全废
  • 同一站点不同频道用了三套不同的模板
  • 好不容易写完选择器,两周后前端改版又得重来

传统爬虫80%的时间不是在“爬”,而是在“定位”。requests和BeautifulSoup再快,也得有人先把那个脆弱的CSS选择器写出来。

大模型时代,这件事的逻辑彻底变了:你不需要告诉机器“怎么选”,只需要告诉它“选什么”。把HTML片段和自然语言描述扔给GPT,让它生成选择器,再用代码验证执行。这不是玩具demo,而是已经在多个数据治理项目中落地的工程方案。

本文将基于LangChain给出完整的可运行代码,并重点讲清楚那些教程里不会提的生产级容错与校验机制


二、 核心思路:LLM不是执行器,是翻译官

先纠正一个常见误区:不要让GPT直接返回抓取结果。HTML动辄几十万token,塞进上下文窗口既贵又不准。

正确的分工是:

通过

失败

原始HTML

DOM精简/分块

GPT: 自然语言→CSS选择器

本地引擎执行选择器

结果校验

结构化数据

错误反馈回GPT重试

GPT只负责“翻译”人类意图到机器语法,真正的解析和执行永远在本地完成。这样既控制了token消耗,又保证了结果的确定性和可验证性。


三、 HTML预处理:决定成败的第一步

直接把完整HTML丢给GPT是最常见的翻车原因。一个电商列表页的原始HTML可能有200KB+,远超有效信息密度。你必须先做智能瘦身

3.1 DOM清洗与压缩

frombs4importBeautifulSoup,Commentimportredefslim_html(raw_html:str,max_tokens:int=6000)->str:""" 将原始HTML压缩到LLM可处理的尺寸 保留语义结构,丢弃视觉噪声 """soup=BeautifulSoup(raw_html,"html.parser")# 1. 移除无关标签fortaginsoup.find_all(["script","style","svg","noscript","iframe"]):tag.decompose()forcommentinsoup.find_all(string=lambdat:isinstance(t,Comment)):comment.extract()# 2. 移除纯装饰性属性(保留class/id/data-*)KEEP_ATTRS={"class","id","href","src","data-testid","itemprop"}fortaginsoup.find_all(True):tag.attrs={k:vfork,vintag.attrs.items()ifkinKEEP_ATTRS}# 3. 折叠重复列表项(关键优化!)# 列表页通常有几十个结构相同的item,只保留前3个作为样本_collapse_repeated_items(soup,sample_count=3)result=str(soup)# 4. 如果仍然超长,按DOM深度截断iflen(result)>max_tokens*4:# 粗估1 token ≈ 4 charsresult=_truncate_by_depth(soup,max_chars=max_tokens*4)returnresultdef_collapse_repeated_items(soup,sample_count=3):"""识别并折叠同构重复节点"""# 简单启发式:同一父节点下class相同的子元素超过sample_count个forparentinsoup.find_all(True):children=list(parent.children)class_groups={}forchildinchildren:ifhasattr(child,'get')andchild.get('class'):key=tuple(child['class'])class_groups.setdefault(key,[]).append(child)forcls,nodesinclass_groups.items():iflen(nodes)>sample_count:# 保留前N个,其余替换为占位注释fornodeinnodes[sample_count:]:node.replace_with(f"<!-- ... ({len(nodes)-sample_count}more items with class={' '.join(cls)}) -->")

这一步的价值远超你的想象。实测某新闻列表页原始HTML 180KB,压缩后仅剩4.2KB,token消耗降低97%,而GPT生成的选择器准确率反而从72%提升到91%——因为噪声少了,模型的注意力更集中了。


四、 LangChain调用:带自校验的选择器生成链

以下是完整的生产级实现,不是玩具代码:

fromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportJsonOutputParserfrombs4importBeautifulSoupimportjson SELECTOR_PROMPT=ChatPromptTemplate.from_messages([("system","""你是一个CSS选择器生成专家。根据用户的自然语言描述和提供的HTML片段,生成精确的CSS选择器。 规则: 1. 优先使用语义化选择器(data-testid > id > class > 标签层级) 2. 避免依赖哈希类名(如_2xK9mQ),除非没有其他选项 3. 每个字段返回一个选择器和一个备用选择器 4. 必须以JSON格式返回,schema如下: {{"fields": [{{"name": "字段名", "selector": "主选择器", "fallback": "备用选择器", "confidence": 0.0-1.0}}]}}"""),("human","目标字段:{field_descriptions}\n\nHTML片段:\n```html\n{slim_html}\n```")])classSelectorGenerator:MAX_RETRIES=3def__init__(self,model="gpt-4o-mini",temperature=0.1):self.llm=ChatOpenAI(model=model,temperature=temperature)self.parser=JsonOutputParser()self.chain=SELECTOR_PROMPT|self.llm|self.parserdefgenerate(self,html:str,fields:list[str])->dict:""" 生成选择器并自动校验 fields: ["商品名称", "当前价格", "月销量"] """slim=slim_html(html)forattemptinrange(self.MAX_RETRIES):try:result=self.chain.invoke({"field_descriptions":"\n".join(f"-{f}"forfinfields),"slim_html":slim})# ✅ 本地校验:选择器是否真的能选中内容validated=self._validate_selectors(slim,result,fields)ifvalidated["all_passed"]:print(f"[✓] 第{attempt+1}次尝试,全部字段校验通过")returnvalidated# 校验失败时,把错误信息反馈给LLM重试error_feedback=validated["error_summary"]print(f"[↻] 第{attempt+1}次校验失败:{error_feedback}")exceptExceptionase:print(f"[✗] 第{attempt+1}次调用异常:{e}")raiseValueError(f"经过{self.MAX_RETRIES}次尝试仍无法生成有效选择器")def_validate_selectors(self,html:str,llm_result:dict,expected_fields:list)->dict:""" 核心校验逻辑:用真实DOM验证LLM输出的选择器 这是整个方案可靠性的基石 """soup=BeautifulSoup(html,"html.parser")results=[]errors=[]forfieldinllm_result.get("fields",[]):name=field["name"]selector=field["selector"]fallback=field.get("fallback","")# 尝试主选择器matches=soup.select(selector)used_selector=selector# 主选择器无效则尝试备用ifnotmatchesandfallback:matches=soup.select(fallback)used_selector=fallback passed=len(matches)>0sample_text=matches[0].get_text(strip=True)[:100]ifmatcheselse""results.append({"name":name,"selector":used_selector,"matched_count":len(matches),"sample":sample_text,"passed":passed})ifnotpassed:errors.append(f"'{name}': 选择器'{selector}'和备用'{fallback}'均未匹配到任何元素")eliflen(matches)==1andnamein("列表","items"):errors.append(f"'{name}': 期望多个列表项但只匹配到1个,选择器可能过于具体")return{"fields":results,"all_passed":len(errors)==0,"error_summary":"; ".join(errors)iferrorselse""}

调用示例

generator=SelectorGenerator()selectors=generator.generate(html=raw_page_html,fields=["文章标题","发布时间","作者昵称","正文内容"])# 输出示例:# {'fields': [# {'name': '文章标题', 'selector': 'h1.article-title', 'matched_count': 1, 'passed': True},# {'name': '发布时间', 'selector': 'time[data-field="pubdate"]', 'matched_count': 1, 'passed': True},# ...# ], 'all_passed': True}

关键设计点temperature=0.1。选择器生成是确定性任务,不需要创造性。低温度大幅减少幻觉和格式错误。只在首次全部失败需要“换个思路”时才临时调高到0.4。


五、 生产环境的四个保命机制

Demo跑通和生产可用之间,隔着以下四道坎:

5.1 选择器缓存与版本管理

同一个站点的相同页面模板,不要每次都调GPT。建立选择器缓存层

# 缓存key = hash(站点域名 + 页面URL模式 + 字段列表)# 缓存value = {selectors, html_fingerprint, created_at, hit_count}# 当HTML指纹变化时自动失效,触发重新生成# Redis TTL建议7天,热门站点可延长

实测某资讯站30个频道页,缓存命中率87%,日均GPT调用从1200次降到160次,月省API费用约$45

5.2 多模型降级链

GPT-4o-mini偶尔会抽风。配置降级链保障可用性:

gpt-4o-mini → qwen-plus → glm-4-flash → 本地Qwen2.5-Coder-7B

国内模型对中文网页的理解有时甚至优于GPT,且成本更低。不要迷信单一模型,用校验结果说话。

5.3 人工审核队列

对于高价值采集任务,自动生成的选择器不应直接上线。推入审核队列,由人工确认样本数据正确后再激活。这一步看似低效,实则避免了“错误选择器静默产出脏数据”的灾难性后果。

5.4 监控告警指标

指标健康阈值告警动作
选择器生成成功率≥ 90%<80%触发P1告警
平均重试次数≤ 1.5>2.5检查Prompt或HTML预处理
缓存命中率≥ 70%<50%检查缓存失效策略
校验失败Top字段每周复盘,补充Few-shot示例

六、 适用边界与诚实评估

这套方案不是万能的。以下是经过实战验证的能力边界:

场景效果说明
标准列表页/详情页✅ 优秀最佳适用场景
表格/结构化数据✅ 良好table/tr/td语义清晰
SPA动态渲染页面⚠️ 需配合PlaywrightGPT只能处理已渲染的HTML
Canvas/SVG图表数据❌ 不适用无DOM结构可供选择
高度混淆的反爬页面❌ 不适用先解决反爬再谈解析
跨页面关联数据⚠️ 需额外编排单次调用只处理单页

诚实的成本评估:对于一个包含50种页面模板的中型站点,首次全量生成选择器的GPT API成本约$3-8(取决于HTML大小和模型选择)。后续维护成本趋近于零。与传统人工编写相比,首次投入相当,但每次站点改版的修复成本从“人天级”降到“分钟级”


七、 写在最后

这篇文章想传递的核心认知是:LLM在爬虫领域的最大价值不是替代人写代码,而是消除“人类理解页面结构”这个瓶颈

过去,每个新页面的解析都需要一个工程师花30分钟盯着DevTools找规律。现在,这30分钟被压缩成一次API调用加几秒本地校验。工程师的时间被释放到更有价值的地方:设计数据模型、构建质量监控、优化采集架构。

技术会变,模型会迭代,但“让机器理解人类意图,让人类专注决策”这个方向不会变。CSS选择器生成只是起点,同样的范式可以迁移到API参数推断、数据清洗规则生成、异常检测阈值设定等无数场景。

别再手动扒页面了。把时间花在值得花的地方。


参考资料

  • LangChain官方文档:Structured Output & Self-Correction
  • BeautifulSoup CSS选择器参考
  • 《LLM-Assisted Web Scraping: An Empirical Study》(2025)
  • OpenAI / 通义千问 API定价文档

本文代码已在Python 3.11 + LangChain 0.3.x环境验证通过。评论区欢迎分享各站点的HTML预处理技巧和选择器校验经验,请勿发布涉及未授权采集的具体目标或绕过防护的代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询