先灌原文再清洗更省事?网页 HTML 直接入 RAG 的召回代价
本文摘要跳过清洗把网页 HTML 直接入库省的是管道时间赔的是召回率。同一渲染页对照两条路线用 BM25 召回率和 token 总量算清代价。静态营销页可以省表格规格页与含脚本状态页必须清洗后再切块。一、问题与结论接内网规格 wiki 进 RAG 时抓取并不难page.content()返回的是 JS 执行后序列化的完整 DOM。难在下一步——有人把这份原始 HTML 按SIZE 800硬切成字符串入库理由是embedding 模型看得懂标签。第一次查询X 型号的电压范围top-3 里出现的是td3.3V/td这样的残片有数字没表头也没型号模型只能猜。本文要验证的结论是跳过清洗并没有省掉账单它把清洗成本从入库时转成了召回时与 token 时而这笔转嫁只在结构简单的页面上划算。判据不用看回答缺不缺内容的主观感受用两个离线指标BM25 Recall3 与 tiktoken token 总量第四节给出复算方法与读数规则。抓取不是本篇变量两条路线喂入的是同一份渲染后 HTML差异只发生在切块与入库。二、排查与选择依据先确认抓取这条腿最短的空白复现page.goto(url)之后不做任何等待就调page.content()异步 hydrate 的页面会给你div idroot/div。修法是wait_untildomcontentloaded加显式等待目标选择器而不是wait_untilnetworkidle——含长轮询的页面等不到 idle 就超时。取文本时locator.inner_text()是渲染可见文本text_content()含隐藏节点两者不要混用。替代方案与取舍方案选择条件代价不该用的情况Firecrawl API多源托管不想自维护渲染按 credit 计费credit 随页数与返回格式字段增长套餐对并发设上限批量抓取时会排队具体数值随版本与套餐变动以官方文档为准本文未验证表格与规格页仍需核对返回体是否丢表头自建Playwright 清洗切分内网、登录墙要控制并发与重试自己维护等待条件、解析器与切分逻辑纯静态页上属于过度工程raw HTML 直灌结构极简的静态页、临时原型token 与召回代价后置到每一次查询规格表页、含__NEXT_DATA__的页三类方案各有各的账托管方案的 credit 消耗随返回格式html、markdown叠加压测前先看余额并发上限决定批量抓取的实际吞吐别按单页耗时线性外推Readability、trafilatura 这类抽取算法在薄内容页会把正文当噪声删掉输出的 markdown 必须抽样核对。三、关键原理标签不识别语义但按 token 计费。任何分词器都把div class...、script、属性值当普通子词切分这些字符进入 embedding 输入、检索上下文和 prompt 预算。比值自行复算同一 URL 分别enc.encode(html)与enc.encode(纯文本)取长度比本文不写死百分比。固定长度切分不感知 DOM 边界。一刀切会落在tr或p中间命中时拿到有单元格没表头或有正文没标题路径的片段。按标题切已是标准件例如langchain-text-splitters里的HTMLHeaderTextSplitter旧代码from langchain.text_splitters import ...在主包拆分后会抛ImportError先确认是包迁移而非拼写。原文有两层隐性文本。每页重复的 nav、footer 行会让不同页面的 chunk 在 BM25 下互相抢命中抓 5–10 页做重复行统计即可复现script里的__NEXT_DATA__、JSON-LD 装着与可见正文不同步的站点文本命中它等于召回用户看不到的内容。四、可运行示例环境Python 3.10 与 Chromium。pipinstallplaywright beautifulsoup4 lxml rank_bm25 tiktoken python-mplaywrightinstallchromium# raw_vs_clean.pyimportre,sysfromplaywright.sync_apiimportsync_playwrightfrombs4importBeautifulSoupfromrank_bm25importBM25Okapifromtiktokenimportget_encoding URL,GOLDsys.argv[1],sys.argv[2:]SIZE,K800,3encget_encoding(cl100k_base)deffetch(url):withsync_playwright()asp:bp.chromium.launch()pgb.new_page()pg.goto(url,wait_untildomcontentloaded)pg.wait_for_selector(h1, main, article,timeout15000)htmlpg.content()b.close()returnhtmldefraw_chunks(h):return[h[i:iSIZE]foriinrange(0,len(h),SIZE)]defclean_chunks(h):soupBeautifulSoup(h,lxml)fortinsoup([script,style,noscript,svg,iframe]):t.decompose()parts,path[],[]forelinsoup.find_all([h1,h2,h3,h4,h5,h6,p,li,pre,td,th]):txtel.get_text( ,stripTrue)ifnottxt:continueifre.fullmatch(rh[1-6],el.name):lvint(el.name[1])pathpath[:lv-1][txt]else:parts.append( .join(path[txt])ifpathelsetxt)out,buf[],forparain\n\n.join(parts).split(\n\n):ifbufandlen(buf)len(para)SIZE:out.append(buf);bufbufpara\n\nreturnout([buf]ifbufelse[])deftokenize(t):returnre.findall(r[a-z0-9_]|[^\w\s],t.lower())# 中文场景换 jiebadefrun(chunks):bm25BM25Okapi([tokenize(c)forcinchunks])hitsum(any(g.lower()inc.lower()forcinbm25.get_top_n(tokenize(g),chunks,nK))forginGOLD)returnhit,sum(len(enc.encode(c))forcinchunks),len(chunks)htmlfetch(URL)iflen(html)500:raiseSystemExit(空壳 HTML渲染未完成请改等具体选择器)forname,chunksin((raw,raw_chunks(html)),(clean,clean_chunks(html))):h,tok,nrun(chunks)print(f{name:6}hits{h}/{len(GOLD)}chunks{n}tokens{tok})运行python raw_vs_clean.pyhttps://内网wiki/规格表页X 型号电压范围Y 接口波特率预期输出格式占位数值以本机结果为准raw hits?/? chunks? tokens? clean hits?/? chunks? tokens?读数规则raw 若hits更低或持平而tokens更高就说明省掉的清洗并没有省掉账单再肉眼确认 raw 的 top-K 片段是不是被标签和导航占了位。实际输出本文未在真实页面上跑过这组数未验证请在自己的目标 URL 执行后填入两行的相对关系才是结论依据。常见失败与修复拿到空壳 HTML原因是渲染晚于goto返回改为等待具体选择器networkidle超时原因是页面长轮询改回domcontentloadedImportError: cannot import name HTMLHeaderTextSplitter原因是该类已迁到独立包langchain-text-splitters中文查询召回失灵原因是rank_bm25默认分词不切中文换jieba或字符级切分。五、失败场景与边界场景 A表格跨tr断裂。查询X 型号的电压范围raw 切分把表头留在上一个 chunk召回片段只剩td3.3V/td同页__NEXT_DATA__的 JSON 含全站规格文本BM25 可能整段命中 JS把正文 chunk 挤出 top-3。根因是结构在切块时丢了不是模型能力不足。场景 B导航跨页串味。站内多页 header 完全相同raw 入库后这些 chunk 与正文混在一起命中菜单文本时返回的是另一个页面的导航检索显示成功、答案全是站点结构。复核方法统计多页重复行再检查 top-K 的来源页是否与金句同页。决策边界按页面类型走页面类型做法静态文档 / 营销页转 Markdown 即可表格与规格页按tr切并继承表头含内嵌 JS 状态页必须删掉script纯代码 / API 参考页保留标签与类名路径反而有利口径提醒cl100k_base与实际 embedding 模型的分词器未必一致token 数只作两条路线的相对比较不当绝对账BM25 是代理指标最终要在自己的查询集上复跑一遍。参考资料tiktokenlangchain-text-splittersFirecrawl 文档Mozilla Readabilitytrafilatura 文档Contextual Retrieval