在 Gopher Meetup 北京站上,阿里巴巴高级开发工程师、前百度 Go 语言编程委员会成员续日进行了主题为《Go语言国际电子表格文档格式标准实践》的演讲。
Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376, ISO/IEC 29500 国际电子表格文档格式标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档,可以应用于各类报表系统中,做为 2018 开源中国最有价值开源项目,目前已成为 Go 语言最受欢迎的 Excel 文档基础库。本次演讲将探究国际文档格式标准,解读使用 Go 语言从 0 到 1 打造 Excelize 背后的故事,分享 Go 语言在国际电子表格文档格式标准方面的实践。
以下为演讲内容整理。
00
前言
大家好!我叫续日。今天给大家带来 Go 语言在国际电子表格文档格式标准实践方面的分享。提起电子表格大家都比较熟悉,以 Excel 为代表的经典电子表格已经应用在各行各业当中,根据相关研究机构的估算数据,世界上 Office 办公文档的数量在 2007 年的时候已经达到了 400 亿规模,并且每年大约以数十亿的规模增长。
为什么要用 Excelize
Excel 文档作为一种数据承载的重要载体,在很多领域都有应用。作为开发者,一些情况下需要用编程的方式操作这些 Excel 文档,比如从已有电⼦表格⽂档中读取内容、创建新的电⼦表格⽂档、基于已有⽂档(模版)⽣成新的电⼦表格⽂档、向电⼦表格⽂档中插⼊图⽚、图表和表格等元素,有时还需要跨平台实现这些操作。Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,使用 Excelize 可以方便的实现上述需求。今天的分享包含两个部分,第一部分介绍国际电子表格文档格式标准的内容,第二部分向大家分享在使用 Go 语言实现标准过程当中的一些经验。
01
文档格式国际标准
如果要用编程的方式操作电子表格文档,首先需要了解其标准。ECMA-376, ISO/IEC 29500 做为国际文档格式标准是一种基于 XML 和 ZIP 技术的文档格式,对应大家所熟悉的 Office 文档文件。这个标准的名字比较长,里面涉及到三个国际的标准化组织。ECMA 是欧洲计算机厂商联盟,ISO 大家比较熟悉,国际标准化组织,IEC 是国际电工协会,这三个国际化组织联合制定了文档格式国际标准。
标准特点分为六个方面,下面解释一下比较有代表性的几个特点。互操作性, 文档格式标准独立于专有格式、功能和运行时环境,平台语言无关,使开发人员可以进行广泛选择。国际化体现在文档标准当中涉及到像语言的书写规范,有些文字是纵向的,或者是从右到左的书写系统,以及各个国家地区的历法、时间的处理。高保真迁移,文档的内容本身不需要开发者把所有的数据结构都实现出来,也能在呈现上表现一致。因为它的数据结构是非常庞大的,也提供了向后扩展的空间,业务扩展空间体现在向后兼容的设计,在文档当中可以嵌入多媒体的文件,比如 VBA 脚本、图片、音视频等。
TC45 是 ECMA 下属的技术委员会,主要负责对该标准的修订和维护。这个标准目前已经发展到第五版,一共有五部分。委员会的代表中有很多大家熟悉的公司,其中包括计算机厂商、互联网公司和传统行业代表,例如光学镜片公司、石油公司和图书馆等,可以发现图书馆在信息化的存储方面也有很多的需求。
右边两张图片是标准文档的下载页面截图,第一张图片是该标准第一部分在 ISO 上的页面,可以看到第一部分的内容有超过 5000 页的文档,数量还是比较多的。第二张图片是 ECMA 网站上该标准的截图,上面有对标准中 5 个部分的描述,我们可以访问网站下载标准文档进行查看。大家比较熟悉的像 Microsoft Office Excel、Apple Numbers、LibreOffice、Google Docs、Apache OpenOffice 等,都是遵循该标准的典型代表。
标准规范
下面介绍一下标准的内容,首先上层是标记语言部分,这部分由四类标记语言组成。
Word 文档对应的标记语言叫 WordprocessingML,电子表格则是 SpreadsheetML,PowerPoint 演示文稿对应的是 PresentationML。Excelize 主要实现的是 SpreadsheetML 这个部分,除此之外 Office 文档支持进行跨应用的嵌套,例如:Word 可以嵌套 Excel,Excel 可以嵌套 Word。通用标记是跨应用的文本标记语言,涉及到可视化图表、可扩展标记、源数据和目录引用等。
中间这一层叫做开放包装公约(Open Packaging Convention),简称 OPC,是组织这些文档格式的上层描述语言,其中定义了文档内部组件之间的关联关系、文档内部数据结构之间的依赖和文件数字签名等。
下面是底层的核心技术,我们可以创建一个 Excel 文件,修改成 zip 扩展名解压一下就知道它是个压缩包,标准委员会采用比较基础的核心技术来定义文档格式,这样做有很强的扩展性,也方便各个平台去实现。
XSD (XML Schema Definition)
为了方便大家理解后面的内容,先介绍一个技术名词 —— XSD。XSD(XML Schema Definition) 是 W3C(万维网联盟)推出的一个标准,用于定义 XML 的格式,XML 格式里面可以有无限的标签和属性,XSD 是用于描述 XML 的标记语言。
如上图所示的 XML 片段中有一个名为 employee 的标签,并包含两个子标签。它对应的 XSD 描述如下:
XSD 中的 element 标签与 XML 中的元素相对应,通过 name 属性定义标签名,type 属性定义其数据类型。上面例子中 firstname 和 lastname 两个子标签是 string 类型。
XML Schema Dependency
![](https://s4.51cto.com/images/blog/202104/19/15c6256eb8d36d3d7f586a1cd09dc885.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
在文档格式标准当中,对数据结构的定义是通过 XSD 实现的,知道数据结构定义的方式之后,我们来梳理一下在 Excel 文档当中所涉及到的数据结构都有哪些。先看左侧的树状结构,这是一个 XSD 文件中所定义的 XML 标签示意图,XSD 之间可以互相引用。对 Excel 文档所涉及的主要数据结构定义 XSD 文件进行分析和梳理,绘制出一个依赖关系图就像右边这样,最上面是 sml.xsd,它是 Excel 文档的数据结构定义所使用的主文件,sml.xsd 中对通用标记语言相关的数据结构做了引用,其中包括依赖关系处理和简单数据类型的相关的结构定义。中间比较多的以 dml- 开头的 XSD 文件是与跨应用可视化相关的数据结构定义文件,例如图片、图表、图形和 SmartArt 等相关的数据结构都是在以 dml- 开头的 XSD 文件中定义。
大家看到 dml- 开头的 XSD 文件比较多,因为它是跨应用的,里面所涉及的 XML 标签和属性大概 8000 个左右,而 sml.xsd 中定义的标签和属性也有数千个之多。
格式解读
每个 Excel 文档都是一个工作簿,也叫做 Workbook,像这样一个简单的 Excel 文档,Workbook 中包含两个工作表(Worksheet),在名为 Sheet2 的工作表中有一些文本类型的数据,在名为 Sheet1 的工作表中创建两个图表,对 Sheet2 中的源数据进行引用。对于这样的一个简单 Excel 文档,我们把它像刚才讲到的那种方式进行分析,先把它的扩展名改一下然后解压看看,它的目录结构是这样子。通过目录结构中的文件名称,我也可以看出文档内部的数据结构是如何组织的:两个工作表对应 worksheets 文件夹中的 sheet1.xml 和 sheet2.xml,对图表和颜色、属性的定义都会从目录结构上体现。
典型关系图
这些文件的依赖关系如何?这边有一个图来解释,工作簿包含多个工作表,工作表包含图表、表格、透视表,透视表又包含透视缓存和透视记录。另外,工作簿还与主题、样式、公式以及字典等部分有关联关系。我们要去实现这个标准,也要把这些关系进行梳理。
XML 结构与依赖关系处理
如何去处理一个最简单的单元格?
上图左上角是一个简单的单元格,这个单元格的坐标是 D2,值是 Q1,带有蓝色背景色和黑色的实线边框样式。下面是读取或创建这样一个单元格的基本过程:图中的这些 XML 的片段来源于目录中的不同文件,每个 c 标签代表一个单元格,s 属性值为 7 代表单元格应用了索引为 7 的样式,我们去 styles.xml 当中找到索引为 7 的样式,通过 xf 标签的 fontId 和 fillId 属性可以确定字体 ID 和 单元格填充样式 ID,borderId 和 applyBorder 属性对应边框样式的定义和边框是否开启,numFmtId 是数字格式相关的定义。根据每一个字段向下去找,可以看到字号是 11,字体是 Calibri,包括单元格填充的颜色,颜色如果用到主题色,再去主题索引查找所对应的主题,找到颜色的色值,根据具体的色值表示方法,需要对色值进行换算。后面是对于边框的处理,根据边框 ID 为 2 找到它的上下左右边框样式定义。最后样式处理完成,到这里我们还没有看到对单元格值 Q1 的定义,这个图里没有体现出 Q1 的实际存储结构。大家可以看到 v 标签的值是 0,而不是 Q1 这个字符,由于该单元格的值是字符串类型,不会随单元格存储,根据索引 0 到字典中查找就可以读到单元格的值了。上面就是一个最基本的读取单元格的过程。
上述过程涉及到对多个文件的处理,这些文件有两种依赖关系:隐式依赖和显式依赖。举例来说,如果一个单元格有一个图片,图片不随着工作表一起存储,而是放在 media 文件夹当中,工作表并没有直接去指向某一个图片的地址,而是通过 ID 去根据 .rels 查找,这就构成一种隐式依赖。如果一个单元格有一个超链接,这个超链接也不随工作表存在一起,而是定义在工作表所对应的 relationship 中,在工作表中会显示声明超链接在 relationship 中的 ID,这是一种显式依赖。
02
Go 语言实现
至此,我们对国际电子表格文档格式标准已经做了一个整体的介绍,对于文档基本结构的构成和解析方法也有了初步的认识。下面将对关于使用 Go 语言对该标准实现过程中的一些经验做分享。
Excelize 技术架构
可以将 Excelize 技术能力划分为基础能力、样式处理能力、数据处理能力、图片/图表、工作簿/工作表、单元格和模型处理,7 大部分。
- 基础能力 - 文件格式识别、媒体格式支持、元数据解析校验、OPC 封装与解构、依赖关系处理、扩展标记处理;
- 样式处理能力 - 边框样式、冻结窗格、字体样式、行高 / 列宽、数字格式、色值计算;
- 模型处理 - 模型组件化、模型校验、计算引擎、升级扩展能力、模型纠错验证;
- 图片 / 图表 - 2D / 3D 处理、簇状 / 堆积 / 面积图、柱形 / 锥形 / 棱锥 / 饼图、气泡 / 散点 / 折线图、属性设置能力;
- 工作簿 / 工作表 - 可见性设置、行 / 列处理、属性设置、工作表属性、页眉页脚、视图属性、搜索能力、数据保护、页面布局;
- 单元格 - 数据类型支持、字典、选区合并、富文本、超链接、批注处理、公式处理、样式索引、单元格样式;
- 数据处理能力 - 数据验证、时间处理、Crypto 加解密、单位转换、表格 / 过滤器、数据透视表、条件格式、VBA 脚本
对于模型的处理,后面会有代码介绍,其中涉及打开文件后,哪些部分需要加载到内存中做处理。大家可能有用过其他一些开源电子表格基础库,操作稍微复杂点的表格,很多情况下保存文件后用 Office 就打不开了,避免这类问题需要在兼容性方面做很多的处理。
图表可视化方面,Excelize 支持 52 种图表的生成,支持创建 2D 和 3D 图表,像柱状图、面积图、堆积图在实现上可以按照同一个类型的图表做处理,根据不同参数的变换来实现,还有柱形图、椎形图、饼图处理方式是相似的。属性设置能力包括了对图表坐标轴系列的绘制、网格线、透明度等属性的设置。
以上是目前 Excelize 的技术架构,后面会举三个例子介绍一下内部的具体实现。
文档结构定义
Excelize 中的 File 结构体⽤来定义⽂档模型,里面包括可导出和不可导出的字段。可导出字段包括工作表、样式、数据结构之间的引用关系等,开发者可以从外部访问到它们。在使用 Excelize 打开电子表格文档后,Excelize 会通过不可导出字段维护内部数据结构之间的依赖关系,在对电子表格进行读取或编辑的过程中,Excelize 会在运行时进行动态内存调节和纠错。
根据按需处理的设计原则,在外部 API 调⽤时,精确识别要操作哪些数据结构,按需加载所需处理的⽂档数据结构到对应的模型上,再通过内部处理器进⾏处理。这种处理⽅式在保证兼容性的同时,也提⾼了⽂档处理的性能。
Excelize 使用
1.创建电子表格文档
我在设计这个库的时候,使用方式上,也考虑到一些其他语言的开发者,希望降低开发者的学习成本,通过简单易用的函数可以实现快速接入。上面是一个创建电子表格文档的简单例子。使⽤ NewFile 新建 Excel ⼯作薄,新创建的⼯作簿中会默认包含⼀个名为 Sheet1 的⼯作表。如果需要生成比较大量的数据,Excelize 也提供了相关迭代的方法,支持在迭代循环过程中转换成相应的坐标。
2.图片图表处理
关于图片图表相关的处理,在处理图片图表的时候要考虑到兼容性,这是比较难实现的功能。在 Excelize 上添加图片和图表比较方便,基于刚才打开的文件调用 AddPicture,把图片路径传进去就可以了。如果有对图片缩放、打印效果等属性的设置,在函数的最后一个参数中声明即可,使用起来十分方便。此外还提供一个从电子表格文档中提取图片的 GetPicture 方法。
在电子表格文档中插入图片是怎么样实现的呢?对图片和图表的操作都涉及到标准中的 DrawingML 通用标记语言。在插入图片的时候,首先计算 DrawingID 是多少,接着对 relationship 进行更新,然后进行图片的数据结构调整;有些情况下图片还带有超链接,点击图片链接到 URL 或者电子表格内的某一个单元格,这里涉及到对依赖属性的处理;最后更新文件属性中对媒体格式的定义。对于图表来说更复杂一些,涉及到 Chart 模块。
上图是 Excelize 处理图片图表的逻辑时序图。在 Excelize 的 Drawing 子系统中,当开发者调用 AddChart 函数时先判断是否是首次在电子表格中添加图片或图表,再计算工作表与图片或图表之间的引用关系,得出对应的 Relationship ID 返回给内容定义部分,进行图表属性的处理,然后更新 drawing 对应的 Relationship,最后生成图表本身的数据结构,如果中间哪一步出现异常导致最后没有生成 Chart 数据,可以提前把对 Related Part 的处理过程终止,也不会影响最终生成的电子表格文档在被应用程序打开时的兼容性。
数据透视表
数据透视表在数据多维度分析领域中的很多业务场景都有使用,上图的例子中包括五列数据,有年份、月份、商品类别、商品销售额和商品销售区域,如果需要按照商品的分类、对月销售额按区域的汇总。对于这样多维度的数据分析,使用传统的条件过滤一次筛选是做不到的,而通过数据透视表可以方便的解决这个问题。
![](https://s4.51cto.com/images/blog/202104/19/3d5088910d390c5fa709dc8d326c477a.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
使用 Excelize 创建一个数据透视表也很简单,目前通过编程方式生成数据透视表的开源方法并不是很多。使用 Excelize 只需要几行代码就可以创建对应的数据透视表。根据对引用数据选区、透视表选区、筛选行和列(分别对应右侧四个象限中的筛选和排序字段)的定义,就可以生成这样的透视表。
在实现透视表功能的过程中,首先需要根据开发者定义的数据选区对数据进行计算,先生成数据透视缓存,基于数据透视缓存和数据筛选字段来生成数据透视表。
内存模型
关于内存模型的调节,大家试想一个场景,用户打开一个有 1 万行数据的 Excel 文档,打开之后有一个操作要把第 10 行删掉,那么从 11 行开始后面所有的数据,所包含的合并单元格、带有超链接的图片等所有与坐标相关的数据都会受影响,需要重新计算向上平移一行,在内存中要做实时调节,Excelize 内部有一个方法叫 adjustHelper,提供了四个参数:工作表名称,调节方向,调节基准和偏移量。调节方向包括横向和纵向,对应插入或删除行和列的情况,调节基准用以处理连续插入或删除行和列的情况。与坐标有关的电子表格数据结构:公式引用选区、数据透视表、超链接、批注、评论、分页、数据校验等,都需要做一系列的内存调节。
Go 处理多命名空间 XML 时的问题
下面分享一个在 Go 语言实现过程中遇到的问题,在实现国际电子表格文档格式标准过程中需要处理大量的 XML 的数据结构,其中涉及到很多带有命名空间的 XML。这样一段 XML:
entity 标签的子标签 a:val 有一个命名空间 a,通常情况在 Go 语言中通过定义 Entity 结构体来解析这段 XML,通过 encoding/xml 官方库进行反序列化,可以拿到 XML 原始信息,但是如果把这个结构体序列化回去,就会发现命名空间信息已经从 entity 标签转移到了子标签 val 上,而且子标签的命名空间前缀已经被丢掉了。
![](https://s4.51cto.com/images/blog/202104/19/11d8e4d67ee06813505bb2618a4de7fb.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
从 2015 年在 GitHub 上就有对这个问题的讨论,Go 语言作者之一 rsc 也对这些 issue 做了回应,不过目前还没有解决这些问题。
为了解决这个问题,定义两个结构体,在反序列化时的结构体上增加对命名空间属性的解析,在序列化时所对应的结构体上,利用 XML Tag 将命名空间前缀与属性名称通过冒号连接作为一个普通属性处理。
对于上面的例子,反序列化时将序列化得到的 XMLNSA 赋值给序列化结构体的 XMLNSA 上,通过这样的方式,可以保持序列化后的 XML 和原始的 XML 完全一致。
![](https://s4.51cto.com/images/blog/202104/19/b66552cc562ec7b454004a0440d53363.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
实现国际电子表格文档格式过程中,处理多命名空间的情况非常多。比如说 Excel 中的的迷你图(Sparkline)功能:迷你图是放入单元格中的小型图,可以展示所选内容中的一行数据的趋势。
这个功能在上面的示例文件中涉及到两个命名空间,第一个是名为 x14 的命名空间,对应的是 SparklineGroups 标签,代表在 Office 2010 版本后被支持的功能,第二个是名为 xm 的命名空间,做为 x14:SparklineGroups 的子标签,对应的是 sparkline 标签。
处理这样多命名空间的方式和刚才上面讲到的例子是一样,通过结构体的交换去补齐命名空间丢失的问题。
性能
目前 Excelize 写入 64 万个单元格要 2 秒钟左右,结合 Go 语言的并发优势,后续还有很大的优化空间。另外一个比较极端的场景测试:创建 1000 张图表耗时 10 秒。右图是 Excelize 与其他语言相关实现在生成 64 万个文本单元格下的性能表现情况,上方橙色条形图是耗时,下方蓝色条形图是内存使用情况。从上至下分别是基于 Go 语言实现的 Excelize、基于 Python 实现的 xlsxWriter、基于 Java 语言实现的 Apache POI、PHP 的 PhpSpreadsheet、还有基于 Node.JS 实现 SheetJS。综合来看 Go 语言的表现还是不错的,这是一个生成纯文本单元格的 Benchmark,Excelize 在开发之初更侧重与复杂文档格式的处理,在高级功能和兼容性上有更具优势。
03
总结
,首先向大家介绍了国际电子表格文档格式标准,以及相关背景、特点和内容;对使用 Go 语言实现该标准相关的实践做了分享,其中涉及到了一系列核心功能的实现。Excelize 的开源也是对 Go 语言生态的一份贡献,希望帮助到更多有需要的朋友。Excelize 未来的发展规划包括了公式计算引擎、透视图、VBA 脚本以及更复杂的图表操作支持和性能的不断优化,同时也欢迎各位 Gopher 通过提交 Issue、PR、Donate 等各种形式参与到开源生态的建设中。
Q & A
Q:对性能后期优化和并发的思路?
A:在大规模数据搜索场景下,可以利用 Go 语言的优势通过多个协程进行分片搜索工作表内容,提升搜索速度,另外 Excelize 也支持对 XML 的流式读取,这样可以节省读取大文件时的内存
有疑问加站长微信联系(非本文作者)