- 网页爬虫
- 后端
【免费下载链接】SwiftSoup
SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)
本文围绕 SwiftSoup 仓库中一份独立的性能验证报告 independent-css-20260909.md 展开:它记录了对Sources/Whitelist.swift中私有方法stripCSSComments的三行快速路径优化(当 style 值的 UTF-8 字节不含 ASCII 斜杠时直接返回原 String,跳过逐 Character 重建),并完整给出了隔离发布验证、对新 master 的合并确认、A/A 噪声矩阵、回归测试与可复现命令。读完后你能掌握两件事:一是在 HTML 净化(sanitize)场景中识别"可安全提前返回"的热点代码模式,二是用配对块(ABBA/BAAB)、新鲜进程、校验和验证的方法学设计一份可信的微优化基准报告。
一、变更范围:只动三行,不碰任何公开 API
报告的 Scope 一节明确界定了这次改动:
- 基于 master 提交
5d593583469c49d3c84449f28251b4cf201042a8的独立主干改动; - 运行时只修改 Whitelist.swift 一个文件,且在私有方法
stripCSSComments开头新增三行; - 不重复已有的正则编译、实体转义、空白归一化等优化;
- 不改动公开 API、缓存、持久化状态、净化器策略、依赖版本或其他并行分支的内容。
优化的核心思想一句话概括:CSS 注释不可能在没有 ASCII 斜杠(/,字节0x2F)的情况下开始。当 style 值的 UTF-8 视图中一个0x2F都没有时,注释扫描器没有任何可移除的对象,因此可以直接返回既有的String,而不是逐Character重建它。反过来,只要存在斜杠——包括引号内的斜杠、或者位置靠后但并非注释的斜杠——就完整保留原来那套认识引号与转义的扫描器;属性过滤和 unsafe 值检查照常执行;Unicode 的斜杠形近字符(如∕/⁄)不会被视为注释定界符。
在 Whitelist.swift 中可以看到当前落地的实现:
private func stripCSSComments(_ style: String) -> String { // A comment cannot start without an ASCII slash. Leave the existing // quote/escape-aware scanner authoritative whenever one is present. guard style.utf8.contains(0x2F) else { return style } var result = "" var quote: Character? var isEscaped = false var index = style.startIndex while index < style.endIndex { // ... 引号状态机 + 转义跟踪 + /* ... */ 注释移除(见源文件 835–882 行) } return result }关键点有两个:
style.utf8.contains(0x2F)是在 UTF-8 字节层面判断,而非 Character 层面。这一点保证了形近字符(∕U+2215、/U+FF0F、⁄U+2044)的多字节序列中不含0x2F,不会被误判为"含斜杠",从而也绝不会成为注释定界符;- 快速路径只是提前返回,扫描器仍然是权威实现。只要输入里出现真实斜杠,行为与改动前逐字节一致。
报告同时坦陈了取舍(Tradeoff):含斜杠的输入要多付一次"扫到第一个斜杠为止"的额外扫描成本,所以基准里特意保留了一个长文本且斜杠出现在末尾的控制用例(late-slash control)。报告还强调:这并不声称"所有 CSS 处理或所有清洗都变快了",只针对 inline style 值中的注释剥离这一步。
二、这条快速路径处在 SwiftSoup 清洗管线的哪个位置
理解这次优化的价值,需要先看清 inline CSS 在 SwiftSoup 净化器中的完整调用链。
入口:Whitelist.safeAttribute。在 Whitelist.swift 中,safeAttribute(_:_:_:)是Cleaner对每个属性调用的公开判定接口;当属性 key 是style且白名单中配置了允许的 CSS 属性时,会进入sanitizeStyleAttribute:
if isStyleAttribute(attr), let allowedCSSProperties = configuredCSSProperties(for: tagName) { guard let sanitizedStyle = sanitizeStyleAttribute(attr.getValue(), allowedProperties: allowedCSSProperties) else { return nil } ... }解析与过滤:sanitizeStyleAttribute→parseStyleDeclarations→stripCSSComments。按 Whitelist.swift 的实现:
parseStyleDeclarations先调用stripCSSComments(style)去掉注释,再用一个认识引号、转义与括号深度的状态机把剩余文本按顶层;切分成CSSDeclaration(name/value);sanitizeStyleAttribute对每条声明做三重过滤:属性名必须在白名单(支持按 tag 与伪 tag:all配置的并集)内;behavior、-moz-binding两个属性名被isAlwaysUnsafeCSSProperty无条件拒绝;isSafeCSSValue会把值再做一次stripCSSComments+ 小写化 + 空白折叠(正则\s+替换为空),然后拒绝包含expression(、@import、url(的值;- 过滤后无存活声明则整个
style属性被丢弃(返回nil),否则以name:value用"; "连接重建。
上游调用方:Cleaner。Cleaner.swift 在createSafeElement中对源元素的每个属性调用whitelist.safeAttribute,通过者放入新元素的Attributes,被拒者计入numDiscarded。所以任何经由Cleaner.clean(_:)的文档级清洗,其 style 属性都会经过上述管线——这正是基准报告中 "Clean and regenerate styled document"、"Parse, clean and regenerate" 等工作负载的底层路径。
快速路径的收益来源也由此清晰:parseStyleDeclarations与stripCSSComments原本的循环是逐Character遍历并append到新的String,对含大量 Unicode(日语、组合记号、emoji ZWJ 序列)的长 style 值而言,重建成本高;而真实页面里相当多的 style 值根本不含注释,此时"重建后逐字节相同"纯属浪费。guard ... else { return style }把这些无斜杠输入直接短路掉了。
三、基准协议:配对块、新鲜进程与先验证后计时
报告采用的测量方法学值得单独拆解,因为它体现了"微优化如何证明不是噪声"的标准做法。
硬件与构建。Linux x86_64,Swift 6.2.1;两侧都是"发布级"库,用-O -g -whole-module-optimization编译,且不带-enable-testing;每一对比对使用同一个单独编译的公开客户端(同一个 client 分别链接 reference 库与 candidate 库),排除客户端自身差异。CPU 亲和性固定到核心 0;每个对比对使用 3 次预热、全新进程、随机化的工作负载顺序、8 个平衡的 ABBA/BAAB 块——每个 revision、每个工作负载共 16 个进程。计时目标为较快 revision 单轮约 250 ms,两侧用相等的迭代次数(等迭代数而非等时长,避免"跑得久的一方摊薄固定开销"的偏差)。编译、测试与 profiling 均被排除在计时之外。
统计口径。指标为每次操作的对数几何平均毫秒数;置信区间是"探索性的配对块 95% t 区间",建在 log 比值上,未做多重比较校正;所有观测值全部保留,不剔除离群点。报告反复强调这些区间不建立设备级精度,也不代表其他机器。
工作负载定义。七个 CSS 工作负载实现在 benchmark_independent_hotspots.swift 中(L112-L132),与报告表格一一对应:
| 报告工作负载 | 基准名 | 内容 |
|---|---|---|
| Sanitize 64 short style values | css-short | 64 个短 style 值(含日语引号字符串)逐一走safeAttribute |
| Sanitize 64 long Unicode style values | css-long | 长 Unicode(日本語名e\u{301}👩🏽💻重复 32 次)暴露重建成本 |
| Long values with a late slash | css-slash | 长值且斜杠在末尾,专门覆盖快速路径的额外扫描代价 |
| Actual-comment control | css-comments | 真实注释(/*before*/co/*x*/lor:...),验证原有扫描器行为 |
| Clean and regenerate 64 styled paragraphs | clean-styled | 走公开Cleaner+ 重新生成 HTML |
| Parse, clean and regenerate styled document | parse-clean | 解析 + 清洗 + 重新生成 |
| Clean document without styles | clean-plain | 无 style 属性文档的清洗,作负向对照 |
报告特别说明:长 Unicode 字符串是刻意用来暴露重建工作的,其生产流量占比并未测量;清洗工作负载用的是公开Cleaner与重新生成的 HTML,而不是私有 helper 专用基准或浅拷贝替代。
先验证、后计时。compare_independent_hotspots.py 是执行整套协议的 Python 工具(依赖 NumPy/SciPy)。它的关键机制在源码中可以直接核对:
- 计时前先用同一个 client 分别以 A/B 两个库运行
--verify,把两侧完整输出逐字节比对,不一致就拒绝基准(L42-L44); - 校准阶段决定每个工作负载的迭代次数(
iterations = ceil(ms*1e6 / min speed),下限 4、上限 2,000,000),即两侧等迭代数; - 每个块内工作负载顺序随机洗牌,但 A/B 顺序严格走
ABBA/BAAB(L69-L74),抵消单调漂移; - 每次计时都校验 checksum 必须等于
expected * iterations,防止结果漂移; - 输出
manifest.json(client/库 SHA-256、平台、参数)、calibration.json、raw.jsonl(全部原始试验)与summary.json(几何均值与 t 区间)。
四、隔离发布验证:对照 PR #3 参考基线
第一张表是隔离归因数据:参考方是"已经过优化的 PR #3 源码"(c9c37c80eb571aabba59a1f3ef31af9b072af4a8),候选方是同一源码加上只有这一个运行时改动。报告明确说明:这些数字不是对 master 的测量。
| 公开工作负载 | 参考 ms | 候选 ms | 时间减少 | 95% 区间 |
|---|---|---|---|---|
| Sanitize 64 short style values | 3.117449 | 2.844915 | 8.74% | 6.59–10.84% |
| Sanitize 64 long Unicode style values | 18.401358 | 13.572643 | 26.24% | 24.77–27.69% |
| Long values with a late slash | 12.041063 | 12.105335 | -0.53% | -2.55–1.44% |
| Actual-comment control | 2.460368 | 2.431834 | 1.16% | -4.63–6.63% |
| Clean and regenerate 64 styled paragraphs | 4.050292 | 3.720846 | 8.13% | 5.71–10.50% |
| Parse, clean and regenerate styled document | 4.643244 | 4.359176 | 6.12% | 0.51–11.41% |
| Clean document without styles | 0.615912 | 0.596120 | 3.21% | -2.34–8.47% |
解读要点:
- 收益集中在长 Unicode 输入(26.24%):这正是逐 Character 重建代价最大的场景;短值收益 8.74%,来自省去整条扫描+重建路径的固定开销;
- late-slash 控制为 -0.53%,区间跨零:证实了报告声明的取舍——含斜杠输入不获益,甚至可能略增开销,但统计上不构成显著变慢;
- 真实注释控制仅 1.16%、区间跨零:含注释输入走的是未改动的原扫描器路径,符合"行为不变"的预期;
- 无 style 文档 3.21%、区间跨零:负向对照没有显示可归因的收益。
报告中另有一句方法论备注:先前一次独立的 4 块/120 ms 筛查被单独保留、不与本表合并(not pooled);--aa模式(A/A 运行)被用于噪声标定而非与 A/B 结果混算。
五、对新 master 的合并确认:同样的收益,但不是隔离归因
master 前移后(参考5d593583469c49d3c84449f28251b4cf201042a8),报告做了第二轮确认。注意这里候选方 = master +两个补丁(本 CSS 补丁 + 独立的 sibling-position 补丁),新 client 对当前参考编译一次后在整个对比对内保持不变,协议相同(8 块/250 ms):
| 公开工作负载 | 参考 ms | 合并 ms | 时间减少 | 95% 区间 |
|---|---|---|---|---|
| Short styles | 3.064962 | 2.827195 | 7.76% | 5.24–10.21% |
| Long Unicode styles | 18.382911 | 13.342432 | 27.42% | 26.83–28.00% |
| Late-slash control | 11.672791 | 11.894930 | -1.90% | -6.30–2.31% |
| Actual-comment control | 2.402561 | 2.392114 | 0.43% | -1.34–2.17% |
| Clean styled document | 4.004747 | 3.718265 | 7.15% | 5.20–9.06% |
| Parse and clean styled document | 4.604337 | 4.281462 | 7.01% | 3.88–10.05% |
| Clean document without styles | 0.633303 | 0.623906 | 1.48% | -3.42–6.16% |
这张表证明收益在 master 前移后依然成立且量级一致(长 Unicode 27.42% vs 26.24%;清洗类工作负载 7% 上下),但报告如实标注:这是合并确认(combined confirmation),不是隔离归因——无法把 7% 的清洗收益完全划给 CSS 补丁单独一份。Late-slash 控制的 -1.90%(区间跨零)再次落在"无显著差异"一侧。
六、噪声标定与诚实的限制声明
性能报告最容易翻车的地方是把主机噪声当成收益。这份报告用 A/A 矩阵(同一二进制跑两遍)直接量化了噪声,并保留了全部记录:
- 初始 6 块、200 ms 的 A/A 矩阵出现1 个假阳性:完全相同的二进制让"无 style 清洗"看起来快了 2.89%(区间 1.23–4.52%,不跨零);
- 独立的 12 块、400 ms 复查给出 -0.40%(区间 -2.64–1.80%,跨零);其 late-slash 控制为 -0.83%(区间 -2.49–0.80%,跨零);
- 当前 master 的独立 A/A 矩阵中所有区间均跨零。
由此得出的结论被写成硬性限制:"不要把小型控制差异解读为加速;这台共享主机有噪声,区间不建立设备级精度;没有整体 Manabi、通用解析器、Apple 设备或 iPhone 的性能声明;没有分配次数或 profiler 来源的计时声明。"
报告同时给出了可复核的完整性证据:旧参考、两个隔离候选、旧合并候选、当前 master 与当前合并候选在 2,108 条完整公开观测记录上逐字节一致(含 CSS 结果、未变化输入、清洗后文档、结构选择器、变更与重新生成的 HTML/文本),SHA-256 为204a9bfdfa9f50a4789b7ce2f0946be5ae482269a6ec9abec7ec2b7ab853c775;完整双候选研究保留 11 个阶段、1,760 个计时新鲜进程,每个计时 checksum 与聚合值都独立重算,聚合 JSON SHA-256 为067a43e72434b422f505444267777a594bb8aa9b66e18f259a798d920915218b。报告也提醒:这些逐字节一致记录是行为一致性证据,不是独立 bug 或浏览器等价性检查。
七、正确性保障:六个回归方法与全量测试矩阵
快速路径最大的风险是改变清洗语义。仓库中新增的 CSSCommentFastPathTest.swift 对应报告所述的六个回归方法,逐一覆盖:
testSlashFreePropertiesStillFilterAndNormalize:无斜杠输入的属性过滤与归一化不变(大小写属性名、未允许属性丢弃、calc()括号内的分号不误切);testUnicodeBytesAndQuotedEscapesArePreserved:日语、组合记号、emoji ZWJ 序列、NBSP、转义引号、形近斜杠(∕/⁄)等字节在引号内容中逐字节保留(Array(...utf8)相等断言);testSlashFreeUnsafeValuesRemainRejected:无斜杠的 unsafe 值仍被拒绝——大小写/空白/换行伪装的expression(...)、@import、url((含u\u{00A0}rl(x))以及behavior/-moz-binding;testCommentsAndCommentLikeQuotedContentRetainOldScanner:含斜杠输入保持旧扫描器行为——注释剥离、未闭合注释、引号内的a/*not-comment*/b与a/b原样保留、exp/*x*/ression(alert(1))经空白折叠后仍被拦截;testSeededSlashFreeStylesAgreeWithForcedCommentScanner:512 个种子化用例中,直接清洗结果与"强制走原扫描器"(在输入前加可被移除的/**/前缀)的结果逐例相等——这是快速路径与原路径等价的直接证明;testCleaningAndRepeatedRuleChangesDoNotMutateInput:经Cleaner清洗、白名单规则变更(removeCSSProperties)重复执行,输入文档不被修改。
报告声明未削弱任何既有测试,并列出全量测试矩阵(17 个可选基准跳过,零失败):
| 运行 | 编译器/配置 | 用例数 |
|---|---|---|
| 原 PR #3 发布版 | — | 806 |
| PR #3 不变 + 两个新测试文件 | — | 821 |
| 合并候选 | 本地 debug 与 release | 821(同左) |
| 当前 master + 两个测试文件、生产代码不变 | 原生 Swift 6.1.3 release | 1,080 |
| 本独立主干生产/测试树 | 原生 Swift 6.1.3 release | 1,071(1,054 通过) |
| master + 两个运行时补丁 | 本地 Swift 6.2.1 全量 DEBUG + AddressSanitizer,ASAN_OPTIONS=detect_leaks=0 | 1,080 |
报告特别说明 ASan 运行关闭了泄漏检测,因此不是泄漏检查,也不是 release-ASan 声明。
八、复现指南
仓库只读,以下命令来自报告 Reproduction 一节,用于在自己的环境中复现验证。
1. 原生测试。在仓库根目录:
swift test -c release2. 构建发布级库。隔离计时时,只把本运行时 diff 应用到所述 PR #3 参考源;对新 master 确认时,把两个运行时 diff 都应用到所述 master。每个对比对内 client 与源码版本必须匹配。在每个独立的源码目录中,Linux 上按报告给出的方式构建(Package.swift 显示该包为 swift-tools-version 6.0,库 target 即Sources全量源码):
mkdir -p shipping swiftc -swift-version 6 -O -g -whole-module-optimization \ -emit-library -emit-module -module-name SwiftSoup \ -emit-module-path shipping/SwiftSoup.swiftmodule \ -o shipping/libSwiftSoup.so Sources/*.swift3. 编译一个对比 client。对 reference 编译一次即可,整个对比对复用:
swiftc -swift-version 6 -O -I /reference/shipping -L /reference/shipping \ -lSwiftSoup Tools/benchmark_independent_hotspots.swift -o /tmp/hotspots4. 运行对比与 A/A 标定。Python 依赖 NumPy/SciPy:
python3 Tools/compare_independent_hotspots.py --client /tmp/hotspots \ --baseline-library /reference/shipping --candidate-library /candidate/shipping \ --suite css --output /tmp/new-css-results --blocks 8 --ms 250要点(均可在 compare_independent_hotspots.py 中核对):
--suite css运行上表七个工作负载(--suite position是另一个 sibling-position 研究用的套件);--blocks(≥2)控制 ABBA/BAAB 块数,--ms(≥1)是较快一侧单轮计时目标;--cpu指定亲和核心(默认 0);- 追加
--aa并写入另一个全新目录,即 A/A 噪声标定(此时 A/B 两侧链接同一库); - 运行器在计时前比对两侧完整非空输出、校验每个 checksum、控制 Linux 亲和性与
LD_LIBRARY_PATH/DYLD_LIBRARY_PATH库加载,并保存校准、原始试验、二进制身份(SHA-256 manifest)与区间; - 计时期间绝不编译或 profiling;完整源码、补丁、原始计时记录与测试日志随交付存档提供。
九、结论:微优化报告的"完整证据链"范本
这份 independent-css-20260909.md 记录的优化本身只有三行——Sources/Whitelist.swift里stripCSSComments开头的一条guard style.utf8.contains(0x2F) else { return style },利用"CSS 注释必须含 ASCII 斜杠"这一语言事实,在无注释的 style 值上跳过逐 Character 重建。其可核验的收益画像非常一致:长 Unicode style 值清洗减少约 26–27%,短值与文档级清洗减少约 6–8%,而 late-slash、真实注释与无 style 三个控制均落在跨零区间。
从源码结构看,这套方法学对任何"在高频字符串路径上加提前返回"的改动都通用:(1) 用字节级谓词(而非语义级扫描)做廉价守卫,且守卫的否定条件必须精确(只认0x2F,不认形近字符);(2) 用与原路径等价的种子化用例证明行为不变(512 例 + 2,108 条逐字节一致的公开记录);(3) 用 A/A 矩阵量化主机噪声,明确声明哪些差异"不得解读为加速";(4) 把构建命令、client 复用规则、协议参数全部落到可执行命令,使第三方可以复算而不是只能转述。
- 网页爬虫
- 后端
【免费下载链接】SwiftSoup
SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)
相关推荐
pstack 的 /unslop Codex 斜杠桩文件:如何用一份 AI 腔清除规则集清洗 Agent 写作
pstack 的 /unslop Codex 斜杠桩文件:如何用一份 AI 腔清除规则集清洗 Agent 写作 pstack 把每个公开 skill 生成为各运
人工智能AI 技能AI 插件开发工具如何快速掌握 CSS Scope Inline:纯CSS作用域终极指南
如何快速掌握 CSS Scope Inline:纯CSS作用域终极指南 CSS Scope Inline 是一个仅16行的轻量级JavaScript库,让你能够
Left编辑器完全指南:从安装到精通的无干扰写作工具
Left编辑器完全指南:从安装到精通的无干扰写作工具 Left编辑器是一款专注于提升写作效率的 无干扰写作工具 ,专为需要深度专注的作家、程序员和内容创作者设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考