ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++生成.xls文件:xlslib-2.5.0编译实战与BIFF8避坑指南

C++生成.xls文件:xlslib-2.5.0编译实战与BIFF8避坑指南 简介xlslib-2.5.0是一套面向C开发者的开源库用来在不依赖Office应用的环境里直接生成Excel文件同一压缩包还包含配套的libxls用于读取既有工作簿的内容两者分别满足不同方向的开发需求尤其适合在Windows系统上使用Visual Studio 2015编写报表生成、数据导出或日常办公工具的程序员。压缩包共收纳二百三十一个文件压缩后约九百六十六KB其中既有数十个头文件与源码文件也有若干解决方案、工程配置、编译脚本、示例程序和使用文档整体结构清楚方便按模块查阅。目前已有七百五十三人学习或下载对需要快速在VC2015环境中获得Excel读写能力的人来说性价比较高。包内带有VS2015工程配置与完整代码免去手工搭建编译环境的麻烦可直接在Visual Studio 2015中打开并生成所需库同时工程里分别提供了动态库与静态库两种配置并专门处理了中文字符集可减少乱码问题显著提升开发效率。 前阵子给一个老客户做数据导出功能对方业务系统是十年前的架构对接方只认.xls格式.xlsx一概不收。翻了一圈现成的 C 方案最后把xlslib-2.5.0拉出来重新编译使用折腾了几天把 BIFF8 的底层限制和编码问题摸了个透。这篇文章就当是给自己留个备忘也给需要在 C 项目里生成老式 Excel 文件的朋友提供一个可以直接抄作业的参考。1. 这个库的定位为什么今天还要写 .xls1.1 一个真实场景先说清楚我在什么场景下用到它。通常我们做报表导出首选肯定是.xlsx但某些银行、政务、制造业的老旧系统接口文档写得清清楚楚仅支持 Excel 97-2003 格式也就是.xls。这种系统往往是十几年前上线、没人敢动的核心链路。你当然可以用 Java 的 POI HSSF 或者 Python 的 xlwt但如果整个服务是 C 写的为了一个导出功能引入一个跨语言服务运维和部署成本都划不来。xlslib是少数能用纯 C 直接生成.xls文件的开源库。它不依赖 Excel COM 组件也不需要装 OfficeLinux 服务器上一条命令就能把文件吐出来。这对我们这种跑在容器里的后端服务来说是最省事的方案。1.2 xlslib 能做什么、不能做什么先给它画个边界免得大家踩到预期落差。它能做生成 BIFF8 格式的.xls文件Excel 97-2003。写入字符串、整数、浮点数、公式、日期。设置单元格样式字体、对齐、背景色、边框。合并单元格、设置列宽行高。多工作表。可选的 RC4 加密需要 libgcrypt 支持。它不能做生成.xlsx那是 Office Open XML 格式和 BIFF8 完全不兼容。读取.xls文件读取是libxls的活别搞混。图表、图片、数据透视表这类高级对象。超大文件。这点后面详细说底层为了兼容老格式行数和列数都有硬上限。如果你只需要生成.xlsxlslib的体量非常合适。核心代码不多编译出来一个静态库也就一两百 KB比动不动好几 MB 的框架轻太多。2. 编译环节把 2.5.0 跑起来需要留意的几件事2.1 源码获取与依赖准备2.5.0 这个版本GitHub 上有很多镜像仓库搜xlslib就能找到。因为原项目在 SourceForge 上维护频率不高GitHub 上反而是社区维护的 fork 更活跃修复了一些老版本在 GCC 新版本下的编译问题。依赖方面官方推荐了几个可选的组件libgcrypt用于文件加密如果你不需要加密功能建议直接关掉少一个系统依赖。libtool、autoconf、automake拉取源码后需要先跑autoreconf生成 configure 脚本。在 Ubuntu/Debian 下我一般这样准备sudo apt-get install autoconf automake libtool # 如果不打算用加密libgcrypt 就不装了2.2 configure 和 Make 的完整过程源码解压后先执行cd xlslib-2.5.0 autoreconf -i -f ./configure --without-libgcrypt make -j$(nproc) sudo make install注意--without-libgcrypt这个参数。如果系统里装了 libgcrypt-devconfigure 会自动启用加密支持那链接时就需要额外加-lgcrypt少写一个链接参数就会编译报错。我习惯统一关掉等真需要加密的再说。编译完会生成libxlslib.a和动态库头文件装在/usr/local/include/xlslib/xlslib.h。测试程序编译时这样链接g test.cpp -lxlslib -o test如果提示找不到头文件确认一下/usr/local/include在搜索路径里。比较老的发行版可能需要-I/usr/local/include手动指定。2.3 Windows 下的 CMake 构建项目里有CMakeLists.txtWindows 上用 CMake 生成 Visual Studio 工程也很顺mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . --config Release需要注意 MSVC 对 C 标准的支持比较严格某些 fork 版本可能要在工程属性里把语言标准调到 C17 或更高。如果编译报错说某个头文件找不到大概率是 fork 版本分支选错的问题换一个活跃维护的镜像就行。3. 核心 API 走读从工作簿到单元格3.1 必会 API 清单xlslib的命名空间是xlslib核心对象就三个workbook工作簿、worksheet工作表、cell单元格。写一个最简单的文件只需要几行#include xlslib/xlslib.h using namespace xlslib; using namespace std; int main() { workbook wb; worksheet* ws wb.sheet(Sheet1); // 字符串单元格 ws-label(0, 0, 项目); ws-label(0, 1, 金额); // 数值单元格 ws-number(1, 0, 1001); ws-write(1, 1, 3.14); // 公式 ws-formula(2, 1, SUM(B2:B3)); wb.Dump(output.xls); return 0; }这里write()是一个模板重载可以自动根据参数类型区分整数、浮点数、字符串比较省事。但如果你需要指定格式还是要显式调用label()或number()把format_t*作为第三个参数传进去。3.2 样式、合并单元格和列宽样式这块是日常用得最多的。xlslib里的样式对象通过workbook::xf()创建复用同一个xf_t*传给多个单元格而不是每个单元格新建一个。xf_t* header_style wb.xf(); header_style-set_font_bold(); header_style-set_font_color(COLOR_WHITE); header_style-set_fill_pattern(SOLID_FILL_PATTERN); header_style-set_fill_fgcolor(COLOR_BLUE); header_style-set_align(ALIGN_H_CENTER | ALIGN_V_CENTER); ws-label(0, 0, 项目, header_style); ws-label(0, 1, 金额, header_style);合并单元格的参数顺序是merged(起始行, 起始列, 结束行, 结束列)四个参数都是行或列的索引ws-merged(0, 0, 0, 1); // 把第一行第0列到第1列合并 ws-label(0, 0, 合并标题, header_style);列宽和行高设置也简单ws-set_col_width(0, 30); // 第0列宽度设置为30个字符宽 ws-set_col_width(1, 20); // 第1列宽度设置为20个字符宽 ws-set_row_height(0, 24); // 第一行行高24磅一个比较容易忽略的点set_col_width的宽度单位是字符数不是像素。默认字体的字符宽度是 8 像素左右所以 30 字符大约等于 240 像素。设置太窄会被 Excel 自动截断设置太宽又显得浪费一般我按内容最长字符串的长度再加 2 个字符来做估算。3.3 日期和时间处理Excel 内部存储日期就是一个双精度浮点数整数部分是天数小数部分是当天的时间比例。xlslib本身不做时间转换你需要自己把time_t转成 Excel 序列数serial number。换算公式很简单1970 年 1 月 1 日对应的 Excel 序列数是 25569。所以static double excel_date(time_t t) { return (double)t / 86400.0 25569.0; }写入的时候配合日期格式format_t* date_fmt wb.format(YYYY-MM-DD HH:MM); ws-write(1, 0, excel_date(time(NULL)), date_fmt);要注意时区问题。上面的公式按 UTC 计算如果你在中国时区time(NULL)得到的是 UTC 时间转出来会差 8 小时。保险起见先把time_t通过localtime转成tm再用mktime计算当天的本地 0 点的序列数这样更稳妥。4. 踩过才知道的坑BIFF8 限制、中文编码和公式4.1 65536 行 256 列是硬约束.xls的 BIFF8 格式行号是 16 位列号是 8 位所以最大是 65536 行 x 256 列。这个限制没得商量是文件格式的物理上限。如果你调number(65536, 0, 1)xlslib不会报错但生成的文件在 Excel 里打开会提示损坏或者直接看不到那一行。我自己第一次踩到这个坑是生成一个十几万行的日志导出写完后 Excel 直接说文件格式错误排查了半天才发现是行数越界。实践中我加了个防御判断if (row_index 65536 || col_index 256) { // 切到下一个工作表或者抛异常提醒前端 }超过 65536 行要么拆分多个 sheet要么直接换.xlsx方案没有第三条路。4.2 中文字符串的编码陷阱这个坑是最隐蔽的。xlslib2.5.0 内部把字符串统一转成 UTF-16 写入 BIFF8它默认输入是 UTF-8 编码的std::string。如果你传入的是 GBK/GB2312 编码的字符串生成的文件打开后中文全是乱码。我们在国内项目里拿到的数据十有八九是 GBK 编码必须先从 GBK 转成 UTF-8。Linux 下用iconv可以搞定std::string gbk_to_utf8(const std::string input) { iconv_t cd iconv_open(UTF-8, GBK); // 标准 iconv 调用注意指针偏移问题 // ... }更省事的方式是把整个系统标准统一为 UTF-8数据库连接、网络协议栈全部走 UTF-8只在写入xlslib前做一个简单的确认。如果输入源是 GBK转完再写。不要寄希望于库帮你自动处理编码。还有一个细节BIFF8 单元格字符串的理论最大长度是 32767 字节但实际测试中超过 1024 字符的单元格在部分旧版 Excel 里会显示异常。所以遇到特别长的文本先截断或者拆到多个单元格。4.3 公式与缓存值xlslib写公式只是把公式字符串写进文件。它会顺便写一个 0 作为缓存值。Excel 打开文件时会自动重新计算所有公式所以最终显示是正确的。但有个坑如果你的下游程序不打开 Excel而是直接通过libxls或者其他组件库读取.xls文件提取数据读取到的公式单元格的缓存值就是 0而不是计算结果。这在自动化报表链路里非常致命。我遇到过的情况是服务端生成报表落库接着又有个数据抽取程序去读这个文件读出来的求和结果全是 0。排查到最后发现是缓存值根本没算。解决方案有两个在生成报表时自己把公式计算好用number()写计算结果不写公式。或者保证读取端是 Excel 打开过的真实场景比如用户手动下载打开。如果不是对公式动态更新有硬需求我建议直接计算结果写死。简单、可靠、不依赖下游行为。4.4 样式数量上限BIFF8 文件里的单元格格式xf数量不能超过 4096 个实际可用的大约 4094 个。这是老格式的限制不是xlslib的限制。曾经踩过循环一万行每行都调wb.xf()新建样式甚至每个单元格都新建结果导出文件在 Excel 里打不开直接提示文件损坏。因为写入的 xf 索引超出了格式表范围。正确做法是全局维护一个样式表同一种样式只创建一次复用同一个xf_t*指针// 按照文字内容使用同一个样式不要在循环里新建 xf_t* normal_style wb.xf(); for (int i 0; i 100000; i) { ws-number(i, 0, i, normal_style); }如果你真的需要大量不同的格式组合先统计一下组合数超过 4000 种就要收敛一下样式方案了。5. 性能实测与内存优化建议5.1 生成大表格的基准数据我在一台 4 核 8G 的容器里做了个简单测试生成 30000 行 x 10 列的数据每列都是数字和短字符串混排用时大约 1.5 秒生成文件约 3 MB。这个速度对于常规报表足够用了。内存方面比较紧张。xlslib是把所有单元格对象都驻留在内存里最后统一序列化输出。30000 行 x 10 列大约是 30 万单元格实测内存峰值在 300 MB 左右。虽然单元格对象不大但每个对象都持有索引、值、样式指针、内置字符串等累加起来非常可观。所以不建议在内存紧张的服务里用xlslib生成非常大的文件。如果数据量到了 4 万行以上建议先拆分成多个 sheet每个 sheet 控制在 2 万行以内避免单工作表过大导致内存爆炸。5.2 三个提高性能的实操习惯第一尽量减少样式对象的创建。这一点前面已经说过从性能角度看wf.xf()内部要注册格式、创建多种内部记录比写一个单元格成本高一个数量级。最好是在函数入口把这个表的所有样式一次性建好后面只传指针。第二字符串写入前先做必要的转换不要在写入路径里重复iconv。如果数据源是 GBK先把整批数据一次性转成 UTF-8 再传给xlslib而不是每个单元格写之前现转转换的开销会小很多。第三用Dump到std::ofstream时把流缓冲区调大。文件流默认是 8 KB 缓冲对于几 MB 的.xls文件读写次数会非常频繁。改用 64 KB 以上的缓冲区能明显降低 IO 耗时std::ofstream fout(output.xls, std::ios::binary); char buf[1024 * 64]; fout.rdbuf()-pubsetbuf(buf, sizeof(buf)); wb.Dump(fout); fout.close();实测这个小改动让生成 3 MB 文件的时间缩短了大约 20%。6. 同类库对比什么时候选它什么时候别选6.1 对比表库语言支持格式功能范围依赖适用场景xlslibC写 .xls (BIFF8)基础单元格、样式、合并、公式、加密极简可选 libgcryptC 后端生成老旧 .xlslibxlsxwriterC写 .xlsx图表、图片、富文本、数据验证极简zlib 可选C/C 后端生成现代 ExcellibxlsC读 .xls读取 BIFF8 数据极简C/C 读取 .xlsOpenXLSXC读写 .xlsx中等偏向面向对象 API需要 C17C 项目读写 .xlsxPOI HSSFJava写 .xls功能全面JDKJava 体系里操作 xls注意最后一行的 POI 是 Java 的如果你本身是 Java 服务直接用 POI 就行没必要在 C 里折腾。但如果你是 C 服务要同时处理.xls的读写xlsliblibxls的组合是比较经典的老牌搭配。6.2 我的选型建议新项目我会优先看libxlsxwriter。因为它支持.xlsx功能丰富性能好且没有行数上限。只有在对接方明确要求.xls或者那套系统老到只能打开.xls时才选xlslib。但如果团队已经有一堆基于xlslib的存量代码其实 2.5.0 用起来问题不大。它的 API 稳定十年前的代码今天还能原样编译运行这种长期兼容性在开源 C 库里面是少见的品质。对于老系统维护稳定的老库往往比换新框架更靠谱。从我个人的实际使用感受来说xlslib-2.5.0在它擅长的领域内表现合格依赖少、接口直接、编译简单。只要把行数上限、编码转换、样式复用这三件事处理好它就是一个非常顺手的工具。如果你的项目正好也卡在“必须生成 .xls 且不能引重量级依赖”这个点上直接照着我上面这几步走能省不少试错时间。本文还有配套的精品资源点击获取
返回列表