ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

面向AI的Web内容重写:从HTML到干净数据的实践指南

面向AI的Web内容重写:从HTML到干净数据的实践指南 很多人做 AI 应用时最常遇到的不是模型能力不行而是喂给模型的网页内容质量太差。传统 HTML 页面是为人类浏览设计的导航、广告、弹窗、追踪脚本和正文混杂在一起大模型直接读这些内容既浪费 token又会把注意力带到错误的地方。Scrunch 这类思路的核心就是重新设计 Web 内容的表达方式把“给人看的页面”改写成“给 AI 读的干净数据”。本文从背景、原理、代码实现到排错建议完整拆解一套可以落地的 Web for AI 重写方案适合正在做 RAG、Agent、智能阅读或内容管道的开发者。1. 背景为什么 AI 看不懂传统 Web1.1 信息消费方式的改变过去十年Web 页面的主要消费对象是浏览器和搜索引擎爬虫。页面设计关注视觉层次、用户跳转和停留时长HTML 里充满nav、aside、header、图片懒加载、复杂 CSS 选择器和大量 JavaScript。搜索引擎爬虫经过多年训练能通过链接权重和页面索引提取有用信息但大模型并不具备这种隐式理解能力。AI Agent 或者大模型应用在获取网页内容时通常有两种方式直接读取 URL 文本让模型解析 HTML。通过爬虫抓取正文后再组装进 Prompt。无论哪种方式HTML 中的噪声都会严重影响效果。导航链接、相关文章推荐、Cookie 弹窗、广告位占位符都会被模型视为有效信息。结果就是模型回答内容偏题、摘要质量下降、引用不准确知识库检索的召回率和精度同时受影响。1.2 传统网页的“噪声”问题一个典型的博客文章页面正文可能只有 2000 字但原始 HTML 往往超过 100KB。这里面包括页面布局标签和样式类名。头部导航与底部页脚。侧边栏推荐位。分享按钮和统计脚本。自动化广告注入的节点。评论区的重复内容。这些内容对人工阅读干扰不大对 AI 来说却是巨大的噪音来源。如果直接把整个 HTML 传给模型不仅会超出上下文窗口还可能导致模型把页脚文本当作正文引用。简单粗暴地剔除脚本标签并不够因为很多页面把正文藏在复杂的div层级中需要结合阅读模式、结构化数据和语义标签来判断。1.3 Scrunch 想解决的问题Scrunch 可以理解成一种设计理念把面向浏览器的 Web 内容重构为面向 AI 的信息结构。它强调的不是换一套抓取工具而是建立一套完整的重写流程包含内容提取、结构净化、语义标注和接口暴露。这套流程解决四个问题可获取性AI 能否稳定拿到页面主要内容。可读性拿到之后能否被大模型准确解析。可检索性内容能否被 RAG 系统按主题切分和检索。可交互性Agent 能否像调用函数一样获取指定信息。因此Scrunch 的实现绝不只是写一个爬虫而是搭建一个“Web 内容到 AI 输入”的转换管道。2. 核心概念什么是面向 AI 的 Web 重写2.1 重写的四个层次面向 AI 的 Web 重写可以从四个层次来理解。内容层这一层负责从 HTML 中提取正文。需要识别哪些节点是文章主体、哪些是页眉页脚、哪些是动态脚本生成的内容。常见的做法是使用可读性算法或者基于语义标签定位article、main等元素。内容层是后续所有处理的基础如果这层出错后面生成的数据也会失去意义。结构层结构层负责给内容补充语义标签。比如页面标题。作者、发布日期、修改时间。文章摘要。主分类和标签。正文中的标题层级。这些信息在传统 HTML 中可能分散在 Metadata、Open Graph 标签或页面正文里。重写时应该把它们整理成统一结构比如 JSON-LD 格式方便 AI 程序读取。表示层表示层负责把原始 HTML 转换成适合大模型阅读的文本格式。Markdown 是目前最通用的选择它用#、-、|等符号表达结构大模型理解成本低token 消耗也远低于 HTML。表示层还需要做长度控制、分块和关键词标签补充确保内容落在模型上下文可处理范围之内。交互层交互层指把重写后的内容封装成 API 或工具函数让 AI Agent 可以通过参数调用。例如一个/rewrite接口接收 URL返回干净的 Markdown 和结构化元数据。这样上游系统可以按需获取内容而不是在每次对话时重新抓取和清洗。2.2 典型应用场景AI 搜索为搜索 Agent 提供干净的网页快照提高答案准确率。RAG 知识库将网页内容分块后写入向量数据库提升检索逻辑。智能阅读助手用户输入链接应用返回“太长不看版”摘要。信息监控定时抓取目标页面提取变更内容并推送给模型分析。企业内部知识库把公司 Wiki 或网页系统重构为可供模型检索的数据。无论哪种场景核心步骤都差不多抓取、清洗、结构化、格式化。3. 环境准备与整体架构3.1 技术栈选型本文演示的 Scrunch 参考服务采用 Python 生态原因很直接类型友好、网页解析库丰富、与大模型工具链集成方便。核心依赖如下Python 3.9。FastAPI提供轻量 API 服务。httpx异步抓取网页内容。BeautifulSoup4 lxml解析 HTML。markdownify把 HTML 转成干净 Markdown。版本不需要刻意固定以当前稳定版本为准。示例代码重点演示处理思路你可以根据项目实际情况调整库和版本。3.2 项目结构scrunch-service/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── scraper.py # 抓取模块 │ └── rewriter.py # 清洗与重写模块 ├── requirements.txt └── README.md3.3 数据流程整个流程可以概括为用户/AI Agent ↓ 传入 URL Scrunch API 服务 ↓ 异步抓取 HTML 内容提取与去噪 ↓ 生成 Markdown / JSON-LD 结构化响应 ↓ 返回给大模型或业务系统这个流程把网页的“展示结构”和“信息结构”彻底分开。后续无论接什么模型上游都能拿到稳定格式的输入。4. 原理拆解如何把网页重写成 AI 友好格式4.1 内容提取与去噪内容提取的目标是找到页面中真正需要保留的主体。最稳定的策略是优先使用语义标签main soup.find(article) or soup.find(main) or soup.bodyarticle标签在 HTML5 语义化规范中专门表示独立文章内容很多现代博客和新闻站都遵循这个规范。main表示页面主要内容区域也可以作为备选。如果两者都不存在soup.body是兜底方案但会引入更多噪声后续需要更精细的过滤。在提取前应该先去掉对 AI 无用的标签。脚本和样式自然不需要iframe通常是嵌入视频或广告svg多数是图标noscript是给禁用 JavaScript 用户看的备用内容。清理之后再调用正文提取。4.2 HTML 转 Markdown 的细节HTML 转 Markdown 不是简单地把p换成换行。真实页面会有大量嵌套结构直接转换可能产生重复空行或丢失标题层级。markdownify库处理得相对稳定但需要指定标题风格让大模型更容易识别层级markdown_text markdownify(body_html, heading_styleATX)ATX 风格就是使用#表示标题。这样生成的内容在 Prompt 里非常直观模型能准确区分“文章标题”“章节标题”和“正文段落”。转换之后还需要做一次空行和首尾空白清理。连续 3 个以上的换行要折叠避免无意义 token 占用。4.3 结构化元数据模型阅读 Markdown 时能理解正文但缺少“发布时间”“作者”这类事实信息。把这些信息整理成 JSON-LD可以让 RAG 系统在检索阶段就完成时间过滤或来源过滤。JSON-LD 是一套基于 JSON 的结构化数据格式Schema.org 定义了常见的文章类型。下面是一个简化示例{ context: https://schema.org, type: Article, headline: Scrunch: Rewriting the Web for AI, url: https://example.com/article, author: Example Author, datePublished: 2025-01-01T10:00:00Z, description: 本文介绍如何将传统网页改写成适合 AI 读取的结构。 }在重写服务中我们不必实现 100% 字段覆盖率但要保证headline、url、主文本内容一定存在。多余字段可以为空避免生成错误类型信息。4.4 长度控制与分块大模型的上下文窗口始终有限。一个长网页转成 Markdown 后可能有几万字直接传给模型既贵又容易丢失重点。更稳妥的做法是提供两种输出模式全文模式适合对准确性要求高、上下文够用的场景。分块模式适合 RAG 场景按章节或固定字符切分。分块时要注意保留标题层级。简单按\n\n切分可能把同一个章节切散导致语义断裂。工程上可以结合标题作为前置信息def split_markdown(text, max_chars2000): blocks [] current [] current_len 0 for line in text.splitlines(): if line.startswith(#) and current_len 0: blocks.append(\n.join(current)) current [] current_len 0 current.append(line) current_len len(line) 1 if current_len max_chars: blocks.append(\n.join(current)) current [] current_len 0 if current: blocks.append(\n.join(current)) return blocks这段代码思路是遇到新的标题块时先关闭当前块避免跨标题切分同时用max_chars限制单块长度。实际项目中可以根据业务调整max_chars比如 1500、2000 或 3000。4.5 将重写内容暴露为 API当重写逻辑做好后把它封装成 HTTP API 是成本最低的接入方式。FastAPI 配合 Pydantic 可以自动生成接口文档AI Agent 一侧只需要维护一个 URL 和参数说明。API 设计成POST /rewrite接收{url: ...}返回完整的 Markdown、标题和结构化元数据。这样做的好处是上游系统可以按需调用。接口可以做缓存、限流和鉴权。后续升级底层抓取逻辑调用方无感知。5. 完整实战搭建一个 Scrunch 风格服务下面我们从空目录开始搭建一个可运行的 Scrunch 参考实现。5.1 初始化项目与依赖先创建项目目录mkdir scrunch-service cd scrunch-service创建虚拟环境并激活python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate编写requirements.txtfastapi uvicorn[standard] httpx beautifulsoup4 lxml markdownify安装依赖pip install -r requirements.txt5.2 编写抓取模块抓取模块需要处理两类问题异步请求和请求头伪装。很多网页对默认爬虫 UA 不友好返回 403所以我们要设置明确的 User-Agent最好加上机器人说明。文件app/scraper.pyimport httpx headers_template { User-Agent: ( ScrunchBot/0.1 (https://example.com/bot; reading page for AI assistant) ) } async def fetch_html(url: str) - str: 抓取目标网页返回 HTML 文本。 async with httpx.AsyncClient( timeout15.0, follow_redirectsTrue, headersheaders_template, ) as client: resp await client.get(url) resp.raise_for_status() return resp.text这里使用了httpx.AsyncClient因为 FastAPI 的异步接口可以直接await不会阻塞事件循环。follow_redirectsTrue可以自动跟随 301/302 跳转避免拿到空内容。5.3 编写清洗与重写模块清洗模块是核心。它需要完成四件事去掉脚本、样式、iframe 等噪声标签。定位正文节点。将正文 HTML 转为 Markdown。生成标题和 JSON-LD 元数据。文件app/rewriter.pyimport re from bs4 import BeautifulSoup from markdownify import markdownify as md def clean_html(soup: BeautifulSoup) - None: 原地清理无关标签。 for tag in soup([script, style, noscript, iframe, svg]): tag.decompose() def normalize_markdown(text: str) - str: 把多余连续空行折叠去掉行尾空格。 lines [line.rstrip() for line in text.splitlines()] text \n.join(lines) text re.sub(r\n{3,}, \n\n, text) return text.strip() def build_json_ld(url: str, title: str, text: str) - dict: 生成 Schema.org Article 结构。 return { context: https://schema.org, type: Article, headline: title, url: url, text: text[:20000], } def rewrite_html(url: str, html: str) - dict: 将 HTML 网页重写为 AI 友好的 Markdown 和元数据。 soup BeautifulSoup(html, lxml) clean_html(soup) if soup.title and soup.title.string: title soup.title.string.strip() else: title url main soup.find(article) or soup.find(main) or soup.body if main is None: raise ValueError(Could not find main content) body_html str(main) markdown_text md(body_html, heading_styleATX) markdown_text normalize_markdown(markdown_text) jsonld build_json_ld(url, title, markdown_text) return { url: url, title: title, markdown: markdown_text, jsonld: jsonld, char_count: len(markdown_text), }为什么要用normalize_markdown因为 HTML 转 Markdown 时嵌套标签很容易留下连续空行。直接返回这种文本大模型可能会把空行误认为是分隔符影响结构判断。压缩空行之后输出更适合放进 Prompt。5.4 编写 API 接口文件app/main.pyfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel from app.scraper import fetch_html from app.rewriter import rewrite_html app FastAPI( titleScrunch API, descriptionRewrite web pages for AI consumption, version0.1.0, ) class RewriteRequest(BaseModel): url: str class RewriteResponse(BaseModel): url: str title: str markdown: str jsonld: dict char_count: int app.post(/rewrite, response_modelRewriteResponse) async def rewrite_page(req: RewriteRequest): # 只允许 http/https避免非网络协议 if not req.url.startswith((http://, https://)): raise HTTPException(status_code400, detailInvalid URL prefix) try: html await fetch_html(req.url) result rewrite_html(req.url, html) except HTTPException: raise except Exception as e: raise HTTPException(status_code502, detailfRewrite failed: {e}) return result app.get(/health) async def health(): return {status: ok}这里增加了基础 URL 校验。实际生产中还需要更严格的 SSRF 防护比如禁止请求内网 IP、保留地址段、云元数据地址等。后面最佳实践部分会展开说。5.5 运行与验证在项目根目录启动服务uvicorn app.main:app --reload看到类似日志INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Application startup complete.接着打开新终端用 curl 测试curl -X POST http://127.0.0.1:8000/rewrite \ -H Content-Type: application/json \ -d {url: https://en.wikipedia.org/wiki/Web}如果目标页面结构正常你会在响应中看到{ url: https://en.wikipedia.org/wiki/Web, title: Web - Wikipedia, markdown: # Web ..., jsonld: { context: https://schema.org, type: Article, headline: Web - Wikipedia, url: https://en.wikipedia.org/wiki/Web, text: # Web ... }, char_count: 12345 }这里需要注意维基百科的页面结构非常规整所以正文提取效果很好。如果你用一个小型个人博客测试可能会发现提取到的正文里仍包含“相关文章”或“评论”。这说明内容提取算法还需要针对站点微调后续可以引入trafilatura或readability-lxml提升鲁棒性。5.6 对接大模型可选有了干净的 Markdown下一步是把它组装进 Prompt。以 OpenAI 兼容接口为例假设有一个函数call_llm(messages)我们可以让模型基于 Markdown 做摘要messages [ { role: system, content: 你是一个信息整理助手请基于用户提供的网页内容生成简洁摘要。, }, { role: user, content: f网页标题{result[title]}\n正文{result[markdown][:6000]}, }, ]这里把正文截断到 6000 字符是为了控制 Prompt 长度。截断不是最优方案更好的做法是先用split_markdown分块再只把相关块传给模型这就是 RAG 的雏形。6. 常见问题与排查思路6.1 抓取到的 HTML 是空内容或 403问题现象常见原因解决思路返回 403 Forbidden网站拦截了默认爬虫 UA设置模拟浏览器 User-Agent并附上联系方式返回 301/302 后内容为空未跟随重定向开启follow_redirectsTrue页面需要登录才能访问内容本身受保护接入会话或 Cookie但必须获得授权请求超时网站响应慢或网络不稳定调整 timeout增加重试和退避策略首先检查是不是站点的反爬策略。很多站点允许搜索引擎抓取但会拦截其他 UA。把 UA 改成带有明确标识的ScrunchBot/0.1 (https://example.com/bot)并在页面中声明机器人抓取规则这样既能降低被封概率也符合爬虫礼仪。6.2 页面靠 JavaScript 渲染抓不到正文现在很多前端项目用 Vue、React 构建HTML 里只有一个空壳div正文由 JavaScript 动态渲染。httpx拿到的源码中没有正文。解决思路有两种使用无头浏览器例如 Playwright渲染完成后再取page.content()。找到页面背后的接口地址直接请求数据接口。无头浏览器成本更高但适配面更广。数据接口方案更轻但需要分析前端源码维护成本高。建议先用静态解析发现动态渲染再引入 Playwright。6.3 正文提取结果包含页脚或相关文章问题现象常见原因解决思路正文包含页脚导航main范围过大在main里继续移除.footer、nav等节点包含评论内容评论区在article内部按注释节点或section属性过滤正文缺少段落页面结构不规范调整正文定位逻辑增加候选节点评估当页面没有语义化标签时soup.find(article)可能会返回None代码会回退到soup.body此时噪声最多。可以使用trafilatura、readability-lxml这类专门做正文抽取的库它们内部实现了基于文本密度和标点比例的判断。6.4 大模型上下文超长网页转化为 Markdown 后仍然可能很长。这时不要盲目截断而应该分块。前面提到的split_markdown是一个基础版本实际项目可以结合“章节标题 向量检索”来定位最相关的内容。把分块结果保存到向量数据库查询时只把相关性高的块放入 Prompt这是 RAG 的标准做法。6.5 JSON-LD 校验失败或字段冲突生成 JSON-LD 后可以使用开放校验工具检查是否符合 Schema.org 规范。常见问题包括text字段过长。datePublished格式错误应该用 ISO 8601。url和页面实际地址不一致。建议把build_json_ld做成可配置函数不同来源站点可以传入不同的元数据解析逻辑。7. 最佳实践与工程建议7.1 遵守 Robots 协议与版权边界面向 AI 的 Web 重写不是无限制爬取。上线前必须检查目标站点的robots.txt并在代码中引入爬取白名单。只处理已经获得授权的站点或在用户主动提供链接时按需抓取。对于版权内容不应该把原文完整倒入知识库后对外提供更多是生成摘要和引用。7.2 做好 URL 安全和 SSRF 防护/rewrite接口如果接受任意 URL就可能被恶意用户用来探测内网资源。生产环境必须做以下检查只允许http和https协议。使用socket.getaddrinfo解析目标域名拒绝内网 IP、保留地址和云元数据地址如 169.254.169.254。对重定向后的目标地址重新校验防止从合法域名跳转到内网。代码示例只是做前缀校验真实项目需要更完整的防护方案。7.3 缓存与限流同一个 URL 被重复抓取很浪费资源。建议引入缓存层内存缓存适合开发环境。Redis 适合多实例部署。缓存 key 可以使用 URL 的哈希值。同时要对接口做限流防止单个用户批量触发抓取影响源站和服务稳定性。抓取频率也要控制尽量配置一个全局请求间隔例如每 3 秒一个请求。7.4 日志与监控重写服务链路较长任何一环失败都会影响上游。建议记录请求 URL、耗时、状态码。抓取结果字符数、分块数。解析失败的具体阶段。源站响应时间。日志可以做结构化输出方便接入监控系统。当某个站点的解析成功率下降时能快速定位是网络问题、反爬问题还是页面改版。7.5 输出格式的前向兼容大模型应用迭代很快今天用的模型和三个月后的模型可能对 Markdown 格式兼容性不同。为了避免后期大规模改动建议在 API 层保持稳定的数据模型把“HTML 转 Markdown”的具体实现隐藏在内部。这样即使替换解析库或分块算法调用方也不受影响。7.6 成本控制与 Token 预估在调用大模型前先估算 Markdown 的 token 数量。可以用简单的字符到 token 比例来粗估英文约 4 字符/token中文约 1.5 字符/token。提前做好成本预算避免长文章摘要消耗过多 token。对于固定来源的内容可以预处理后缓存 Prompt 结果减少重复调用。8. 总结与后续学习路线本文从“AI 看不懂传统 Web”这个实际问题出发介绍了 Scrunch 的设计理念把网页内容从展示层重写为信息层。我们动手搭建了一个最小可用服务包含异步抓取、内容清洗、HTML 转 Markdown、JSON-LD 结构化和 API 暴露整个流程只有几个文件却覆盖了 Web for AI 的核心链路。如果继续深入可以从这几个方向扩展引入Playwright处理动态渲染页面。接入向量数据库实现真正的 RAG 检索。使用trafilatura提升正文抽取效果。为不同站点编写自定义解析规则。在重写结果上增加实体识别和链接提取形成知识图谱。不要把 Scrunch 看作某个固定工具它更像一套“以 AI 为中心”的内容加工理念。只要你有网页抓取、大模型应用或 RAG 检索的需求这套思路都可以帮助你构建更稳定、更干净的数据管道。现在最值得做的是拿一两个你常访问的网站跑通整个流程然后根据实际输出逐步优化。
返回列表