在做数据导出功能时,如果数据集有上百万行,直接往一个Excel工作表里塞显然行不通。Excel单个工作表的上限是1048576行,而且即便没超限,一次性写入百万行数据也会导致内存暴涨、生成文件极慢。比较成熟的做法是把数据集按固定行数切块,每一块写入一个独立的工作表,这样既绕开行数限制,又能让每个表的体量保持在合理范围。下面详细介绍这套分块写入方案的完整实现。

分块策略的设计思路
分块的核心是确定每个工作表承载多少行数据。切块太小会导致工作表数量爆炸,一个包含500万行数据的集合如果每块只切1万行,会生成500个工作表,用户打开文件后在表之间来回切换会非常痛苦;切块太大又起不到分散压力的作用。实践经验中,每块10万到20万行是比较均衡的选择,既能保证单个工作表打开流畅,又不会让表的数量过多。
具体切割逻辑很简单:假设总行数为totalRows,每块行数为chunkSize,那么需要的块数为(totalRows + chunkSize - 1) / chunkSize,这个向上取整的写法可以处理最后一块不满的情况。每一块数据的起始行是index * chunkSize,结束行是min((index + 1) * chunkSize, totalRows),按这个区间从源数据中截取即可。
还有一个容易被忽略的细节是表头处理。每个工作表都应该带上相同的表头行,否则用户切到后面的表就不知道每列是什么含义。建议把表头单独存储,在每个工作表创建时先写入表头再写入数据块。另外工作表命名建议采用统一的规则,比如Sheet_1、Sheet_2,如果业务上有分页含义,也可以命名为数据_第1页这种形式,但要注意Excel工作表名称不能超过31个字符,且不能包含\ / ? * [ ]这几个字符。
用xlnt库实现分块写入
xlnt是一个纯C++的Excel操作库,支持读写xlsx格式,跨平台且不依赖Office组件,非常适合服务端程序使用。用xlnt实现分块写入的思路是:先创建工作簿,然后循环创建工作表,每个工作表写入一个数据块,最后统一保存。
#include <xlnt/xlnt.hpp>
#include <vector>
#include <string>
// 模拟数据行:每行若干列字符串
using DataRow = std::vector<std::string>;
void writeInChunks(const std::vector<DataRow>& dataset,
const DataRow& header,
const std::string& filePath,
std::size_t chunkSize)
{
xlnt::workbook wb;
std::size_t totalRows = dataset.size();
if (totalRows == 0 || chunkSize == 0) return;
// 向上取整计算块数
std::size_t chunkCount = (totalRows + chunkSize - 1) / chunkSize;
for (std::size_t i = 0; i <chunkCount; ++i) {
// 为每块创建一个工作表
xlnt::worksheet ws = wb.create_sheet();
ws.title("Sheet_" + std::to_string(i + 1));
// 先写表头
for (std::size_t col = 0; col <header.size(); ++col) {
ws.cell(xlnt::cell_reference(col + 1, 1)).value(header[col]);
// 表头加粗便于阅读
ws.cell(xlnt::cell_reference(col + 1, 1)).font()
.bold(true);
}
// 计算当前块的行区间
std::size_t begin = i * chunkSize;
std::size_t end = std::min(begin + chunkSize, totalRows);
for (std::size_t r = begin; r <end; ++r) {
const DataRow& row = dataset[r];
for (std::size_t c = 0; c <row.size(); ++c) {
ws.cell(xlnt::cell_reference(c + 1, r - begin + 2))
.value(row[c]);
}
}
}
wb.save(filePath);
}这段代码里有个关键点:写入单元格时行号要加上偏移r - begin + 2,加2是因为第1行留给了表头,而当前块内的数据要重新从第2行开始编号,而不是沿用原始数据的全局行号。这是分块写入时最常见的出错点,如果直接用r + 1做行号,后面的工作表会尝试写入超出块范围的位置,虽然不一定报错,但结果完全错乱。
性能方面,如果数据集特别大,逐格调用cell()会有一定开销。xlnt提供了批量追加行的接口,用ws.append(row)可以把一整行作为vector一次写入,内部处理效率更高,百万级数据下能节省可观的构造时间。此外建议在写入前用reserve预留内存,避免工作簿对象频繁扩容。
流式处理大数据源的内存优化
上面的示例把整个数据集放在内存里,但真实场景中数据往往来自数据库游标或者大文件,一次性加载不现实。这时候应该把分块逻辑改成流式:维护一个行计数器,每凑满chunkSize行就落一个工作表,然后清空缓冲区继续读取。
#include <xlnt/xlnt.hpp>
#include <vector>
#include <string>
#include <fstream>
// 从CSV文件流式读取并分块写入多个工作表
void streamWrite(const std::string& csvPath,
const std::string& xlsxPath,
std::size_t chunkSize)
{
xlnt::workbook wb;
std::ifstream in(csvPath);
std::string line;
xlnt::worksheet* ws = nullptr;
std::size_t rowsInSheet = 0;
int sheetIndex = 0;
bool firstLine = true;
while (std::getline(in, line)) {
if (firstLine) {
firstLine = false;
// 首行视为表头,逐块复制
std::vector<std::string> header;
size_t start = 0, pos;
while ((pos = line.find(',')) != std::string::npos) {
header.push_back(line.substr(start, pos - start));
start = pos + 1;
}
header.push_back(line.substr(start));
// 存起来供后续每个表使用,这里简单起见重新解析
continue;
}
if (ws == nullptr || rowsInSheet >= chunkSize) {
++sheetIndex;
ws = &wb.create_sheet();
ws->title("Sheet_" + std::to_string(sheetIndex));
rowsInSheet = 0;
}
// 按逗号拆分后追加一行
std::vector<std::string> cells;
size_t start = 0, pos;
std::string rest = line;
while ((pos = rest.find(',')) != std::string::npos) {
cells.push_back(rest.substr(start, pos - start));
start = pos + 1;
rest = rest.substr(start);
start = 0;
}
cells.push_back(rest);
ws->append(cells);
++rowsInSheet;
}
wb.save(xlsxPath);
}流式方案的好处是内存占用只跟单块大小相关,跟总数据量无关。即使数据源有几千万行,程序占用的内存也始终稳定在单块10万行左右的水平。需要注意xlnt本身的workbook对象会把所有工作表数据都缓存在内存里,直到调用save才真正落盘,所以如果数据规模达到千万行级别,更彻底的做法是改用LibXL或者直接生成xlsx的底层格式,或者干脆拆成多个xlsx文件再加一个汇总目录表。
另一个实用技巧是进度回调。长时间写入过程如果没有任何反馈,用户会以为程序卡死。可以在每写完一个工作表时回调一次进度百分比,界面上显示当前进度,体验会好很多。同时建议把save放在异常保护的范围内,一旦中途失败及时清理临时文件,避免留下损坏的半成品xlsx。
Windows下用COM接口的替代方案
如果程序只在Windows环境运行,且机器上装了Office,也可以通过COM自动化接口驱动Excel进程完成写入。这种方式的好处是格式控制能力最强,能直接设置样式、图表等高级特性,缺点是依赖Office环境,性能也比xlnt慢不少,适合对格式要求苛刻的桌面软件场景。
#import "Excel.exe" rename("DialogBox", "ExcelDialogBox")
// 简化示例:核心是Worksheets的Add调用
// 每写满一块数据就调用一次
// pSheet = pWorksheets->Add();
// 然后用Range的Value2批量赋值,比逐Cell快一个数量级
// 伪代码示意:
// for each chunk:
// Add new worksheet
// set Range(top-left, bottom-right).Value2 = 2D array
// release references用COM方案时要特别注意批量赋值。把整块数据构造成二维SAFEARRAY,通过Range::Value2一次性赋值,速度比逐个Cell赋值快几十倍。同时务必在程序退出前正确释放所有COM对象引用并调用CoUninitialize,否则Excel进程会残留在后台占用文件句柄,导致用户打不开生成的文件。
综合来看,跨平台项目首选xlnt,桌面工具且格式要求高可以用COM,两者分块写入的核心算法完全一致,只是API层面的差异。掌握了按块切分、表头复制、行号偏移这几个要点,应对百万级数据导出就不再是难题。
C++ Excel写入大数据分块多工作表修改时间:2026-09-11 20:48:54