当前位置:首页 > 文章列表 > 文章 > 前端 > X-ray解析HTML为对象数组技巧

X-ray解析HTML为对象数组技巧

2026-02-13 11:03:41 0浏览 收藏

哈喽!今天心血来潮给大家带来了《X-ray 解析非嵌套 HTML 为对象数组方法》,想必大家应该对文章都不陌生吧,那么阅读本文就都不会很困难,以下内容主要涉及到,若是你正在学习文章,千万别错过这篇文章~希望能帮助到你!

如何使用 X-ray 解析非嵌套 HTML 结构为对象数组

X-ray 本身不支持直接抓取兄弟节点构成的逻辑区块,但可通过 jsdom 预处理 HTML,将散列的 `

` 及其后续同级内容动态包裹为独立容器,再用 X-ray 按标准父子结构解析,最终得到符合预期的 sections 数组。

在实际网页爬虫场景中,常遇到类似文档式 HTML:多个

标题平级排列,各自后跟可选的

副标题和
    列表,但无统一父容器分隔各节。这种“非嵌套、靠顺序语义组织”的结构,与 X-ray 基于 CSS 选择器树形遍历的设计范式天然冲突——它无法原生表达“以某个元素为起点,收集其后所有同级兄弟直到下一个同类元素”这样的逻辑。

    直接尝试 xray("h2", [...]) 会失败,因为 X-ray 会将每个

    作为独立上下文,而

      并不在其子树内;而 xray("article", [...]) 加数组语法,则只会执行一次迭代(因全文仅一个
      ),无法生成多节结果。

      核心思路:DOM 预处理 + X-ray 后解析
      借助 jsdom 构建内存 DOM,遍历所有

      ,将其及后续连续非-

      兄弟节点(如

        )动态包裹进新
        中。这样就把线性结构转化为多个具备明确父子关系的块级容器,X-ray 即可自然地对每个
        执行对象映射。

        以下是完整、健壮的实现方案(含错误处理与空值兼容):

        const { JSDOM } = require('jsdom');
        const xray = require('x-ray')();
        
        // 自定义 trim 过滤器(X-ray 默认不内置)
        xray.filters({
          trim: (value) => typeof value === 'string' ? value.trim() : value
        });
        
        async function parseNonNestedSections(html, url = '') {
          const dom = new JSDOM(html);
          const doc = dom.window.document;
        
          // 步骤1:定位所有 h2,并按逻辑分组包裹
          const h2s = doc.querySelectorAll('article h2'); // 限定在 article 内更安全
          if (h2s.length === 0) return { pageTitle: '', sections: [] };
        
          // 创建临时根容器,避免污染 body
          const wrapper = doc.createElement('div');
          wrapper.innerHTML = doc.querySelector('article').innerHTML;
        
          // 重置 wrapper 内部引用
          const h2List = wrapper.querySelectorAll('h2');
        
          for (let i = 0; i < h2List.length; i++) {
            const h2 = h2List[i];
            const sectionDiv = doc.createElement('section'); // 语义化标签更佳
        
            // 移动 h2 到新 section
            sectionDiv.appendChild(h2);
        
            // 收集后续同级节点,直到下一个 h2 或 null
            let next = h2.nextElementSibling;
            while (next && next.tagName !== 'H2') {
              const toMove = next;
              next = next.nextElementSibling;
              sectionDiv.appendChild(toMove);
            }
        
            // 插入到 wrapper 中原位置(保持顺序)
            h2.parentNode.insertBefore(sectionDiv, h2);
            h2.remove(); // 清理已移动的 h2
          }
        
          // 步骤2:用 X-ray 解析预处理后的 HTML
          const processedHtml = wrapper.innerHTML;
        
          return new Promise((resolve, reject) => {
            xray(processedHtml, {
              pageTitle: 'h1 | trim',
              sections: xray('section', [{
                subtitle: 'h3 | trim',
                elements: xray('ul li', ['| trim']) // 返回字符串数组,自动过滤空值
              }])
            })((err, result) => {
              if (err) return reject(err);
              // 确保 elements 始终为数组(即使 ul 不存在)
              result.sections = result.sections.map(sec => ({
                subtitle: sec.subtitle || undefined,
                elements: Array.isArray(sec.elements) ? sec.elements : []
              }));
              resolve(result);
            });
          });
        }
        
        // 使用示例
        const sampleHtml = `
          <h1>Page title</h1>
          <article>
            <h2 id="first">Title 1</h2>
            <h3>Subtitle 1</h3>
            <ul><li>Element 1</li><li>Element 2</li><li>Element 3</li></ul>
            <h2 id="second">Title 2</h2>
            <h3>Subtitle 2</h3>
            <h2 id="third">Title 3</h2>
            <h3>Subtitle 3</h3>
            <ul><li>Element 1</li><li>Element 2</li><li>Element 3</li></ul>
          </article>
        `;
        
        parseNonNestedSections(sampleHtml)
          .then(console.log)
          .catch(console.error);

        ? 关键注意事项: