第一期主题:
在Go中国社区首届Go Open Talk线上活动中,开源项目的作者们继续以“开源背后的故事”为主题进行分享。
关于作者
徐日,软件工程师,专注于Go语言实践、中间件开发和大规模数据处理。 目前就职于阿里巴巴,曾就职于百度、奇虎360等公司。曾任百度Go语言编程委员会委员,在百度期间从事内部Go语言开发框架和代码规范相关工作。 他热爱开源,会在他的网站上发布一些开源项目:
执行摘要
本次分享的内容主要包括四个部分:项目的初衷和发展历史、实施过程、设计理念和实际应用。
关于
它是一个用Go语言编写的用于操作Excel文档的基础库,基于ECMA-376和ISO/IEC 29500国际标准。 您可以使用它来读取和写入由 Excel™ 2007 及更高版本创建的电子表格文档。 支持XLSX/XLSM/工作日志等多种文档格式。 可应用于各种报表平台、云计算、边缘计算等系统。
入选2020中国围棋之星开源项目(GSP)和2018开源中国码云最具价值开源项目(Gitee Most)。
:
项目初衷
每个人都熟悉电子表格。 以Excel为代表的经典电子表格已经应用于各行各业。 据相关研究机构测算,每年产生的办公文档数量至少以数十亿的速度增长。 作为数据的重要载体,电子表格文档被应用于许多领域。 作为开发人员,在某些情况下,您需要使用编程来操作这些电子表格文档。 最初,为了满足报表系统导出数据的需要,作者不断研究市场上多种主流语言的相关基础库,希望找到一个高性能、复杂风格、跨平台的- 平台解决方案。 但经过一番查找,他并没有找到能够满足业务复杂需求的开源实现。 于是他决定从文档格式标准入手,从头开始使用Go语言。 实现一个考虑性能和兼容性的电子表格文档基础库。 总结起来,启动该项目的初衷可以概括为以下6点:
发展路径
在开源之前,它已经在公司内部和多个在线产品中得到应用。 2016年9月正式开源时,可以满足很多常见的Excel文档操作的需求。 除了基本的工作簿、工作表和单元格操作外,还具有插入图片、图表和合并单元格的功能。 经过四年多的开发,目前已经发布了14个版本。 让我们回顾一下 的发展历史。 以下是各个版本中代表性功能的一些示例(在 的页面和文档网站上有更详细的发布记录):
实施过程
在设计之初,Xuri调研了主流编程语言的开源和商业解决方案,分析了开源领域相关基础库的设计和源代码,阅读了电子表格文档格式的国际标准。 使用过软件的朋友可能知道,同一个文档在不同的办公软件中打开时,可能会出现修复不一致的问题,比如风格细节的差异; 开发者借助第三方基础库,采用编程方式操作电子表格文档,尤其是在处理包含复杂样式的文档时,可能会出现保存的文档无法正确打开、提示“文档已损坏”或部分数据丢失等问题。 造成这些问题的原因通常是由于这些办公应用程序或基础库对文档进行了格式化。 标准实施不一致、不完整或存在一定缺陷。 如果想避免这个问题,就需要深入了解复杂的文档格式标准。 这也是实现基本电子表格库的难点和挑战之一。
ECMA-376、ISO/IEC 29500是办公文档的国际标准(以下简称技术标准),对应我们熟悉的三大办公应用程序生成的文档:Word、Excel等。 它是一套非常庞大且复杂的技术标准。 其官方文档有7000多页内容,Excel文档的相关部分也有2000多页。 此外,Excel中的一些功能还涉及制造商技术标准。 例如,对加密文档的支持需要实施 MS 技术标准。 这里我就不详细说了。 接下来主要介绍ECMA-376和ISO/IEC 29500标准。 主要内容。
了解了相关的技术标准后,我们开始使用Go语言来实现技术标准。 2019年12月,徐日做了题为《Go语言国际电子表格文档格式标准实践》的技术分享,详细介绍了这个国际标准的特点以及使用Go语言实现这个标准的相关实践。 有兴趣的朋友可以在GoCN社区阅读相关文章。
操作电子表格文档离不开对其内部数据结构的处理。 由于电子表格文档涉及多种数据结构,手动编写代码既费时、费力,又容易出错。 那么,它能通过标准文档中给出的规则吗? 数据结构描述自动生成Go语言的结构代码。 因此,我设计了一个数据结构代码生成器。
公式计算也是电子表格的主要功能之一。 如果您想在不依赖电子表格应用程序的情况下计算表格文档中的公式,则需要实现一个计算引擎,其中涉及词法分析和Excel公式的解析。 等待。
技术标准
ECMA-376、ISO/IEC 29500作为国际文档格式标准,是一种基于XML和ZIP技术的文档格式。 DOCX / XLSX / PPTX 等常见文档文件都遵循此规范。
下面介绍该标准的主要内容。 上层是标记语言部分( ),由四种标记语言组成:调用Word文档对应的标记语言、调用电子表格对应的标记语言、调用演示文稿对应的标记语言。 这部分主要是实现。 此外,文档支持跨应用程序嵌套。 例如,Word可以嵌套Excel,Excel可以嵌套Word。 通用标记是一种跨应用程序的文本标记语言:包括可视化图表、可扩展标记、源数据和目录引用等。 中间层称为开放封装约定(Open),简称OPC,定义了文档内部组件之间的依赖关系()、文档内容类型(Types)定义和数字签名()。 底层基于ZIP、XML和. 我们可以创建一个Excel文档,将其扩展名更改为zip,然后将其解压,得到一个包含多个子文件夹和XML文档的文件夹。 这种设计具有很强的可扩展性,相比二进制文档格式也具有更好的兼容性。
电子表格文档中 XML 文件的布局和依赖关系是什么? 这里用一张图来解释一下:
工作簿()包含多个工作表()。 工作表包括图表、表格、数据透视表等。数据透视表还包括数据透视缓存和数据透视记录。 (枢)。 此外,工作簿还涉及主题(Theme)、风格(Style)、公式计算链(Calc Chain)、共享字符表( )等。 要实施这个技术标准,首先需要理清这些关系。
数据结构代码生成器
这里引入一个技术术语——XSD(XML),它是W3C(万维网联盟)推出的一个技术规范,用于定义XML 。 XML文件理论上可以有无限的标签和属性,而XSD是一种用于描述XML的模式语言。 在文档格式技术标准中,有XSD描述的数据结构定义。 下图是基于技术标准中的XSD模式语言绘制的树形结构片段,用于描述电子表格文档的数据结构:
可以看到Excel中XML标签的根节点、命名空间和自动()相关数据结构的定义,以及XML中相关节点的嵌套关系、标签属性名称和定义数据类型。 描述 XML 的 XSD 也可以互相引用。 分析整理Excel文档中涉及的主要数据结构定义XSD文件,并绘制如下依赖关系图:
sml.xsd是用于Excel文档的数据结构定义的主文件。 sml.xsd引用了通用标记语言相关的数据结构,包括依赖处理和简单数据类型的相关结构定义。 最常见的以 dml- 开头的 XSD 文件是与跨应用程序可视化相关的数据结构定义文件。 例如图片、图表、曲线图等相关数据结构都是在以dml-开头的XSD文件中定义的。 可以看到有很多以dml-开头的XSD文件,因为它们是跨应用的。 涉及到的XML标签和属性大约有8000个,sml.xsd中定义的标签和属性多达2589个。 在实现过程中,需要处理大量的XML,并需要定义相应的数据结构(),称为数据模型。 由于数据模型数量众多,嵌套关系错综复杂,为了实现结构代码生成的自动化,衍生出了一个数据结构代码生成器xgen(XSD):
xgen -i /path/to/your/xsd -o /path/to/your/output -l Go
-i参数指定输入源(input),可以传入XSD目录,-o参数指定代码生成()的输出路径,-l参数用于指定生成代码的编程语言()。 该工具也是开源的:。 这样我们就获得了操作数据模型所需的结构定义代码。
架构设计
技术能力分为基础能力、样式处理能力、数据处理能力、图片/图表、工作簿/表格、单元格和模型处理,7大部分:
公式词法分析器和解析器
使用提供的API,您可以轻松设置单元格的公式。 也可以在不依赖电子表格应用程序的情况下读取和评估设定的公式。 下面我们通过一个例子来说明公式求值内部是如何实现的。 的。 对于这样一个公式(为了便于理解,下面的公式只包含操作数、运算符和函数,其中的人工数字可以替换为实际场景中单元格的地址,并参考值进行运算):
=1+SUM(SUM(1,2*3),4)
公式的词法分析和语法分析都是用Go语言编写的。 首先基于有限状态机和堆栈令牌生成器通过efp(Excel)对公式文本字符进行词法分析:
import "github.com/xuri/efp"
// ...
ps := efp.ExcelParser()
ps.Parse("=1+SUM(SUM(1,2*3),4)")
println(ps.PrettyPrint())
获取以下令牌:
1 <Operand> <Number>
+ <OperatorInfix> <Math>
SUM <Function> <Start>
SUM <Function> <Start>
1 <Operand> <Number>
, <Argument> <>
2 <Operand> <Number>
* <OperatorInfix> <Math>
3 <Operand> <Number>
<Function> <Stop>
, <Argument> <>
4 <Operand> <Number>
<Function> <Stop>
这样就完成了公式的词法分析。
公式引擎
了解了公式的词法分析之后,我们再来看看公式引擎的设计。 对中缀表达式求值,需要5个栈(OPD:操作数、OPT:运算符、OPF:函数、OPFD:函数操作数、OPFT:函数运算符)和1个链表(ARGS:函数参数)来实现,运算过程状态如图所示如下图所示:
通过上述过程,即可实现公式的求值运算。 具体代码请参考源码calc.go文件中的函数。 公式函数的动态调用入口如下:
// call formula function to evaluate
result, err := callFuncByName(&formulaFuncs{},
strings.NewReplacer(
"_xlfn", "", ".", "").Replace(
opfStack.Peek().(efp.Token).TValue),
[]reflect.Value{
reflect.ValueOf(argsList),
})
当执行公式函数调用时,OPF(函数栈)栈顶元素的类型被推断为Token,该元素的值就是函数名。 首先对函数名进行预处理,然后进行函数调用。 实现如下: 表示将根据给定的函数、函数名(name)和函数参数()进行函数调用:
// callFuncByName calls the no error or only
// error return function with reflect by given
// receiver, name and parameters.
func callFuncByName(
receiver interface{},
name string, params []reflect.Value) (
result string, err error) {
function := reflect.ValueOf(
receiver).MethodByName(name)
if function.IsValid() {
rt := function.Call(params)
if len(rt) == 0 {
return
}
if !rt[1].IsNil() {
err = rt[1].Interface().(error)
return
}
result = rt[0].Interface().(string)
return
}
err = fmt.Errorf("not support %s function",
name)
return
}
这样,只需要实现一系列函数签名与形参数据类型一致的公式函数,比如实现求和公式SUM:
func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error)
余弦三角函数 COS:
func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error)
中值函数:
func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error)
这样就避免了定义公式函数名与函数之间的映射关系,具有高度的可扩展性。 开发人员可以继续实现其他公式函数或基于此模式创建自定义公式函数。
设计理念
它是用Go语言编写的,支持并发设置单元格值。 在利用语言性能的同时,也可以轻松方便地运行在各种平台上:Linux、macOS、嵌入式系统。
第一个原则是从一开始就确保文档兼容性,这需要理解文档内的大量数据结构,并在运行时提供兼容性检查、模型验证和纠错能力。
API 的设计力求众所周知且尽可能简洁。 一个功能只能用一种方式实现,避免开发者在使用过程中陷入陷阱。 对新手友好,API的设计也考虑到其他语言开发者的使用习惯,降低学习成本。
2016年刚发布时,Go语言官方文档godoc还不支持插入图片,电子表格功能复杂多样。 很多场景下,通过图像描述,开发者更容易理解。 目前官方文档支持9种语言。
在满足业务需求的同时,它是开源的。 希望为Go语言生态的发展做出贡献,通过开源帮助更多的朋友。 没想到该项目一开源就收到了社区的大量反馈,解决了很多有相同需求的开发者的痛点,并应用于很多不同的应用场景。
通过开源,我们收到了社区开发者的很多建议和反馈。 感谢朋友们的支持!与开源社区保持积极互动,让项目更加完善,吸引更多开发者参与,通过 Pull、Issue、捐赠等方式支持开源产品,促进开源项目的发展和其他方法。
实际应用
下面通过4个典型使用场景介绍该对的实际应用。
通过以下代码,打开一个名为Book1的工作簿,并通过API在指定的单元格区域上创建图表。 目前支持52种图表。 每个图表的枚举值可以在文档网站上查询。 图表格式有很多可配置的参数,例如图例项、折线图的线宽和线端类型、气泡图的气泡大小、坐标轴的刻度步长等。
灵活运用本示例,开发人员还可以将创建的带有个性化设计图表的电子表格文档作为模板,打开它,通过API修改图表引用的数据区域单元格的值,并保存为新文件。 实现了基于自定义模板的电子表格文档生成。
f, err := excelize.OpenFile("Book1.xlsx")
if err != nil {
fmt.Println(err)
return
}
f.AddChart("Sheet1", "E1", `{
"type": "col3DClustered",
"series": [
{
"name": "Sheet1!$A$2",
"categories": "Sheet1!$B$1:$D$1",
"values": "Sheet1!$B$2:$D$2"
},
{
"name": "Sheet1!$A$3",
"categories": "Sheet1!$B$1:$D$1",
"values": "Sheet1!$B$3:$D$3"
},
{
"name": "Sheet1!$A$4",
"categories": "Sheet1!$B$1:$D$1",
"values": "Sheet1!$B$4:$D$4"
}],
"title": { "name": "三维簇状柱形图" }
}`)
数据透视表是一种交互式表格,是计算、汇总和分析数据的强大工具。 它可以帮助我们理解数据中的比较、模式和趋势。 通过提供的 API,我们在包含 5 列源数据的工作表上创建一个数据透视表:
f.AddPivotTable(&excelize.PivotTableOption{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$24",
Rows: []excelize.PivotTableField{
{Data: "月",
DefaultSubtotal: true},
{Data: "年"}},
Filter: []excelize.PivotTableField{
{Data: "区域"}},
Columns: []excelize.PivotTableField{
{Data: "类型",
DefaultSubtotal: true}},
Data: []excelize.PivotTableField{
{Data: "销售额",
Name: "销售额汇总",
Subtotal: "Sum"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
})
您可以在数据透视表中指定字段、过滤项、行/列数据、聚合维度等分析条件。 上面的例子实现了按月对各地区在售商品的销售情况进行分类汇总,并支持按销售区域、时间、品类进行过滤。
为了处理包含大规模数据的电子表格文档,提供了与流式相关的API:可以使用行/列迭代器进行流式读取,可以使用流式写入器生成大文件。 在下面的示例中,我们首先通过 API 创建流式写入器,然后创建字体样式,并将数据逐行写入工作表。 同时我们还可以指定单元格样式,写入后调用Flush。 流式写入过程结束时,将创建一个包含行 * 50 列、总共 512 万个单元格的工作表。 对于需要生成大规模数据的场景,流式API相比普通写入在时间消耗和内存占用方面具有明显的优势。
streamWriter, err := file.NewStreamWriter("Sheet1")
if err != nil {
fmt.Println(err)
}
styleID, err := file.NewStyle(
`{"font":{"color":"#777777"}}`)
if err != nil {
fmt.Println(err)
}
if err := streamWriter.SetRow("A1",
[]interface{}{
excelize.Cell{
StyleID: styleID, Value: "Data",
},
}); err != nil {
fmt.Println(err)
}
for rowID := 2; rowID <= 102400; rowID++ {
row := make([]interface{}, 50)
for colID := 0; colID < 50; colID++ {
row[colID] = rand.Intn(640000)
}
cell, _ := excelize.CoordinatesToCellName(1,
rowID)
if err := streamWriter.SetRow(cell,
row); err != nil {
fmt.Println(err)
}
}
streamWriter.Flush()
轻松融入线下、线上业务场景。 在以下示例中,我们创建一个简单的 HTTP 服务器,用于接收上传的电子表格文档,将新工作表添加到接收的电子表格文档中,并返回下载响应:
func main() {
http.HandleFunc("/process", process)
http.ListenAndServe(":8090", nil)
}
使用的 API 打开数据流,然后处理内存中的电子表格:
func process(w http.ResponseWriter, req *http.Request) {
file, _, err := req.FormFile("file")
if err != nil {
fmt.Fprintf(w, err.Error())
return
}
defer file.Close()
f, err := excelize.OpenReader(file)
if err != nil {
fmt.Fprintf(w, err.Error())
return
}
f.NewSheet("NewSheet")
w.Header().Set("Content-Disposition",
"attachment; filename=Book1.xlsx")
w.Header().Set("Content-Type",
req.Header.Get("Content-Type"))
if _, err := f.WriteTo(w); err != nil {
fmt.Fprintf(w, err.Error())
}
return
}
下图是一个典型的主流编程语言的电子表格开源基础库,基于普通个人电脑(操作系统:macOS 10.15.7,CPU:3.4 GHz Intel Core i5,RAM:16 GB 2400 MHz DDR4,HDD:1 TB) )生成 50 行列纯文本单元格的性能:
耗时情况
记忆性能
结论
总结一下今天的主题,本期Go开源漫谈首先向大家介绍一下开源项目的初衷和发展过程; 实施过程中的多项核心技术点:国际电子表格文档格式标准和公式计算引擎的设计; 设计理念和典型实例实际应用场景。 开源希望能够帮助到更多有需要的朋友,未来也会不断完善和优化。 同时,也欢迎您通过提交Issue、Pulls、Stars等形式参与开源生态的建设。
提问时间
问:目前在哪些项目中使用?
答:很多企业应用在数据输入输出环节都有电子表格文档自动处理的需求,比如将电子表格中的数据导入到系统中。 为了完成数据闭环,一般都有导出功能。 在这些场景中已经有了广泛的应用。
问:是否同时兼容Excel和WPS?
答:它已经实现了电子表格文档的国际标准。 Excel、WPS、以及在线应用程序:Docs、 等都按照相同的标准支持 XLSX 电子表格文档。
Q:是否可以将打开的文件对象序列化存入数据库,然后读取时反序列化?
A:是的,提供了打开数据流的API和写入数据流的API。
Q:边缘计算场景有应用吗?
A:已经有开发者将其应用到其他一些嵌入式系统中,在本地终端上进行电子表格文档操作。
问:您认为从事开源项目最困难的部分是什么?
A:开源不仅仅是开放源代码那么简单,它需要持续的努力。 最难的是要有持续的热情,不仅要经营“生活”,还要“养育”。
Q:在做开源项目的过程中,有哪些点激励你去做这件事?
A:来自开源社区和技术交流群的朋友对项目的支持,线下技术分享活动中与使用该项目的开发者的交流,了解其如何应用到各种业务中,以及Go中文组织的开源社区从项目选择等方面的认可,是项目不断发展的动力。
Q:在做开源的过程中给自己带来了哪些收获?
A:做开源是一个和社区互动、相互促进的过程。 同时,对开源项目价值的认可也给我带来了一些荣誉。 更重要的是,它扩大了我的社交圈,结交了很多朋友。 这些都给我带来了收获。
《Go Open Talk》栏目诚邀开源先驱者。 如果您有优秀的Go开源项目,请点击阅读原文并推荐或推荐。 Go China愿意帮助普及你的开源项目~