首期 Go 開源說實錄:Excelize 開源背后的故事

電子表格辦公文檔有著廣泛的應用 自動化電子表格文檔處理系統(tǒng)與云計算、邊緣計算場景融合 開發(fā)者需要通過編程方式處理 Excel 文檔 對復雜或包含大規(guī)模數(shù)據(jù)的 Excel 文檔進行操作的需求 文檔格式領域難以完全實現(xiàn)的復雜技術標準 開源領域、Go 語言缺少具備良好兼容性的基礎庫
v1.0.0 (2016-09-19) 首次發(fā)布:支持插入圖片、圖表、合并單元格等 v1.1.0 (2017-08-19) 支持條件格式、復制工作表 v1.2.0 (2017-12-01) 兼容 Go 1.9,支持堆積圖表 v1.3.0 (2018-05-11) 行列分組、單元格批量賦值 v1.4.0 (2018-08-14) 支持數(shù)據(jù)驗證、色值轉換 v1.4.1 (2019-01-01) 搜索單元格、保護工作表 v2.0.0 (2019-05-02) 引入流式讀寫,性能進一步提升 v2.0.1 (2019-07-01) 頁眉頁腳、文檔屬性設置 v2.0.2 (2019-10-09) 嵌入 VBA 工程、數(shù)據(jù)透視表 v2.1.0 (2020-02-10) 支持非 UTF-8 編碼文檔 v2.2.0 (2020-05-11) 支持圖表工作表、富文本、分頁 v2.3.0 (2020-08-06) 新增工作表視圖屬性設置 v2.3.1 (2020-09-23) 兼容 Go 1.15,支持讀加密 Excel v2.3.2 (2021-01-04) 圖表、數(shù)據(jù)透視表功能增強,更好的兼容性與更快的流式讀寫
調研市面上主流的技術實現(xiàn)
理解相關技術標準 ECMA-376,ISO/IEC 29500,MS-OFFCRYPTO
使用 Go 語言編寫代碼實現(xiàn)功能
數(shù)據(jù)結構代碼生成器的設計
公式詞法 / 語法分析、計算框架
文檔格式標準解讀 
電子表格文檔格式典型關系



xgen -i /path/to/your/xsd -o /path/to/your/output -l Go
-i 參數(shù)指定了輸入源(input),可以傳入 XSD 目錄,-o 參數(shù)指定了代碼生成的輸出路徑(output),-l 參數(shù)用于指定生成代碼的編程語言(language),該工具也開源到了 GitHub 上:https://github.com/xuri/xgen。這樣我們就獲得了操作數(shù)據(jù)模型所需要的結構體定義代碼了。
基礎能力 - 文件格式識別、媒體格式支持、元數(shù)據(jù)解析校驗、OPC 封裝與解構、依賴關系處理、擴展標記處理; 樣式處理能力 - 邊框樣式、凍結窗格、字體樣式、行高 / 列寬、數(shù)字格式、色值計算; 模型處理 - 模型組件化、模型校驗、計算引擎、升級擴展能力、模型糾錯驗證; 圖片 / 圖表 - 2D / 3D 處理、簇狀 / 堆積 / 面積圖、柱形 / 錐形 / 棱錐 / 餅圖、氣泡 / 散點 / 折線圖、屬性設置能力; 工作簿 / 工作表 - 可見性設置、行 / 列處理、屬性設置、工作表屬性、頁眉頁腳、視圖屬性、搜索能力、數(shù)據(jù)保護、頁面布局和流式讀寫; 單元格 - 數(shù)據(jù)類型支持、字典、選區(qū)合并、富文本、超鏈接、批注處理、公式處理、樣式索引、單元格樣式和計算緩存處理; 數(shù)據(jù)處理能力 - 數(shù)據(jù)驗證、時間處理、Crypto 加解密、單位轉換、表格 / 過濾器、數(shù)據(jù)透視表、條件格式、VBA 腳本
=1+SUM(SUM(1,2*3),4)
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>

通過上述過程即可實現(xiàn)對公式的求值運算,具體代碼可參考 Excelize 源代碼 calc.go 文件中的 evalInfoxExp 函數(shù)。公式函數(shù)的動態(tài)調用入口如下:
// call formula function to evaluateresult, err := callFuncByName(&formulaFuncs{},strings.NewReplacer("_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue),[]reflect.Value{reflect.ValueOf(argsList),})
當執(zhí)行公式函數(shù)調用時,將 OPF(函數(shù)棧)棧頂元素類型推斷為 Token,元素的值即為函數(shù)名,先對函數(shù)名稱做預處理,然后進行函數(shù)調用,其中 callFuncByName 的實現(xiàn)如下面的代碼所示,它會根據(jù)給定的函數(shù) Receiver、函數(shù)名稱(name)和函數(shù)參數(shù)(params)進行函數(shù)調用:
// 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}
這樣僅需實現(xiàn)一些列函數(shù)簽名與 callFuncByName 形參數(shù)據(jù)類型相一致的公式函數(shù)即可,例如實現(xiàn)求和公式 SUM:
func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error)余弦三角函數(shù) COS:
func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error)中位數(shù)函數(shù) MEDIAN:
func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error)這樣避免了定義公式函數(shù)名與函數(shù)的映射關系,并具備高度的可擴展性,開發(fā)者可以根據(jù)此模式繼續(xù)實現(xiàn)其他公式函數(shù)或創(chuàng)建自定義公式函數(shù)。
使用 Go 語言編寫,得益于其跨平臺、高性能的優(yōu)勢
兼容性第一原則,以高保真無損編輯為目標
簡潔明了的 API 設計,遵循最小可用原則,兼顧其他語言開發(fā)者
圖文并貌的多國語言參考文檔 https://xuri.me/excelize
代碼開源,豐富 Go 語言生態(tài)
時刻與開源社區(qū)保持積極互動
動態(tài)度表

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": "三維簇狀柱形圖" }}`)
數(shù)據(jù)透視
數(shù)據(jù)透視表是一種交互式的表,是計算、匯總和分析數(shù)據(jù)的強大工具,可以幫助我們了解數(shù)據(jù)中的對比情況、模式和趨勢。通過 Excelize 提供的 AddPivotTable API 我們在一個包含 5 列源數(shù)據(jù)的工作表上創(chuàng)建一個數(shù)據(jù)透視表:

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: "區(qū)域"}},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,})
PivotTableOption 中可以指定數(shù)據(jù)透視表中的字段、篩選項、行/列數(shù)據(jù)、聚合維度等多種分析條件。上面的例子實現(xiàn)了按月對各區(qū)域在售的商品銷售額進行分類匯總,并支持按銷售區(qū)域、時間和分類進行篩選。
大規(guī)模數(shù)據(jù)處理
NewStreamWriter API 創(chuàng)建了一個流式寫入器,接著創(chuàng)建了字體樣式,并按行將數(shù)據(jù)寫入工作表中,與此同時還可以指定單元格的樣式,寫入結束后調用 Flush 結束流式寫入過程,創(chuàng)建了一個包含 102400 行 * 50 列累計 512 萬單元格的工作表。對于需要生成大規(guī)模數(shù)據(jù)的場景,流式 API 相比普通寫入在耗時和內存占用方面都有明顯的優(yōu)勢。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()
與 Web 應用集成
func main() {http.HandleFunc("/process", process)http.ListenAndServe(":8090", nil)}
OpenReader API 打開數(shù)據(jù)流,接著在內存中對電子表格進行處理: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}
性能表現(xiàn)




