4.2 数据提取与结构化输出 本节摘要:让模型"顺便看看"和"正式取证"是两回事。提取类武器配合 Pydantic 字段模型,能把页面信息压成带类型约束的 JSON——字段名你定、格式它守、错了重试。本节给出字段建模模板、提取代码、翻页续取与嵌套结构三个实战形态。 从"随口汇报"到"记录在案" 问模型"这页卖什么",它回一段话,读着挺顺,可你没法把一段话塞进数据库。第 3 章说过观察拍负责"看见",本节是把看见的东西记录在案:先定义证据格式(有哪些字段、什么类型、哪些必填),再让模型按格式填。这套机制的可靠性来自约束——不是求模型好好说话,而是让它只有一种说话方式。
本节摘要:让模型"顺便看看"和"正式取证"是两回事。提取类武器配合 Pydantic 字段模型,能把页面信息压成带类型约束的 JSON——字段名你定、格式它守、错了重试。本节给出字段建模模板、提取代码、翻页续取与嵌套结构三个实战形态。
问模型"这页卖什么",它回一段话,读着挺顺,可你没法把一段话塞进数据库。第 3 章说过观察拍负责"看见",本节是把看见的东西记录在案:先定义证据格式(有哪些字段、什么类型、哪些必填),再让模型按格式填。这套机制的可靠性来自约束——不是求模型好好说话,而是让它只有一种说话方式。

合同立得不好,提取就废。三条原则:
from pydantic import BaseModel class 商品条目(BaseModel): 名称: str # 页面上显示的商品全名 到手价: float # 页面标示的当前售价,不含运费 是否包邮: bool = False # 有"包邮"标识则为真,缺省按不包邮 评分: float | None = None # 部分商品无评分,允许为空 class 商品列表(BaseModel): 条目: list[商品条目] # 整页多条,套一层列表
把合同交给 Agent,任务单只负责划定范围:
from browser_use import Agent, Browser from browser_use import Controller from langchain_openai import ChatOpenAI controller = Controller(exclude_actions=[]) # 提取靠内置提取动作,无需自定义 agent = Agent( task=( "打开演示商城的搜索结果页(关键词:机械键盘)," "提取第一页全部商品的名称、到手价、是否包邮、评分," "不要点进任何商品详情页" ), llm=ChatOpenAI(model="gpt-4o", temperature=0.1), browser=Browser(), controller=controller, structured_output=商品列表, # 挂上合同:产出必须符合商品列表结构 ) result = agent.run(max_steps=8) print(result)
预期输出为符合合同的 JSON(示意):
{"条目": [ {"名称": "素盾K87", "到手价": 99.0, "是否包邮": true, "评分": 4.6}, {"名称": "铁臂K100", "到手价": 149.0, "是否包邮": false, "评分": null} ]}
拿到这串东西,直接序列化入库,不需要任何正则清洗。
翻页续取用 3.3 的粒度拆解:一页一仗,仗仗交割,主循环里把各页 JSON 拼起来。比"一单翻五页"稳定得多——某页翻失败只重打那一页。
all_rows = [] for page in range(1, 6): agent = Agent( task=( f"打开搜索结果第{page}页,提取本页商品(字段同前)," "只提取不点击" ), llm=llm, browser=browser, structured_output=商品列表, ) r = agent.run(max_steps=8) all_rows.extend(解析出条目(r)) # 解析函数按实际返回结构写 print(len(all_rows)) # 预期:与五页实际条目总数一致
嵌套结构(商品加评论、订单加明细)是同一套打法的加深:外层模型套内层模型,字段名照旧写清语义。但别贪深——层级超过两层,模型的填表错误率明显上升,此时该考虑先提外层、再开子任务提内层。
提取类武器适合"量不大、结构杂、页面常变"的场景:几十个页面、字段五花八门、明天就改版——模型提取的适应性完胜写死的选择器。不适合"量大海且结构稳定"的场景:十万条同构列表页,一个选择器脚本的成本是它的百分之一。记得 1.3 的口诀,两者从来是搭档而不是对手。
**问:字段越多越好吗?**不是。每加一个字段,填表错误率就抬一分,尤其可选字段多的时候——模型会倾向"都填点什么"。原则:入库要什么就定什么,"顺便也拿一下"的字段全部砍掉。真要扩展,改合同重跑的成本很低,别在一张合同上贪大求全。
**问:数字拿回来变成字符串怎么办?**先查合同——字段声明为 float 时框架会强制转换,转不了的会打回重试。若你自己写的是字符串类型又想排序计算,那就是合同的锅不是模型的锅。类型收紧是免费的质检,别浪费。
**问:空值算失败还是算正常?**在合同里显式表态:允许为空的字段给默认值并写明语义("无评分时为空"),不允许为空的字段不设默认——缺了就打回。最怕的是既没默认也没约束,空值静默入库,到分析阶段才发现整列是空的,6.3 里那个"静默漂移"病例就是这么来的。
很多站点自带导出功能(下载报表、发送邮件)。能用导出就用导出:一次下载的数据完整性远高于逐字段提取,成本还低。提取武器的主场是"页面根本不提供导出"的场景——判断顺序永远是:先接口(1.3),再导出按钮,最后才是模型逐页提取。
字段合同不是一次定稿的宪法,它随业务理解加深而改版——这周发现还需要"促销标签",下周发现"评分"其实没人用。给合同留版本习惯:改动记一行(日期、改了什么、为什么),旧版存档。别小看这一行字:三个月后数字对不上时,"合同什么时候改的"永远是第一个要查的问题。合同版本与巡检基准(5.3)配合时更是刚需——字段变了,基准比对逻辑必须同步知道。
武器库最后一件:给军械库添自造装备——自定义动作,顺便把危险操作锁进人工闸门。