本文介绍一种健壮的 javascript 方法,用于遍历含嵌套子表(如 `colspan="3"` 内嵌 `
`)的 html 表格,递归提取所有**有效数据行**(即具有完整列数的 ``),过滤掉仅作容器用途的合并单元格行,最终生成结构清晰、无重复/错位的 csv 文件。在实际前端开发中,动态渲染的层级化数据(如用户 Profile 及其关联的 Linked Users)常通过嵌套
实现。但传统遍历 tbody tr 会将包裹子表的 (如 | )误判为数据行,导致 CSV 输出混乱——例如把整个嵌套块当作单行输出 "Tom Jack Sam Alex",而非独立的四行。 根本问题在于:需区分“容器行”与“数据行”。理想策略是——忽略所有包含 colspan 或嵌套
的 本身,只提取那些直接包含等量 | 单元格(数量匹配表头列数)的 |
,并对其中嵌套的 递归执行相同逻辑。以下是一个生产就绪的解决方案: function exportTableToCsv() {
const table = document.querySelector('table');
if (!table) return;
// 提取表头(严格按 th 顺序)
const headers = Array.from(table.querySelectorAll('thead th'))
.map(th => sanit izeCell(th.textContent));
// 获取预期列数(用于过滤无效行)
const expectedCols = headers.length;
// 递归收集所有有效数据行(二维数组:[row1, row2, ...])
function collectRows(rows) {
const result = [];
rows.forEach(row => {
// 跳过含 colspan 的行(通常是容器行)或空行
const hasColspan = row.querySelector('td[colspan], th[colspan]');
if (hasColspan) {
// 查找该行内所有嵌套 table,并递归处理其 tbody tr
const nestedTables = row.querySelectorAll('table');
nestedTables.forEach(nestedTable => {
const nestedTbody = nestedTable.querySelector('tbody');
if (nestedTbody) {
result.push(...collectRows(nestedTbody.querySelectorAll('tr')));
}
});
return;
}
// 提取当前行的 td/th 文本,跳过空单元格
const cells = Array.from(row.querySelectorAll('td, th'))
.map(cell => sanitizeCell(cell.textContent));
// 仅保留列数匹配表头的有效数据行
if (cells.length === expectedCols) {
result.push(cells);
}
});
return result;
}
// 开始递归收集(从主 tbody 出发)
const tbody = table.querySelector('tbody');
const allDataRows = tbody ? collectRows(tbody.querySelectorAll('tr')) : [];
// 组装 CSV 字符串(含表头)
const csvLines = [
headers.join(','),
...allDataRows.map(row => row.join(','))
];
const csvContent = csvLines.join('\n');
// 触发下载
downloadCsv(csvContent, 'export.csv');
}
// 辅助函数:清洗单元格内容(处理换行、逗号、引号,符合 RFC 4180)
function sanitizeCell(text) {
const trimmed = text ? text.trim() : '';
// 若含换行、逗号或双引号,需用双引号包裹,并转义内部双引号
if (/[\n",]/.test(trimmed)) {
return `"${trimmed.replace(/"/g, '""')}"`;
}
return trimmed;
}
function downloadCsv(content, filename) {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}✅ 关键设计亮点:
-
精准过滤:通过 cells.length === expectedCols 确保每行数据列数与表头严格对齐,自动剔除 colspan 容器行;
-
深度递归:对每个含嵌套
的 ,定位其内部 tbody tr 并递归采集,不遗漏任意层级; -
CSV 安全转义:sanitizeCell() 遵循 RFC 4180 标准,自动包裹含特殊字符的字段并转义双引号,避免 Excel 解析错误;
-
健壮容错:支持缺失 thead/tbody、空单元格、混合 th/td 等边缘场景。
⚠️ 注意事项:
- 该方案假设所有嵌套子表结构与主表一致(列数相同)。若嵌套表列数不同,需扩展逻辑(如映射字段或填充空值);
- 避免在大型表格(>1000 行)中使用同步递归,可考虑 requestIdleCallback 分片处理;
- 如需支持中文导出,请确保 Blob 使用 utf-8 编码(已内置),并在 Excel 中用“数据 → 自文本”选择 UTF-8 编码打开。
调用 exportTableToCsv() 即可一键导出符合预期的 CSV: Tom,20,London Jack,30,Glasgow Sam,40,Belfast Alex,50,Hull Josh,20,Cardiff
结构清晰、语义准确、开箱即用。
|