- 网页爬虫
- 后端
【免费下载链接】SwiftSoup
SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)
本篇技术指南围绕 SwiftSoup 仓库中的基准报告 entity-escaping-20260909.md 展开,讲解 SwiftSoup 的 HTML 实体转义(entity escaping)助手函数如何从"逐序列写入"升级为"按相邻透传序列批量拷贝",以及这项纯行为保持(behavior-preserving)优化是如何通过 6 个独立回归测试、逐字节输出比对和 ABBA 块状基准协议得到验证的。读完后,你将掌握Sources/Entities.swift中escapeFastAscii的批量透传实现原理、配套回归测试的字节预言机(byte oracle)设计,并能用仓库自带的swiftc+ Python 运行脚本在本地复现整套测量流程。
一、改动本体:相邻透传 Unicode 序列的批量拷贝
该优化针对的是 UTF 输出路径下的转义助手:它原来会为每一个非 ASCII 序列单独调用一次内部缓冲区写入器;现在改为把相邻的透传(pass-through)序列一次性拷贝到输出缓冲区。报告的边界声明非常明确,这些行为完全不变:
- ASCII 替换规则(
&、<、>、"的实体替换)不变; - NBSP(U+00A0)替换不变(HTML 模式下替换为
,XHTML 模式下替换为 ); - 自定义公开替换回调(即调用方覆写
StringBuilder的append路径)及其调用顺序不变; - 停止位置仍然落在相同的字节边界上;对畸形/截断字节保留历史性的宽容 lead-byte 步进(permissive lead-byte stepping),没有悄悄换成会校验的 UTF-8 解码器;
- 不引入任何新缓存、新状态、新公开 API 或规范化策略。
也就是说,这是一次纯粹的写放大(write amplification)削减:连续非 ASCII 内容(例如一长串日文、表情符号、组合标记)在旧实现中会触发多次小的写入调用,新实现合并成一次memcpy式的整段拷贝,而字节级输出逐位不变。
1.1 源码定位:escapeFastAscii 的主循环
仓库中的对应实现位于 Sources/Entities.swift 的escapeFastAscii私有函数。公开入口是 Entities.escape(::),内部按ByteSlice重载分发;当输出编码器是 UTF-8/UTF-16/Unicode 且不需要空白规范化时,会路由到该快速路径(见 ByteSlice 重载中的路由判断)。主循环对每个字节分三种情况处理:
- ASCII 段:查 128 字节替换表,命中则追加对应实体(
&/</>/"),未命中则通过"扫描到下一个需处理字节"的游标把整段安全 ASCII 一次性write进缓冲区; - NBSP 两字节序列:
C2 A0命中时输出 或 ,单独处理; - 其余非 ASCII 序列:按 lead byte 推算序列宽度(1/2/3/4),进入下面新增加的批量透传逻辑。
1.2 批量透传的核心代码
优化后的透传分支(Sources/Entities.swift)可以概括为:
// Copy adjacent non-ASCII sequences together. Keep the existing // lead-byte stepping, including for malformed/truncated UTF-8. var runEnd = end while runEnd < count { let next = base[runEnd] if next < asciiUpperLimitByte { break } // 遇到 ASCII 即停止 let nextEnd = min(runEnd &+ utf8CharLength(for: next), count) if nextEnd &- runEnd == 2 && next == StringUtil.utf8NBSPLead && base[runEnd &+ 1] == StringUtil.utf8NBSPTrail { break // 遇到 NBSP 即停止(它要替换) } runEnd = nextEnd } accum.write(contentsOf: base.advanced(by: i), count: runEnd &- i)三个关键细节与报告一一对应:
utf8CharLength(for:)(Sources/Entities.swift)按 lead byte 的取值区间(<128→1、<0xE0→2、<0xF0→3、否则 4)推断序列宽度。对畸形或截断字节,这个推断只是"宽容地向前跨步",与优化前的旧行为完全一致——报告特别强调这一点,避免读者误以为引入了 UTF-8 校验;- 扫描在遇到 ASCII 字节或 NBSP 两字节序列时立即停止,保证替换回调的触发点与顺序不变;
- 整段透传最终通过 StringBuilder.write(contentsOf:count:) 一次写入,替代了原来逐序列的多次写入。
二、行为保持验证:6 个独立回归测试
报告称新增 6 个回归方法,与仓库中 Tests/SwiftSoupTests/EntityEscapeRunTest.swift 完全吻合:
| 测试方法 | 覆盖点(对应报告描述) |
|---|---|
testUnicodeRunsAcrossSettingsAndStorage | 组合标记与 emoji、base/extended/XHTML 三种EscapeMode、UTF-8/UTF-16 输出设置、attribute/text 上下文,并遍历 array、offset-array、Data、非零索引Data、指针五种存储形态 |
testMalformedAndTruncatedRuns | 2,048 条带种子的畸形字节串(截断长度 0~64),全部走转义路径 |
testEveryLeadByteAtBoundaries | 每一个字节值(0~255)作为 lead byte 出现在截断边界,7 种后缀组合 |
testASCIIAndUnchangedPaths | 纯 ASCII 与免转义快路径不受影响,含空白规范化参数组合 |
testReplacementCallbacksAndSnapshots | 自定义append回调的调用序列、builder 别名(alias)与保留快照(retained snapshot)不被破坏 |
testRegeneratedTextAndAttributes | 无原始 HTML 来源、重新生成的文档(source-less regenerated HTML)中 text 与 attribute 上下文输出正确 |
这套测试的骨架是一个独立字节预言机(EntityEscapeRunTest.swift 中的reference方法):它不复用生产代码,而是用几十行独立逻辑按历史规则重新实现转义——ASCII 按 38/60/62/34 四个替换点处理(<在 attribute 且 XHTML 模式、"仅在 attribute 下替换),非 ASCII 按224/240阈值推断宽度并原样拷贝,C2 A0替换为 / 。生产输出必须与该预言机逐字节相等。测试还刻意构造了"非零基址"输入(在字节前后垫上[255, 34, 60]/[38, 255]再取切片)以及ByteSlice指针存储,确保优化没有隐含任何"数组从头开始"的假设;RecordingBuilder子类则逐条记录append回调收到的内容,验证替换回调顺序(&、 、<、")与快照隔离(见 EntityEscapeRunTest.swift)。
三、性能测量结果
3.1 独立确认:目标 500 ms/进程,fixture 大小 128
三个目标工作负载均为合成输入(报告强调它们暴露的是该助手函数,而非真实流量分布):
escape-japanese:长连续日文串加一个必须替换的&;escape-mixed:日文、ASCII、emoji、组合标记、NBSP 与标记(markup)交错;serialize-japanese:重新生成一个 128 段落的无来源文档,每段含 text 与 title 属性,不复用原始 HTML 来源。
这些输入在 Tools/benchmark_entities.swift 中直接可见(8 条变体输入防止循环展开干扰),serialize-japanese的无来源文档构造见 benchmark_entities.swift。测量结果(几何均值毫秒/次,正数表示候选更快):
| Workload | Reference ms | Candidate ms | Less time | 95% interval |
|---|---|---|---|---|
| escape-japanese | 0.020657 | 0.011167 | +45.94% | +44.59 to +47.26% |
| escape-mixed | 0.036759 | 0.030078 | +18.17% | +14.68 to +21.52% |
| serialize-japanese | 0.242221 | 0.150415 | +37.90% | +36.56 to +39.22% |
| escape-plain | 0.004125 | 0.004108 | +0.41% | -2.43 to +3.17% |
3.2 小/ASCII 对照组:不宣称 ASCII 有收益
12 个块的测量中,初步中位数曾提示一个很小的 ASCII 变慢,但更长的重复测量及其 A/A 对照都未能确认这一点。已经安全的字符串保留既有的免转义快速路径(即ByteSlice重载中基于memchr的needsEscape预判,见 Sources/Entities.swift)。
| Workload | Reference ms | Candidate ms | Less time | 95% interval |
|---|---|---|---|---|
| escape-ascii | 0.015277 | 0.015022 | +1.67% | -0.75 to +4.02% |
| escape-short | 0.000313 | 0.000310 | +1.10% | -0.85 to +3.01% |
3.3 Reader 对照:不做通用应用层声明
两项实体改动合并后,还在 8 个平衡块、每修订每工作负载 16 个全新进程、目标 400 ms/进程的设置下,与未改动的 PR #3 基线做了对比(沿用中等/字节输入的 Reader fixture;深拷贝注入被真实测量,浅拷贝路径仅作相等性哨兵;这些 SwiftSoup 侧回放排除了形态分析、Realm/词典 I/O、WebKit 与 UI):
| Workload | Reference ms | Candidate ms | Less time | 95% interval |
|---|---|---|---|---|
| manabi-parse | 3.745240 | 3.801148 | -1.49% | -4.53 to +1.46% |
| manabi-injection-deep | 23.271460 | 22.932565 | +1.46% | -1.36 to +4.19% |
| manabi-ruby | 5.326387 | 5.349208 | -0.43% | -1.70 to +0.83% |
| manabi-inner | 0.018072 | 0.017880 | +1.07% | -0.89 to +2.98% |
所有 Reader 区间的置信区间都横跨零。因此报告明确不主张:整体 Reader 加速、通用解析器收益、Apple 设备上的性能结果或 iPhone 结果。两个成功的助手级(helper-specific)结果不改变这一结论——这是该报告最值得学习的克制:微基准显著 ≠ 端到端收益,必须用应用层 workload 单独验证并如实报告"区间跨零"。
四、测量协议与统计方法
协议细节(全部记录于原文档,值得在复现任何 Swift 微基准时直接借鉴):
- 环境:Swift 6.2.1、Linux x86_64、CPU 0 亲和性绑定、
-O -g -whole-module-optimization -swift-version 6出货库,且不带-enable-testing;同一个编译优化的公开客户端动态加载各版本库。fixture 准备、编译、全量输出校验、sanitizer 测试与 profiling 全部在计时区间之外;每个计时循环前有 3 次进程内预热。哈希按 Swift 默认随机化,不刻意固定为确定性以保证计时真实性; - 确认轮:12 个平衡 ABBA/BAAB 块,即每个修订、每个工作负载 24 个全新进程。每块对每个修订取两次对数时间的均值;表格报告几何均值毫秒/次,以及"对数时间比"的成对块 Student t 95% 区间(换算为"节省时间百分比")。报告同时声明:这是探索性区间,未做多重比较校正,也不构成对其他机器或生产流量的预测。墙钟与进程 CPU、校准记录、全部原始进程记录、精确命令/库/客户端/源码哈希均被保留,不删除任何样本或离群点;
- 初筛与对照:初筛(6 块、每修订每工作负载 12 进程)单独保留,不与确认轮选择性混池;目标与 ASCII A/A 对照用 8 块,Reader A/A 用 6 块。所有墙钟 A/A 区间都包含零;多实体点(multipoint-entity)的 A/A 区间特别宽,因为随机化的字典顺序会影响基线线性扫描——报告明确"不隐藏这一不确定性"。
五、行为学验证:测试套件、逐字节一致性与 ASan
- release 测试套件:在完全相同的 PR #3 构建输入上,未改动的参考库 + 两个新测试文件通过原生 release 套件:818 用例,801 通过,17 个 opt-in benchmark 跳过,0 失败;每个隔离候选库 + 自己的 6 个新测试通过812 用例,795 通过,17 跳过,0 失败。新测试对未改动的生产源码同样通过——这证明改动是行为保持的优化而非正确性修复,且未删除或削弱任何既有测试;
- 逐字节一致性:四个出货变体(参考、仅转义、仅查找、两者)的输出与基线逐字节一致:9,408 条完整 DOM 记录、4,608 条上下文片段记录、72 个非空 Reader 输出集合。DOM 输出 SHA-256 为
c243b0307f2fbfaeb781bd5786ac51e9e66f659c165b9f33f1590345416b6ca1,片段输出 SHA-256 为1c582c7608ddef757b25c57b270e4e63460a21a871025db35dcb60edf5585a98; - ASan:合并后的生产候选库通过完整原生 AddressSanitizer release 套件(818 用例、801 通过、17 跳过、0 失败),使用
ASAN_OPTIONS=detect_leaks=0——报告明确声明这不是泄漏检查。
六、可移植复现步骤
改动建立在 fork mastercb57c9e160da093c7dd87eb91d335b6891d0efb7之上,数值对比对象是已优化的 PR #3(c9c37c80eb571aabba59a1f3ef31af9b072af4a8)与其"仅叠加本改动"的候选树;参考 Sources Git tree 为98e22c134afa5144fe64565f448260239545f90f,两个修订版本的原始Entities.swift完全相同(blob9a8e292e58323cedb86e834cc24c8bf14af89739)。PR #1~#7 已按作用域与变更文件清单审计,均未改动 Entities;后续复查包含正则 PR #8,它只改 Pattern。两个实体 PR 改的是不同助手、可独立评审,且两个 Tools 文件刻意保持字节一致,合并任意顺序后共享同一套可移植 harness(本项的姊妹报告见 entity-decoding-20260909.md)。
要精确复现报告中的测量,在c9c37c80eb571aabba59a1f3ef31af9b072af4a8上建两个 detached worktree,然后只把本 PR 的Sources/Entities.swiftdiff(相对 master)应用到候选端——不要把另一个实体改动混入隔离对比;若改测 master 本身,请用两个 master 基的 worktree 并标注为新的实验。设BASE、CANDIDATE指向两个 worktree,OUT为全新的绝对路径构建目录:
mkdir -p "$OUT/base" "$OUT/candidate" ext=so [ "$(uname -s)" = Darwin ] && ext=dylib for variant in base candidate; do root="$BASE" [ "$variant" = candidate ] && root="$CANDIDATE" swiftc -swift-version 6 -O -g -whole-module-optimization \ -emit-library -emit-module -module-name SwiftSoup "$root"/Sources/*.swift \ -emit-module-path "$OUT/$variant/SwiftSoup.swiftmodule" \ -o "$OUT/$variant/libSwiftSoup.$ext" done swiftc -O -g -I "$OUT/base" -L "$OUT/base" -lSwiftSoup \ Tools/benchmark_entities.swift -o "$OUT/benchmark-entities"然后运行以下命令,再用--aa和不同的输出前缀重复一次作为 A/A 对照(Tools/compare_entity_benchmarks.py 会在计时前比对两份完整公开输出、检查计时校验和,并拒绝覆盖已有运行目录;在 Linux 上固定一个允许的 CPU,在 macOS 上记录无法绑定亲和性):
python3 Tools/compare_entity_benchmarks.py \ --baseline-library "$OUT/base" --candidate-library "$OUT/candidate" \ --client "$OUT/benchmark-entities" --blocks 12 --ms 500 --size 128 \ --workloads escape-japanese escape-mixed serialize-japanese escape-plain escape-ascii escape-short \ --output "$OUT/escape-ab" # Repeat with --aa and --output "$OUT/escape-aa".计时期间不要与编译、sanitizer 运行或 profiler 并发执行。项目常规正确性检查仍是swift test -c release以及(在支持的平台)ASAN_OPTIONS=detect_leaks=0 swift test -c release --sanitize address;该基准不修改Package.swift,也不新增依赖。
七、被测产物身份与适用前提
- 参考库 SHA-256:
97486712c374feabf95d22d9ae8564ee427e80e210388bee7bf963231ada16f9 - 候选库 SHA-256:
8224610433f55df4fa9d408d02a750dcbc5de43f8e0cc503d3826e42aa06c99f - 两端相同客户端 SHA-256:
22672ec15224612f36814f41a8ac792c7d24c550f903f66da8c2b15cb9e51154 - 候选
Entities.swiftGit blob:8dc30c59cabd7d9156a5bab3e92539e0682ec93d - 运行时补丁 SHA-256:
e7d0ee7f5df9441a37397ed089ee0bcbc54504b8f061a17a2f519b2c27928e71
最后归纳适用前提,避免误读:本优化的收益上限由"输入中连续非 ASCII 透传段的长度"决定——纯 ASCII 与短字符串走既有快路径,收益可忽略(对照组区间跨零);合成工作负载证明了助手级收益(escape-japanese 约 46%、serialize-japanese 约 38%),但 Reader 端到端对照区间全部跨零,不能据此宣称解析器整体加速或任何 Apple 设备上的表现。完整的原始 screen/confirmation/A/A 记录、CPU 数据、校准、fixture 与源码清单、失败/中性观察、精确源码快照、测试日志与完整输出集随配套交付包保留,且报告明确警示:不要用其他早期报告替代本实验的结论。
- 网页爬虫
- 后端
【免费下载链接】SwiftSoup
SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)
相关推荐
AltStore终极指南:iOS侧载应用商店技术深度解析与实践
AltStore终极指南:iOS侧载应用商店技术深度解析与实践 AltStore是一款专为非越狱iOS设备设计的第三方应用商店,通过巧妙的技术手段实现应用侧载功
移动开发SwiftSoup 实体解码优化:复用多点实体字典的下标查找与基准验证方法
SwiftSoup 实体解码优化:复用多点实体字典的下标查找与基准验证方法 本篇围绕 Benchmarks/entity decoding 20260909.m
网页爬虫后端Portall用户指南:轻松实现端口冲突解决与跨IP迁移
Portall用户指南:轻松实现端口冲突解决与跨IP迁移 Portall是一款功能强大的端口管理系统,专为开发者和系统管理员设计,帮助他们轻松解决端口冲突问题并
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考