许多政府机构通过 Excel 方式发布官方数据和统计信息。使用 SheetJS 库有助于将这些文件转换为有用的信息,如将 Excel 数据转换成 JSON 格式的数据,方便程序使用。
本示例的目标是处理来自 XLS 工作表的联邦学生资助组合数据。我们将从美国教育部下载并解析一个 Excel。原始数据解析完成后,我们将提取未偿还的总金额,并将数据显示在表格中。
以下序列图展示了该过程:

原始数据可在一个 XLS 工作簿中获取,文件地址 https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls。如下图:

注意,SheetJS 支持多种旧版和现代格式,确保历史数据不会在时间的长河中丢失。
这里将介绍如何使用 fetch 下载文件,用法十分简单,代码如下:
// 文件地址
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
// file 是一个 ArrayBuffer 存放文件数据
const file = await (await fetch(url)).arrayBuffer();文件数据存储在一个 ArrayBuffer 中。
有了文件数据后,XLSX.read 会解析该文件并生成一个 SheetJS 工作簿(workbook)对象,如下:
const workbook = XLSX.read(file);上面 workbook 对象遵循“通用电子表格格式”,这是一种用于工作簿、工作表、单元格和电子表格功能的内存中的模式。
实际使用中的电子表格采用了许多不同且不一致的格式。要确定如何处理这些数据前,最好先检查文件,查看文件的格式。
正如在“工作簿对象”页面中所解释的,SheetNames 属性是工作簿中工作表名称的列表。
下面代码展示了工作表名称的有序列表:
<table>
<thead>
<tr>
<th>SheetName</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
workbook.SheetNames.forEach(item => {
// 创建tr元素
const row = document.createElement("TR");
// td数据填充到tr中
row.innerHTML = `<td>${item}</td>`;
tbody.appendChild(row);
});
})();
</script>运行上面示例,将输出 sheetjs_PortfolioSummary.xls 表格中的所有工作表名称,如下图:

工作簿(workbook)对象的 Sheets 属性是一个对象,其键是工作表名称,值是工作表对象。例如,可以通过检索 SheetNames 并使用该名称检索 Sheets 来获取第一个工作表,例如:
// workbook.SheetNames[0] 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];注意:可以直接检查工作表对象,但强烈建议使用实用函数来提取相关数据。
sheet_to_html 函数用于从工作表对象生成 HTML 表格。
以下实时示例展示了表格中数据的前 20 行:
<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为 HTML 表格字符串
const table = XLSX.utils.sheet_to_html(first_sheet);
document.getElementById('app').innerHTML = table;
})();
</script>运行示例,效果如下图:

查看该表格得出的要点如下:
数据从第 7 行开始
第 5 行和第 6 行是标题行,其中通用标题所在的单元格是合并的
对于年度数据(2007-2012 年),A 列和 B 列是合并的
对于季度数据(2013 年第一季度及之后),A 列存储年份。单元格可能会纵向合并以涵盖 4 个季度
sheet_to_json 函数用于从工作表对象生成数据数组。对于像这样的复杂布局,最简单的方法是生成一个“数组的数组”,其中每一行都是一个单元格值数组。截图显示了第 5~8 行:

在数组的数组中,第5行有许多空白,这些空白对应着空单元格以及合并范围内被覆盖的单元格:
// 第 5 行 —— 空白处对应没有内容的单元格
[ , , "Direct Loans", , "Federal Family Education Loans (FFEL)", , "Perkins Loans", , "Total1" ]第 7 行包含 2007 财政年度的数据:
// 第 7 行——B 列被合并单元格覆盖了
[ 2007, , 106.8, 7, 401.9, 22.6, 8.2, 2.8, 516, 28.3 ]如果指定了选项 header: 1,XLSX.utils.sheet_to_json() 将生成一个数组的数组:
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const raw_data = XLSX.utils.sheet_to_json(worksheet, {header: 1});单元格 A13:A16 是合并的:

合并的数据仅适用于左上角单元格(A13)。数组的数组在单元格A14:A16中会有缺失值(以 null 表示):
// 第 13 行 [2013, "Q1", 508.7, 23.4, 444.9, 22.1, 8.2, 3, 961.9, 38.7]
// 第 14 行 [null, "Q2", 553, 24.1, 437, 21.6, 8.3, 3, 998.6, 38.9]
// 第 15 行 [null, "Q3", 569.2, 24.3, 429.5, 21.2, 8.2, 2.9, 1006.8, 38.7]
// 第 16 行 [null, "Q4", 609.1, 25.6, 423, 20.9, 8.1, 2.9, 1040.2, 39.6]<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为JSON数据
const raw_data = XLSX.utils.sheet_to_json(first_sheet, {header:1});
// 提取 Excel 的 13 到 16 行(SheetJS 的 12 到 15 行)
const rows_13_16 = raw_data.slice(12,16);
// 显示数据
var htmls = [];
rows_13_16.forEach(item => {
htmls.push(`<p>${JSON.stringify(item)}</p>`)
});
document.getElementById('app').innerHTML = htmls.join('');
})();
</script>效果如下图:

工作表的 worksheet!merges 属性包含了工作表中的所有合并区域。可以遍历每个合并块并填充单元格,但在这种情况下,对原始数据进行后处理会更简单:
// 处理逻辑:
// 根据观察上面的输出,第一列 null 的正确填充值应该是最后一个不为 null 的值,上面是 2013
let last_year = 0; // 记录最后一个年份
// 遍历原始数据
// 如果当前行的第一列不为null,则将该列的值赋值给第一列和last_year
// 如果当前行的第一列为null,则将 last_year 的值赋值给第一列和last_year
raw_data.forEach(r => last_year = r[0] = (r[0] != null ? r[0] : last_year));经过后处理后,这些行现在有了正确的年份字段:
// 第 13 行 [2013, "Q1", 508.7, 23.4, 444.9, 22.1, 8.2, 3, 961.9, 38.7]
// 第 14 行 [2013, "Q2", 553, 24.1, 437, 21.6, 8.3, 3, 998.6, 38.9]
// 第 15 行 [2013, "Q3", 569.2, 24.3, 429.5, 21.2, 8.2, 2.9, 1006.8, 38.7]
// 第 16 行 [2013, "Q4", 609.1, 25.6, 423, 20.9, 8.1, 2.9, 1040.2, 39.6]<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为JSON数据
const raw_data = XLSX.utils.sheet_to_json(first_sheet, {header:1});
// 填充年份
var last_year = 0;
raw_data.forEach(r => last_year = r[0] = (r[0] != null ? r[0] : last_year));
// 提取 Excel 的 13 到 16 行(SheetJS 的 12 到 15 行)
const rows_13_16 = raw_data.slice(12,16);
// 显示数据
var htmls = [];
rows_13_16.forEach(item => {
htmls.push(`<p>${JSON.stringify(item)}</p>`)
});
document.getElementById('app').innerHTML = htmls.join('');
})();
</script>填充年份后效果如下图:

此时,每一行数据的年份都在 A 列,美元数值在 C 列。年份(行中的第一个值)将在 2007 到 2029 之间。数值(第三个值)为正数。以下函数用于测试一行数据是否符合要求:
const is_valid_row = r =>
r[0] >= 2007 && r[0] <= 2029 // 年份(A列)在2007到2029之间
&& r[2] > 0; // 美元数值(C列)为正数Array#filter 可以使用之前的测试来筛选出匹配的行:
// 2016年第一季度 - 1220.3美元(十亿)
{ "FY": 2016, "FQ": "Q1", "total": 1220.3 }<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为JSON数据
const raw_data = XLSX.utils.sheet_to_json(first_sheet, {header:1});
// 填充年份
var last_year = 0;
raw_data.forEach(r => last_year = r[0] = (r[0] != null ? r[0] : last_year));
// 选择数据行(2007~2029年,且金额大于0)
const rows = raw_data.filter(r => r[0] >= 2007 && r[0] <= 2029 && r[2] > 0);
// 显示数据
var htmls = [];
rows.forEach(item => {
htmls.push(`<p>${JSON.stringify(item)}</p>`)
});
document.getElementById('app').innerHTML = htmls.join('');
})();
</script>生成行对象
查看标题:

所需数据位于 I 列,列索引可以使用 decode_col实用函数 来计算。例如:
<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为JSON数据
const raw_data = XLSX.utils.sheet_to_json(first_sheet, {header:1});
// 填充年份
var last_year = 0;
raw_data.forEach(r => last_year = r[0] = (r[0] != null ? r[0] : last_year));
// 选择数据行(2007~2029年,且金额大于0)
const rows = raw_data.filter(r => r[0] >= 2007 && r[0] <= 2029 && r[2] > 0);
// 使用 XLSX.utils.decode_col() 方法将列名称转换为索引
const cols = ["A", "B", "I"];
var data = cols.map(col => {
return `<p>${col} => ${XLSX.utils.decode_col(col)}</p>`;
});
document.getElementById('app').innerHTML = data.join("");
})();
</script>所需的列如下:
列 | 描述 | 对象中的属性 |
A / 0 | 财政年度 | FY |
B / 1 | 财政季度(如适用) | FQ |
I / 8 | 未偿还美元总额 | total |
对数据执行Array#map可以生成所需的行对象:
const objects = rows.map(r => ({FY: r[0], FQ: r[1], total: r[8]}));这将生成一个行对象数组。每个行对象将如下所示:
// 2016 Q1 - $1220.3 (billion) { "FY": 2016, "FQ": "Q1", "total": 1220.3 }<div id="app"></div>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
// 获取第一个工作表的名称,PortfolioSummary
var sheet_name = workbook.SheetNames[0];
// 根据工作表名称从 Sheets 中获取工作表对象
var first_sheet = workbook.Sheets[sheet_name];
// 将工作表对象转换为JSON数据
const raw_data = XLSX.utils.sheet_to_json(first_sheet, {header:1});
// 填充年份
var last_year = 0;
raw_data.forEach(r => last_year = r[0] = (r[0] != null ? r[0] : last_year));
// 选择数据行(2007~2029年,且金额大于0)
const rows = raw_data.filter(r => r[0] >= 2007 && r[0] <= 2029 && r[2] > 0);
// 生成行对象(第一列、二列和九列的数据)
const objects = rows.map(r => ({FY: r[0], FQ: r[1], total: r[8]}));
// 显示数据
var htmls = [];
rows.forEach(item => {
htmls.push(`<p>${JSON.stringify(item)}</p>`)
});
document.getElementById('app').innerHTML = htmls.join('');
})();
</script>下面是完整实例代码:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SheetJS 教程</title>
<!-- 引入 SheetJS 库 -->
<script src="https://www.hxstrive.com/cdn/libs/SheetJS/0.20.3/xlsx.full.min.js"></script>
<style>
table {
border: 1px solid #e2e8f0;
border-collapse: collapse;
}
table th, table td {
border: 1px solid #e2e8f0;
}
table th {
border-bottom: 2px solid #cbd5e1;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Fiscal Year</th>
<th>Quarter</th>
<th>Total (in $B)</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<script>
(async() => {
// 下载文件数据
const url = "https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_PortfolioSummary.xls";
const file = await (await fetch(url)).arrayBuffer();
// 解析工作簿
const workbook = XLSX.read(file);
console.log("所有工作表名称:", workbook.SheetNames);
// 获取第一个工作表
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 将工作表的数据转换成JSON数据,JSON格式
// [[第一行数据], [第二行数据], ...]
const raw_data = XLSX.utils.sheet_to_json(worksheet, {header:1});
console.log(raw_data)
// 填充年份列,处理合并问题
var last_year = 0; // 临时存放上一行的年份信息
raw_data.forEach(row => {
last_year = row[0] = (row[0] != null ? row[0] : last_year)
});
// 选择数据行,只选择2007-2029年间且有数据的行
const rows = raw_data.filter(row => {
return row[0] >= 2007 && row[0] <= 2029 && row[2] > 0
});
// 生成行对象数组,方便后续操作
const objects = rows.map(row => ({
FY: row[0],
FQ: row[1],
total: row[8]
}));
// 将数据行信息添加到table的tbody中
objects.forEach(o => {
// 创建tr元素
const row = document.createElement("TR");
// td数据填充到tr中
row.innerHTML = `<td>${o.FY}</td><td>${o.FQ||""}</td><td>${o.total}</td>`;
tbody.appendChild(row);
});
})();
</script>
</body>
</html>参考资料:https://docs.sheetjs.com/docs/getting-started/examples/import