如何将法语维基词典复杂动词变位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
相关产品推荐
相关产品推荐

