DOMParser 不保留重复属性,首个重复属性在 Tokenizer 阶段即被静默丢弃;html2text 因需遍历原始属性列表而受重复属性拖慢,预处理正则清理或 monkey patch 用 Map 去重可优化性能。

重复属性在 DOMParser 中根本不存在
DOMParser 不会保留重复属性,你在 HTML 字符串里写的 <div id="a" id="b">,解析后 el.id 只返回 "b",el.attributes.getNamedItem('id').value 也只返回 "b"。第一个 id="a" 在 Tokenizer 阶段就被静默丢弃了——它没进 DOM 树,更谈不上“哈希映射瓶颈”或“去重逻辑”。你用 Array.from(el.attributes) 或 new Map(el.attributes) 拿到的,永远只是最终生效的那一组属性。
html2text 为什么会被重复属性拖慢
html2text 不走浏览器解析路径,它把原始标签字符串里的属性当列表接收。比如 class="a" class="b" 会变成 [['class', 'a'], ['class', 'b']],后续所有 CSS 类提取、语义判断、文本归一化逻辑都得遍历整个列表。重复越多,耗时越接近线性增长。
- 预处理最稳:用正则全局清理,例如
html.replace(/(\s+[^\s=]+)\s*=\s*["'][^"']*["'](\s+\1\s*=\s*["'][^"']*["'])+/g, '$1=$2') - monkey patch 更直接:在
handle_starttag开头加一行attrs = Array.from(new Map(attrs).entries()) - 别在循环里拼接含重复属性的字符串再喂给
html2text——这种“假批量”会让性能问题翻倍
需要原始属性序列时只能靠正则提取
如果你的任务依赖原始声明顺序或重复次数(比如模板审计、合规检查、指纹生成),必须在调用 DOMParser 之前下手。DOM 树一旦建好,历史就不可逆丢失。
- 用
/<[^\>]*>/g提取所有标签字符串,再对每个匹配做属性解析 - 高危模式可提前拦截,例如
/id\s*=\s*["']([^"']+)["'].*id\s*=\s*["']([^"']+)["']/s - 避免用
innerHTML或DOMParser先建树——此时已晚
生成稳定属性指纹该用 Map 而不是 Object
当你基于最终生效属性生成缓存 key 或 diff 判定时,顺序一致性很关键。用 Object 存属性,数字键会提前出现({1:'a', id:'b'} → ['1', 'id']),导致指纹错乱;Map 严格保序,且 keys() 返回确定性迭代器。
立即学习“前端免费学习笔记(深入)”;
- 推荐写法:
Array.from(attrs.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join('|') - 这个指纹反映的是浏览器实际渲染所依据的属性,和原始 HTML 的重复声明无关——这是规范行为,不是 bug
Map 稳定建模,要么在解析前用正则锁定原始字符串——二者不可混用。



















