2.3 分拣台:网页解析与信息提取


文档摘要

2.3 分拣台:网页解析与信息提取 本节摘要:解析是把稳定 DOM 变成干净数据的关键工位。本节讲两条产线——全文产线(净化为模型友好 Markdown)与字段产线(声明式 schema 抽结构化 JSON),外加动态页面的等待与滚动配合,让分拣台既抓得准又抓得稳。 分拣台的两条产线 调度室的任务单上,要素二"字段与格式"决定了分拣台开哪条产线。全文产线服务语料类任务(预训练补充、RAG 知识库):整页文本都要,但要剪掉导航、广告、推荐位这些噪声。字段产线服务结构化任务(商品监控、评论采集):只要 schema 里列出的字段,多一行都不要。两条产线的下游不同,评价标准也不同——前者看噪声率,后者看字段完整率。

2.3 分拣台:网页解析与信息提取

本节摘要:解析是把稳定 DOM 变成干净数据的关键工位。本节讲两条产线——全文产线(净化为模型友好 Markdown)与字段产线(声明式 schema 抽结构化 JSON),外加动态页面的等待与滚动配合,让分拣台既抓得准又抓得稳。

分拣台的两条产线

调度室的任务单上,要素二"字段与格式"决定了分拣台开哪条产线。全文产线服务语料类任务(预训练补充、RAG 知识库):整页文本都要,但要剪掉导航、广告、推荐位这些噪声。字段产线服务结构化任务(商品监控、评论采集):只要 schema 里列出的字段,多一行都不要。两条产线的下游不同,评价标准也不同——前者看噪声率,后者看字段完整率。

图 2-3 分拣台双产线与关键工序

图 2-3 分拣台双产线与关键工序

字段产线:声明式抽取

字段产线的核心思想是把"怎么抽"从代码搬进 schema:一段声明描述记录边界与字段取值,换来的好处是站点改版时只改声明不动逻辑。schema 的三要素:baseSelector 圈定"一条记录长什么样",fields 描述每个字段从哪取,type 说明取文本还是属性:

import asyncio, json from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode from crawl4ai import JsonCssExtractionStrategy schema = { "name": "商品评论", "baseSelector": "div.review", # 每个评论块是一条记录 "fields": [ {"name": "author", "selector": "span.name", "type": "text"}, {"name": "rating", "selector": "span.stars", "type": "text"}, {"name": "content", "selector": "p.body", "type": "text"}, {"name": "date", "selector": "time", "type": "attribute", "attribute": "datetime"}, # 嵌套列表:一条记录里的多张图片 {"name": "images", "selector": "img", "type": "list", "fields": [{"name": "src", "type": "attribute", "attribute": "data-src"}]}, ], } async def extract_reviews(url: str): cfg = CrawlerRunConfig( cache_mode=CacheMode.BYPASS, extraction_strategy=JsonCssExtractionStrategy(schema=schema), ) async with AsyncWebCrawler() as crawler: result = await crawler.arun(url, config=cfg) rows = json.loads(result.extracted_content or "[]") for r in rows[:2]: print(r["author"], r["rating"], r["date"], "图片x%d" % len(r["images"])) # 输出:用户3921 四星 2025-11-03 图片x2 # 输出:匿名买家 五星 2025-11-05 图片x0 return rows asyncio.run(extract_reviews("https://shop.example.com/p/1001"))

写 schema 的三条经验。第一,baseSelector 选"记录级"容器而不是页面级——选错了整页只剩一条记录。第二,能取属性就别取文本:datetime 属性是机器格式,渲染出来的"3 天前"还要再解析。第三,选择器的抗改版能力排序:语义标签与稳定 class 优于样式类名(那些由构建工具生成的乱码 class),最脆的是 nth-child 这类位置选择器——页面加一个元素就全线报废。

CSS 选择器不是唯一选项。XPath 擅长"向上找父节点""按文本内容定位"这类 CSS 做不到的路径,正则则适合从自由文本里抠电话、编号等模式,三者常在一条产线上接力。Crawl4AI 同样提供 XPath 版的抽取策略,用法与 CSS 版同构。

动态页面的等待配合

分拣台的上游是渲染工位。脚本渲染的页面,数据可能晚于 DOM 出现;懒加载的列表,要滚动才肯吐出后面的内容。三个参数按需组合:

import asyncio from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode from crawl4ai.content_scraping_strategy import PruningContentFilter async def fetch_dynamic(url: str): cfg = CrawlerRunConfig( cache_mode=CacheMode.BYPASS, # 等待条件:目标容器出现且内容非空才继续,比死等固定秒数稳得多 wait_for="css:.product-grid .item:nth-child(30)", # 懒加载:分五次滚动到页底,每次间隔800毫秒,逼出剩余内容 js_code=[ "window.scrollTo(0, document.body.scrollHeight);", ], js_only=False, delay_before_return_html=1.5, # 最后再稳一稳,等图片与文本落位 content_filter=PruningContentFilter(threshold=0.45, threshold_type="fixed"), page_timeout=40000, ) async with AsyncWebCrawler() as crawler: result = await crawler.arun(url, config=cfg) md = result.markdown.fit_markdown # 全文产线:净化后的正文 raw = result.markdown.raw_markdown # 原始全文,两版对比可估噪声量 print("原始字符:", len(raw)) # 输出:原始字符: 18340 print("净化字符:", len(md)) # 输出:净化字符: 6215 print("压缩到 {:.0%}".format(len(md) / len(raw))) # 输出:压缩到 34%(噪声占三分之二,正是净化存在的理由) asyncio.run(fetch_dynamic("https://mall.example.com/list?q=laptop"))

这段代码把两条产线拧在了一起:wait_for 保证分拣台拿到的 DOM 是熟的,PruningContentFilter 与 fit_markdown 是全文产线的剪枝工序。threshold 参数控制剪枝力度,调高剪得狠、可能误伤正文,调低留得全、噪声跟着进语料——没有万能值,抽样人工审是唯一可靠的标定方式,这与 3.3 节的清洗质检是同一套方法论。

常见坑:wait_for 写了选择器但页面分页是"点击加载更多"型,滚动脚本永远等不到第 30 个元素,任务挂在超时上。排查顺序:先确认交互类型(滚动还是点击),再改 js_code 为对应的点击语句,最后才考虑放宽超时。

关键直觉:raw 与 fit 两版 Markdown 的比值本身就是信号。比值稳定说明站点结构稳定;某天比值骤变,多半是改版或净化参数失配——这个指标在 5.4 节的值班看板上是常客。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U