如何将 HTML 结构解析为文本与标记分离的嵌套对象数组

1次阅读

如何将 HTML 结构解析为文本与标记分离的嵌套对象数组

本文介绍一种基于递归遍历 dom 树的专业方法,将任意 html 片段准确拆解为按渲染顺序排列的 {text: “…”} 和 {markup: “”} 对象数组,完美处理嵌套、兄弟节点及闭合标签位置问题。

本文介绍一种基于递归遍历 dom 树的专业方法,将任意 html 片段准确拆解为按渲染顺序排列的 `{text: “…”}` 和 `{markup: ““}` 对象数组,完美处理嵌套、兄弟节点及闭合标签位置问题。

在前端开发中,有时需要对 HTML 内容进行语义化结构分析——例如实现富文本编辑器的内容序列化、无障碍辅助解析、或自定义 Markdown/HTML 混合渲染器。核心挑战在于:必须严格保持 DOM 渲染时的节点顺序,同时区分纯文本内容(Text 节点)与 HTML 标记(Element 节点),尤其当元素存在嵌套或相邻兄弟元素时,闭合标签的位置极易出错。

使用 TreeWalker 或线性遍历 childNodes 的迭代逻辑容易陷入边界判断困境(如父元素末尾闭合时机、子元素与文本混排时的插入顺序)。而递归深度优先遍历(DFS)天然契合 DOM 树结构,能自动保证:

  • 开始标签在子内容之前;
  • 所有子节点(含文本与嵌套元素)被完整处理;
  • 结束标签在子内容之后 —— 完全符合 HTML 渲染流。

以下为简洁、健壮、可直接复用的实现方案:

function parseHtmlToTokenArray(rootNode) {   const tokens = [];    function walk(node) {     for (const child of node.childNodes) {       if (child.nodeType === Node.TEXT_NODE) {         const text = child.textContent.trim();         // 可选:跳过纯空白文本(提升结果纯净度)         if (text.length > 0) {           tokens.push({ text });         }       } else if (child.nodeType === Node.ELEMENT_NODE) {         // 推入开始标签(小写化确保规范)         tokens.push({ markup: `<${child.tagName.toLowerCase()}>` });         // 递归处理所有子节点         if (child.hasChildNodes()) {           walk(child);         }         // 推入结束标签         tokens.push({ markup: `</${child.tagName.toLowerCase()}>` });       }       // 忽略注释、CDATA 等其他节点类型(如需支持可扩展)     }   }    walk(rootNode);   return tokens; }  // 使用示例 const htmlString = `   <h2 id="mcetoc_1h1m1ll27l">Lorem ipsum dolor sit amet...</h2>   <p>Lorem ipsum...<a href="#">tr</a><a title="titulo">adsf afjdasi k</a></p><div class="aritcle_card flexRow">                                                         <div class="artcardd flexRow">                                                                 <a class="aritcle_card_img" href="/ai/1496" title="PhotoAid Image Upscaler"><img                                                                                 src="https://img.php.cn/upload/ai_manual/000/969/633/68b7a456e0374854.png" alt="PhotoAid Image Upscaler"  onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>                                                                 <div class="aritcle_card_info flexColumn">                                                                         <a href="/ai/1496" title="PhotoAid Image Upscaler">PhotoAid Image Upscaler</a>                                                                         <p>PhotoAid出品的免费在线AI图片放大工具</p>                                                                 </div>                                                                 <a href="/ai/1496" title="PhotoAid Image Upscaler" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>                                                         </div>                                                 </div><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p> `; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlString; const result = parseHtmlToTokenArray(tempDiv); console.log(result);

输出效果示例(节选):

[   {"markup": "<h2>"},   {"text": "Lorem ipsum dolor sit amet..."},   {"markup": "</h2>"},   {"markup": "<p>"},   {"text": "Lorem ipsum..."},   {"markup": "<a>"},   {"text": "tr"},   {"markup": "</a>"},   {"markup": "<a>"},   {"text": "adsf afjdasi k"},   {"markup": "</a>"},   {"markup": "</p>"} ]

⚠️ 关键注意事项:

  • 空格与换行处理:原始 textContent 包含 HTML 中的空白符(如换行、缩进)。生产环境建议调用 .trim() 或使用 innerText(注意其会触发布局计算);若需保留格式,可改用 nodeValue 并预处理 ntr。
  • 属性完整性:当前方案仅生成基础标签名(如 ),不包含属性。如需还原完整 outerHTML,可替换为 child.outerHTML,但需注意: 会被整体推入一次,破坏“开–内容–闭”原子结构。此时应先提取起始标签(正则或 child.cloneNode(false).outerHTML),再单独处理闭合。
  • 性能考量:对于超大文档(>10k 节点),递归可能触发溢出。可改用显式栈的迭代 DFS(维护 {node, state: ‘enter’|’exit’} 元组),但绝大多数 CMS 或编辑器场景无需优化。
  • 安全边界:本函数操作的是已解析的 DOM 节点,不执行 HTML 字符串解析,因此xss 风险;但若输入源自不可信字符串,请务必先通过 DOMPurify.sanitize() 等库净化。

该方案以最小认知负荷达成最高结构保真度——它不试图“修复”DOM,而是忠实反映浏览器引擎的渲染遍历逻辑。掌握此模式,你将能轻松构建 HTML 解析中间层,为内容分析、序列化、差异对比等高阶功能奠定坚实基础。

text=ZqhQzanResources