当前位置: 首页 > article >正文

Cloudflare HTML 解析器的十年演化史(一)

本文基于 Cloudflare 工程博客系列文章第一篇梳理了 Cloudflare 从 2010 年起构建 HTML 流式解析器的完整历程。这不是一篇又一个 HTML 解析器的介绍而是一个工程团队在极端性能约束下反复与现实妥协、不断重建的真实故事。原文链接https://blog.cloudflare.com/html-parsing-1/为什么 Cloudflare 需要自己的 HTML 解析器很多人第一反应是浏览器不是都有现成的 HTML 解析器吗直接用不就好了问题在于Cloudflare 的场景和浏览器完全不同。浏览器解析 HTML 的目标是构建一棵 DOM 树供渲染引擎使用。整个文档下载完之后交给解析器慢慢建树内存够用就行。浏览器甚至可以边解析边让用户等因为用户本来就在等页面加载。Cloudflare 的场景是数据流从源站流过来流经 Cloudflare 的边缘节点流向用户。边缘节点需要在数据经过时实时改写 HTML改完之后继续往前传。整个过程中不能把整个 HTML 文档缓冲下来再处理——太慢用户会感觉到延迟不能构建完整 DOM 树——内存不够而且会引入大量延迟处理速度必须极快——Cloudflare 每个 CPU 承载数百 MB 的流量解析器的吞吐量需求比浏览器高一个数量级内存占用必须极低——哪怕每个请求多用几十 KB乘以海量并发之后都会成为灾难。这就是**流式 HTML 改写器Streaming HTML Rewriter**的设计目标数据流进来边解析边改改完边往外发全程缓冲的数据尽可能少。v0从一个简单的需求开始2010 年Cloudflare 想给客户提供一个功能自动混淆页面上的邮件地址防止爬虫收集。思路很直接——找到页面里长得像邮件地址的字符串把它编码再注入一段 JavaScript 在浏览器端解码还原对真实用户透明但爬虫拿不到明文地址。听起来很简单实际上第一个坑就来了数据是分包到达的。网络传输的数据按包分割你无法控制一个邮件地址userexample.com会不会被切成两个包——前半个包里是userexam后半个包里才是ple.com。如果只处理当前包就会漏掉跨包的邮件地址如果把所有包缓冲起来再处理延迟就不可接受了。解决方案是把正则表达式转换成状态机finite automata用工具 Ragel 来生成高效的状态机代码。状态机的好处是可以在字节流上增量执行处理完当前包之后保存当前状态等下一个包来了接着跑不需要缓冲整个文档。但邮件地址不是孤立存在的——它可能出现在 HTML 注释里可能出现在script标签里这些地方的邮件地址不应该被混淆。于是工程师在状态机里加了跳过注释和标签的逻辑。这是 Cloudflare HTML 解析能力的起点虽然那时还远远谈不上是一个解析器。2011 年Cloudflare 又想加 HTML 压缩minification功能。他们引入了一个叫 jitify 的外部库。问题出现了jitify 有自己的 HTML 处理规则和已有的邮件混淆模块的规则不兼容。两个并行运行的解析器各自有各自的状态各自有各自的 bug还会产生组合 bug。这是临时方案堆叠的经典结局每加一个新功能系统就多一层脆弱性。v1从头来过做一个合规的 HTML5 解析器到 2016 年工程师们意识到必须打破这个循环彻底重写基于 HTML5 规范从零构建一个真正的流式解析器。这个项目最终产出了 LazyHTML已开源https://github.com/cloudflare/lazyhtml 。构建过程中踩了几个有意思的坑每一个都值得展开讲。坑一HTML5 解析规范要求有 DOM但我们不能建 DOMHTML5 规范对 HTML 解析的定义方式是一个词法分析器tokenizer 一个树构建器tree builder协同工作的状态机。关键在于树构建器会反过来驱动词法分析器——词法分析器的当前状态取决于已经构建好的 DOM 树的状态。换句话说你不构建 DOM就没办法正确地切分 token。这也是为什么大多数 HTML 解析器都选择把整个文档读完然后一次性建树——流式解析和规范要求之间天然存在矛盾。Cloudflare 的解法是引入一个**“树构建器反馈模拟器”Parser Feedback Simulator**。它不真正建树但它跟踪足够多的上下文信息让词法分析器相信它在和一个正常的树构建器交互。经过在大量真实页面上的测试这个模拟器能正确处理互联网上绝大多数写得乱七八糟的页面。这个思路来自对已有开源 HTML5 解析器 parse5 的研究部分测试用例也回馈给了上游项目。坑二字符编码——绕开解码一个聪明的洞察流式改写 HTML 还有一个棘手问题字符编码。HTML 文档可以用 UTF-8、GBK、Latin-1 等各种编码而且编码信息可能藏在文档里面比如meta charset...需要读了 1KB 内容之后才能确定。如果要先确认编码再处理就需要缓冲大量数据违背了流式处理的初衷。如果解码之后再处理还要面对不同编码的字节序问题实现复杂度极高。工程师发现了一个关键性质HTML 规范允许的所有字符编码除了 UTF-16 和 ISO-2022-JP都是 ASCII 兼容的。也就是说所有 ASCII 字符在这些编码中的字节表示与纯 ASCII 完全相同非 ASCII 字符的字节值一定在 ASCII 范围之外。而 HTML 语法中所有有意义的边界字符、、、等全部是 ASCII 字符。这意味着只要不修改非 ASCII 内容完全可以在不知道文档编码的情况下安全地解析和改写 HTML。做法是对 UTF-16 文档做嗅探并直接跳过这类文档在现实中不到 0.1%对其他文档原样处理字节流只操作 ASCII 范围内的 token 边界。这个洞察直接避免了解码和重编码的开销既提升了性能也规避了一大类潜在的编码相关安全漏洞。规范中要求把 U0000NUL 字符替换为 UFFFD替换字符的条款LazyHTML 选择了静默忽略。因为 UFFFD 是非 ASCII 字符在不同编码下字节表示不同不知道编码就没法正确处理。而且使用胖指针length pointer而非 C 风格 null 结尾字符串之后NUL 字符本身也不会带来安全问题。坑三Content-Type 是谎言25% 的流量声称是 HTML 但并不是当一个 HTTP 响应的Content-Type是text/html时你会认为里面是 HTML对吧Cloudflare 的实测数据给出了一个令人绝望的答案大约 25% 声称是text/html的响应实际上根本不是 HTML。原因很简单PHP 的默认Content-Type就是text/html。大量开发者写了返回 JSON 的接口、输出图片的脚本却从来没有手动设置Content-Type于是服务器就用默认值把这些响应都标注成了text/html。如果 Cloudflare 的 HTML 改写器对所有text/html响应都盲目处理后果是把 JSON 响应当 HTML 解析时可能错误地把ab识别成一个不完整的 HTML 标签然后在末尾注入脚本代码直接把 API 响应破坏掉。工程师在代码里留了一段注释坦诚记录了这个痛苦的现实原文大意亲爱的后来者我也不喜欢这个 hack但写这段代码的时候互联网是个糟糕的地方。大量网站用 PHP 默认的Content-Type: text/html来返回 JSON API 响应、私钥、二进制图片……我们只能在正式解析之前先确认第一个非空白字符是不是来提高我们猜对的概率。最终形成的内容嗅探逻辑要检测二进制数据、JSON、AMP 页面、XML许多 XML 文档也错误地标注为text/html……整个检测流程复杂到可以画成一张相当大的流程图。这是规范与现实之间鸿沟的典型写照。坑四标签名比较——一个用 5 位哈希换来的单指令比较HTML 解析的一个高频操作是比较标签名当前遇到的标签是a还是div还是script朴素实现是逐字节比较。对于短标签名大多数 HTML 标签名都很短来说这没什么问题但这是一个在每个 token 上都要执行的操作在 Cloudflare 的流量规模下积少成多。LazyHTML 用了一个精妙的哈希方案所有标准 HTML 标签名只包含小写 ASCII 字母和数字 1-6用于h1到h6。这意味着只需要 32 个不同的字符值就能覆盖所有可能。用 5 位来编码每个字符一个 64 位整数可以容纳64 ÷ 5 12个字符——而所有标准 HTML 标签名都不超过 12 个字符最长的是blockquote9 个字符。这样任意一个标准标签名都能被编码成一个唯一的 64 位整数标签名比较就变成了一次整数相等比较。这个哈希甚至不需要额外的遍历——可以在解析标签名的同时逐字节更新哈希值完全零额外开销。有一个小陷阱32 种字符里必须用到00000这个 5 位值如果字母 ‘a’ 被编码为00000那么ab和aab的哈希值就会相同前导零不影响整数值。解决方法也简单把数字 1-6 编码为00000-00101字母从00110开始编码。由于 HTML 规范规定标签名的第一个字符必须是字母不可能是数字前导零的冲突就被规避掉了。LazyHTML 的最终成绩经过以上所有设计权衡LazyHTML 问世。它通过了 HTML5 规范官方测试套件团队还把几处规范描述中的歧义和可简化之处回馈给了规范本身。在 2016 年 9 月的基准测试中以改写 HTML5 规范文档本身7.9 MB 的 HTML 文件为测试用例对比了同时期流行的 HTML 解析器仅 tokenization 模式不构建 ASTLazyHTML 的速度比其他解析器快了约一个数量级。在对 HTTP Archive 中 238 万份真实 HTML 文档的测试中LazyHTML 没有在任何一份文档上崩溃。约 0.2% 的文档超出了缓冲区限制——这些文档实际上是 JavaScript、RSS 或其他内容只是被错误标注成了text/html排除两个已知问题广告网络的文档后这个比例降至 0.03%。一条演化主线背后的工程教训回顾这整段历程有几个模式一再出现特殊场景催生专用方案专用方案积累成负担。邮件混淆、HTML 压缩每个功能都带着自己的解析逻辑和边界处理最终形成了一堆互相干扰、难以维护的代码。这种债务没有办法靠局部修修补补偿还只能重写。规范描述的是理想现实是混乱的。Content-Type: text/html有 25% 在说谎大量页面的 HTML 不符合规范但浏览器能正常渲染规范要求的 NUL 字符替换在不知道编码的情况下无法安全实现……每个按规范来的方案都需要一层针对现实世界的适配。约束是创造力的来源。不能用 DOM催生了 Parser Feedback Simulator不能做字符编码解码促使工程师发现了 ASCII 兼容性这个性质内存极度受限逼出了 5 位哈希标签名比较。如果没有这些约束LazyHTML 就只是又一个普通的 HTML 解析器。第二篇博客将描述 LazyHTML 的下一代继承者LOL HTML——一个用 Rust 写成、支持 CSS 选择器 API 的流式改写器也就是今天 Cloudflare WorkersHTMLRewriterAPI 的底层实现。

相关文章:

Cloudflare HTML 解析器的十年演化史(一)

本文基于 Cloudflare 工程博客系列文章第一篇,梳理了 Cloudflare 从 2010 年起构建 HTML 流式解析器的完整历程。这不是一篇"又一个 HTML 解析器"的介绍,而是一个工程团队在极端性能约束下,反复与现实妥协、不断重建的真实故事。原…...

Keras深度学习实战:从官方文档到社区资源全指南

1. 为什么需要Keras深度学习帮助资源?当你第一次打开Keras文档时,可能会被那些简洁的API示例所迷惑。表面上看起来几行代码就能实现一个神经网络,但真正投入实战时,各种意想不到的问题就会接踵而至。我至今记得自己第一次尝试用Ke…...

serversideup/php性能调优:从开发到生产的完整优化策略

serversideup/php性能调优:从开发到生产的完整优化策略 【免费下载链接】docker-php 🐳 Production-ready Docker images for PHP. Optimized for Laravel, WordPress, and more! 项目地址: https://gitcode.com/gh_mirrors/do/docker-php server…...

英特尔模块化PC设计解析与维修经济性探讨

1. Intel模块化PC设计提案解析英特尔近期发布了一份关于模块化PC设计的白皮书,提出了一种全新的可维修笔记本电脑和迷你PC架构方案。这个提案的核心目标是通过模块化设计提升设备的可维修性,同时减少电子垃圾的产生。作为一名长期关注PC硬件发展的技术从…...

超强Python指南python-guide:Web自动化与浏览器控制终极教程

超强Python指南python-guide:Web自动化与浏览器控制终极教程 【免费下载链接】python-guide Python best practices guidebook, written for humans. 项目地址: https://gitcode.com/gh_mirrors/py/python-guide GitHub 加速计划的 py/python-guide 是一份面…...

Copilot Next 工作流配置不再玄学:12个可复制的settings.json片段,附真实项目性能对比数据(+47.2%编码速度)

更多请点击: https://intelliparadigm.com 第一章:Copilot Next 工作流配置不再玄学:从认知重构到效能跃迁 传统 Copilot 配置常陷入“模板堆砌—反复试错—局部调优”的循环,而 Copilot Next 的核心突破在于将工作流视为可声明、…...

数值型特征选择实战:方法与最佳实践

1. 特征选择的核心价值与挑战当你的数据集包含成百上千个数值型特征时,特征选择就像在嘈杂的派对上寻找真正有价值的对话。我在处理金融风控数据集时曾遇到一个典型案例:原始数据包含387个特征,但实际建模发现只有23个真正影响预测结果。盲目…...

Meteor云原生:Kubernetes集群部署终极指南

Meteor云原生:Kubernetes集群部署终极指南 【免费下载链接】meteor Meteor, the JavaScript App Platform 项目地址: https://gitcode.com/gh_mirrors/me/meteor Meteor作为JavaScript应用平台,提供了从开发到部署的全栈解决方案。本文将详细介绍…...

Ruby LLM框架:为Ruby开发者打造的AI应用开发利器

1. 项目概述:一个为Ruby语言量身打造的LLM应用框架如果你是一名Ruby开发者,最近被各种AI应用搞得心痒痒,想在自己的Rails项目里集成一个智能聊天助手,或者给后台加个自动生成报告的功能,那你可能已经发现了一个尴尬的现…...

Ansible Role Docker多用户管理:团队协作权限配置指南

Ansible Role Docker多用户管理:团队协作权限配置指南 【免费下载链接】ansible-role-docker Ansible Role - Docker 项目地址: https://gitcode.com/gh_mirrors/an/ansible-role-docker Ansible Role Docker是一款强大的自动化工具,能帮助团队轻…...

猫抓浏览器扩展实战指南:从资源嗅探到M3U8解析的完整解决方案

猫抓浏览器扩展实战指南:从资源嗅探到M3U8解析的完整解决方案 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你是否经常遇到网页视频无…...

终极PHP导航菜单指南:从KnpMenu到Spatie Menu的完整实现方案

终极PHP导航菜单指南:从KnpMenu到Spatie Menu的完整实现方案 【免费下载链接】awesome-php A curated list of amazingly awesome PHP libraries, resources and shiny things. 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-php PHP导航菜单是Web应…...

强化学习智能体记忆系统设计:从经验回放到语义检索的架构演进

1. 项目概述:从“记忆”到“决策”的智能体进化最近在复现和调优一些强化学习智能体时,我反复遇到一个瓶颈:智能体在复杂、长周期的任务中表现不稳定,常常“好了伤疤忘了疼”。它可能在某次尝试中摸索出一个绝佳的策略&#xff0c…...

ARM NEON与VFP指令集:高性能嵌入式开发实战

1. ARM NEON与VFP指令集概述在嵌入式系统和移动计算领域,ARM架构的NEON和VFP指令集是提升计算性能的关键技术。作为一位长期从事嵌入式开发的工程师,我经常需要在资源受限的环境中实现高性能计算,而NEON和VFP正是解决这一矛盾的利器。NEON是A…...

nw.js调试工具:10个高级调试技巧解决复杂开发问题

nw.js调试工具:10个高级调试技巧解决复杂开发问题 【免费下载链接】nw.js Call all Node.js modules directly from DOM/WebWorker and enable a new way of writing applications with all Web technologies. 项目地址: https://gitcode.com/gh_mirrors/nw/nw.js…...

ARM DSP加速指令SMLSLD与SMMLA深度解析

1. ARM指令集与嵌入式DSP加速指令概述在嵌入式系统开发领域,ARM架构凭借其精简指令集(RISC)设计理念,长期占据着移动设备和物联网终端的核心地位。作为一位长期从事ARM架构开发的工程师,我发现其指令集设计中特别值得称道的是那些为数字信号处…...

og-aws容器监控终极指南:ECS服务发现与健康检查全解析

og-aws容器监控终极指南:ECS服务发现与健康检查全解析 【免费下载链接】og-aws 📙 Amazon Web Services — a practical guide 项目地址: https://gitcode.com/gh_mirrors/og/og-aws og-aws(GitHub 加速计划)是一份实用的 …...

终极指南:5个技巧加速Elixir宏生成函数编译速度

终极指南:5个技巧加速Elixir宏生成函数编译速度 【免费下载链接】elixir Elixir is a dynamic, functional language for building scalable and maintainable applications 项目地址: https://gitcode.com/GitHub_Trending/el/elixir Elixir是一种动态函数式…...

如何快速解决Elixir项目中Hex模块加载失败的10个实用技巧

如何快速解决Elixir项目中Hex模块加载失败的10个实用技巧 【免费下载链接】elixir Elixir is a dynamic, functional language for building scalable and maintainable applications 项目地址: https://gitcode.com/GitHub_Trending/el/elixir Elixir作为一种动态函数式…...

LSTM时间序列预测中的数据缩放技术与实战

1. 为什么LSTM网络需要数据缩放?在处理时间序列数据时,数据缩放(Scaling)是LSTM网络预处理的关键步骤。想象一下,如果你的数据中某些特征值范围在0-1之间,而另一些特征值范围在1000-10000之间,这…...

如何编写专业Vim文档:从入门到精通的完整指南

如何编写专业Vim文档:从入门到精通的完整指南 【免费下载链接】vim The official Vim repository 项目地址: https://gitcode.com/gh_mirrors/vi/vim Vim作为一款经典的文本编辑器,其强大的功能和高度可定制性使其在开发者社区中广受欢迎。编写清…...

os-tutorial键盘输入:PS/2键盘驱动实现终极指南

os-tutorial键盘输入:PS/2键盘驱动实现终极指南 【免费下载链接】os-tutorial How to create an OS from scratch 项目地址: https://gitcode.com/gh_mirrors/os/os-tutorial 在操作系统开发中,键盘输入是用户与系统交互的基础通道。os-tutorial项…...

Apache Hop实战:Windows平台MySL数据迁移的深度排错与性能调优

AI训练存储选型的演进路线 第一阶段:单机直连时代 早期的深度学习数据集较小,模型训练通常在单台服务器或单张GPU卡上完成。此时直接将数据存储在训练机器的本地NVMe SSD/HDD上。 其优势在于IO延迟最低,吞吐量极高,也就是“数据离…...

如何高效使用PostCSS Input:源文件信息与位置跟踪完整指南

如何高效使用PostCSS Input:源文件信息与位置跟踪完整指南 【免费下载链接】postcss Transforming styles with JS plugins 项目地址: https://gitcode.com/gh_mirrors/po/postcss PostCSS作为一款强大的CSS转换工具,其Input模块在处理源文件信息…...

如何快速掌握Python XML处理技术:从入门到精通的完整指南

如何快速掌握Python XML处理技术:从入门到精通的完整指南 【免费下载链接】python-guide Python best practices guidebook, written for humans. 项目地址: https://gitcode.com/gh_mirrors/py/python-guide GitHub 加速计划的 py/python-guide 项目是一份…...

net-speeder快速入门:5分钟安装配置网络加速神器

net-speeder快速入门:5分钟安装配置网络加速神器 【免费下载链接】net-speeder net-speeder 在高延迟不稳定链路上优化单线程下载速度 项目地址: https://gitcode.com/gh_mirrors/ne/net-speeder net-speeder是一款在高延迟不稳定链路上优化单线程下载速度的…...

如何使用Yew构建高性能实时通信Web应用:WebSocket完全指南

如何使用Yew构建高性能实时通信Web应用:WebSocket完全指南 【免费下载链接】yew Rust / Wasm framework for creating reliable and efficient web applications 项目地址: https://gitcode.com/gh_mirrors/ye/yew Yew是一个基于Rust和WebAssembly的现代Web框…...

Deepnote:云端原生协作笔记本如何重塑数据科学工作流

1. 项目概述:一个为数据科学家量身定制的云端协作笔记本 如果你和我一样,常年和数据、代码、模型打交道,那你一定对Jupyter Notebook又爱又恨。爱它的交互式探索能力,恨它在团队协作、环境管理、版本控制上的种种不便。每次想和同…...

Python统计假设检验17种方法速查与应用指南

## 1. 统计假设检验的核心价值与应用场景统计假设检验是数据分析师和研究人员最常使用的工具之一。在Python生态中,借助SciPy、StatsModels等库,我们可以快速实现各类检验方法。实际工作中经常遇到这样的场景:产品经理拿着AB测试数据问你&quo…...

超轻量歌声转换终极指南:Tiny配置参数调优与性能平衡策略

超轻量歌声转换终极指南:Tiny配置参数调优与性能平衡策略 【免费下载链接】so-vits-svc SoftVC VITS Singing Voice Conversion 项目地址: https://gitcode.com/gh_mirrors/so/so-vits-svc SoftVC VITS Singing Voice Conversion(so-vits-svc&…...