You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将法语维基词典复杂动词变位HTML表解析为JSON?

解析带rowspan/colspan的多层动词变位表(无需DOM修改)

核心逻辑

不用修改任何DOM节点,而是在遍历单元格时动态追踪当前所有生效的表头层级:

  • 用数组维护跨行列表头的生效范围(比如rowspan到第几行结束)
  • 通过位置匹配识别th[style*="background"]的分组表头
  • 把所有层级表头拼成路径键,对应到数据单元格内容

分步实现

1. 预提取分组表头

先把所有带背景色的顶级分组表头捞出来,记录它们的位置和跨行列范围,方便后续判断单元格属于哪个分组:

const groupHeaders = Array.from(table.querySelectorAll('th[style*="background"]')).map(th => {
  const row = th.parentNode;
  const rowIndex = Array.from(table.querySelectorAll('tr')).indexOf(row);
  const colIndex = Array.from(row.children).indexOf(th);
  const colspan = parseInt(th.getAttribute('colspan') || 1);
  const rowspan = parseInt(th.getAttribute('rowspan') || 1);
  return {
    text: th.textContent.trim(),
    startRow: rowIndex,
    endRow: rowIndex + rowspan - 1,
    startCol: colIndex,
    endCol: colIndex + colspan - 1
  };
});

2. 完整解析代码

遍历每一行时动态维护行/列表头的生效状态,自动处理跨行列属性,最终生成指定格式的JSON:

function parseConjugationTable(table) {
  const result = {};
  // 预提取分组表头
  const groupHeaders = Array.from(table.querySelectorAll('th[style*="background"]')).map(th => {
    const row = th.parentNode;
    const rowIndex = Array.from(table.querySelectorAll('tr')).indexOf(row);
    const colIndex = Array.from(row.children).indexOf(th);
    const colspan = parseInt(th.getAttribute('colspan') || 1);
    const rowspan = parseInt(th.getAttribute('rowspan') || 1);
    return {
      text: th.textContent.trim(),
      startRow: rowIndex,
      endRow: rowIndex + rowspan - 1,
      startCol: colIndex,
      endCol: colIndex + colspan - 1
    };
  });

  const rows = Array.from(table.querySelectorAll('tr'));
  let activeRowHeaders = []; // 当前行生效的行表头(处理rowspan)

  rows.forEach((row, rowIdx) => {
    // 更新行表头:移除已过期的(rowspan结束的)
    activeRowHeaders = activeRowHeaders.filter(h => rowIdx <= h.endRow);
    // 加入当前行的新行表头
    Array.from(row.querySelectorAll('th:not([style*="background"])')).forEach(th => {
      const rowspan = parseInt(th.getAttribute('rowspan') || 1);
      activeRowHeaders.push({
        text: th.textContent.trim(),
        endRow: rowIdx + rowspan - 1
      });
    });

    let activeColHeaders = []; // 当前列生效的列表头(处理colspan)
    let colIdx = 0;

    Array.from(row.querySelectorAll('td, th')).forEach(cell => {
      // 更新列表头:移除已过期的(colspan结束的)
      activeColHeaders = activeColHeaders.filter(h => colIdx <= h.endCol);
      // 加入当前列的新列表头(非分组表头的th)
      if (cell.tagName === 'TH' && !cell.hasAttribute('style')) {
        const colspan = parseInt(cell.getAttribute('colspan') || 1);
        activeColHeaders.push({
          text: cell.textContent.trim(),
          endCol: colIdx + colspan - 1
        });
      }

      // 找到当前单元格所属的分组表头
      const currentGroup = groupHeaders.find(g => 
        rowIdx >= g.startRow && rowIdx <= g.endRow && colIdx >= g.startCol && colIdx <= g.endCol
      );

      // 拼接完整路径键
      const path = [];
      if (currentGroup) path.push(currentGroup.text);
      path.push(...activeRowHeaders.map(h => h.text));
      path.push(...activeColHeaders.map(h => h.text));
      const key = path.join(' > ');

      // 数据单元格存入结果
      if (cell.tagName === 'TD') {
        result[key] = cell.textContent.trim();
      }

      // 更新列索引(处理colspan)
      colIdx += parseInt(cell.getAttribute('colspan') || 1);
    });
  });

  return result;
}

3. 关键细节

  • 全程无DOM修改:所有表头状态都用数组维护,避免了DOM操作的副作用
  • 分组表头匹配:通过行/列索引范围精准判断单元格所属分组
  • 跨行列处理:用endRow/endCol标记表头的生效边界,自动清理过期表头
  • 路径格式可自定义:把join(' > ')改成join('.')就能生成嵌套JSON的键,或者直接拼成数组路径

内容的提问来源于stack exchange,提问作者Lance Pollard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 12:42:46