当前位置: 首页 > news >正文

go excel解析库xuri/excelize中的SAX

文章目录

    • 概要
    • 一、excel文件结构
    • 二、go官方xml库
    • 三、xuri/excelize库
        • 3.1、解压xlsx文件,得到其中的xml文件
        • 3.2、go官方xm库解析文件内容
        • 3.3、释放资源
    • 四、小节
    • 五、参考

概要

xuri/excelize(qax-os/excelize)是go相当好用的一款excel库,再加上月初导入服务磁盘告警,内存波动异常,遂看了下其读取excel文件的源码,特此记录一下。

一、excel文件结构

Office Open XML,也称为OpenXML或OOXML,是用于办公室文档的基于XML的格式,包括文字处理文档,电子表格,演示文稿以及图表,图表,形状和其他图形材料。
简单来说,OOXML是一个基于XML的文档格式标准,最早是微软Office2007的产品开发技术规范,先是成为 Ecma(ECMA-376) 的标准,最后改进推广,成为了 ISO 和 IEC (as ISO/IEC 29500) 的国际文档格式标准。我们熟知的xlsx,pptx,docx都是基于OOXML实现的,所以,通过OOXML标准,我们能够在不依赖Office的情况下,在任何平台读写Office Word,PPT和Excel文件,这也是那么多产品支持多人协作在线文档的基础。

微软OOXML SDK介绍。
所以说excel文件本质就是一堆xml文件的压缩包。
我们可以随便把一个excel文件后缀从.xlsx改成.zip,用解压工具打开,即可见其真容;
现创建了一个excel文件,包含两个sheet,内容如下:
sheet1
sheet2
用解压工具解压后可以看到下图:

xlsx.zip
其中xl目录下的worksheets/sheet1.xml对应Sheet1-top,worksheets/sheet2.xml对应Sheet1-bob,打开后发现有些数据搜索不到,那是因为字符串类型存到xl/sharedStrings.xml,通过索引关联。
打开worksheets/sheet1.xml,可以看到:

<sheetData>
<row r="1" spans="1:2">
<c r="A1">
<v>11</v> 
</c>
<c r="B1" t="s">
<v>0</v>
</c>
</row>
<row r="2" spans="1:2">
<c r="A2">
<v>22</v>
</c>
<c r="B2" t="s">
<v>1</v>
</c>
</row>
<row r="3" spans="1:2"> //一行的起点
<c r="A3"> //一个cell开始
<v>33</v>//cell中的值
</c> //一个cell开始
<c r="B3" t="s">//t是定义cell中值的类型,s表示字符串
<v>2</v> //字符串的时候,这里不是cell的值,而是索引,具体值要到xl/sharedStrings.xml文件中找,这样有效降低重复字符串对文件大小的影响。
</c>
</row>
</sheetData>

打开xl/sharedStrings.xml文件,可以看到:

<si>
<t>qw</t> 
</si>
<si>
<t>as</t>
</si>
<si>
<t>zx</t> //索引为2的数据
</si>
<si>
<t>top</t>
</si>
<si>
<t>down</t>
</si>
<si>
<t>dfg345sdfs</t>
</si>
<si>
<t>bob</t>
</si>
<si>
<t>join</t>
</si>

简单了解了excel文件结构,是不是感觉我们自己也可以写一个库来读取excel文件内容了:
1:用go官方archive/zip库解压解析excel文件,拿到sheet文件;
2:读取sheet文件内容,用go官方encoding/xml库读取xml内容,读取数据;
3:遇到字符串类型数据到xl/sharedStrings.xml文件匹配即可。
没错,xuri/excelize就是这么做的,只不过有很多细节处理,来降低解析excel文件时内存等资源的消耗。

二、go官方xml库

我是在使用Java Apache POI库时第一次了解sax概。SAX的全称是Simple API for XML,是一种基于事件驱动的XML解析方法。不同于DOM一次性读入XML,SAX会采用边读取边处理的方式进行XML操作。简单来讲,SAX解析器会逐行地去扫描XML文档,当遇到标签时会触发解析处理器,从而触发相应的事件Handler,这样在解析xml文件时相比DOM模式,内存消耗是极少的。
go官方xml库就是SAX模式:

func saxParse() {f, err := os.Open(XmlPath)if err != nil {fmt.Println("os.Open:", err)return}b := bufio.NewReaderSize(f, 1024)decoder := xml.NewDecoder(b)token, _ := decoder.Token()switch t := token.(type) {case xml.StartElement: //新XML元素事件,还有[EndElement], [CharData], [Comment], [ProcInst], or [Directive]事件fmt.Println("StartElement:", t.Name.Local)for _, attr := range t.Attr {fmt.Println("Attr:", attr.Name.Local, attr.Value)}}
}

这是基于go官方xml库实现的一个DOM模式xml解析库。

三、xuri/excelize库

如第一章所说,xuri/excelize库读取excel文件分为若干步骤。

3.1、解压xlsx文件,得到其中的xml文件

OpenReader

func OpenReader(r io.Reader, opts ...Options) (*File, error) {b, err := io.ReadAll(r)//读取压缩后的xlsx文件内容if err != nil {return nil, err}f := newFile() //初始化excel解析实例f.options = f.getOptions(opts...)if err = f.checkOpenReaderOptions(); err != nil {//检查参数return nil, err}if bytes.Contains(b, oleIdentifier) {if b, err = Decrypt(b, f.options); err != nil {return nil, ErrWorkbookFileFormat}}zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))//通过官方解压库解压xlsx文件内容if err != nil {if len(f.options.Password) > 0 {return nil, ErrWorkbookPassword}return nil, err}file, sheetCount, err := f.ReadZipReader(zr)//得到其中的xml文件if err != nil {return nil, err}f.SheetCount = sheetCountfor k, v := range file {f.Pkg.Store(k, v)}if f.CalcChain, err = f.calcChainReader(); err != nil {return f, err}if f.sheetMap, err = f.getSheetMap(); err != nil {return f, err}if f.Styles, err = f.stylesReader(); err != nil {//得到样式return f, err}f.Theme, err = f.themeReader()return f, err
}

ReadZipReader

func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {var (err     errordocPart = map[string]string{"[content_types].xml":  defaultXMLPathContentTypes,"xl/sharedstrings.xml": defaultXMLPathSharedStrings,}fileList   = make(map[string][]byte, len(r.File))worksheets intunzipSize  int64)for _, v := range r.File {fileSize := v.FileInfo().Size()unzipSize += fileSizeif unzipSize > f.options.UnzipSizeLimit {return fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit)}fileName := strings.ReplaceAll(v.Name, "\\", "/")if partName, ok := docPart[strings.ToLower(fileName)]; ok {fileName = partName}//fileSize > f.options.UnzipXMLSizeLimit时才会将文件存到一个临时文件,便于后续sax解析,否则整个文件内容直接加载到内存中if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit {tempFile, err := f.unzipToTemp(v)//将xl/sharedstrings.xml 文件内容存到一个临时文件,linux默认在/tmp目录下if tempFile != "" {f.tempFiles.Store(fileName, tempFile)}if err == nil {continue}}if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") {worksheets++if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() {tempFile, err := f.unzipToTemp(v)//将excel中各个sheet内容也存到一个临时文件if tempFile != "" {f.tempFiles.Store(fileName, tempFile)}if err == nil {continue}}}if fileList[fileName], err = readFile(v); err != nil {//其他文件内容直接加载到内存中return nil, 0, err}}return fileList, worksheets, nil
}
3.2、go官方xm库解析文件内容

Rows

func (f *File) Rows(sheet string) (*Rows, error) {  //...省略var err errorrows := Rows{f: f, sheet: name}rows.needClose, rows.decoder, rows.tempFile, err = f.xmlDecoder(name)//解析文件内容return &rows, err
}
func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {var (content  []byteerr      errortempFile *os.File)if content = f.readXML(name); len(content) > 0 {return false, f.xmlNewDecoder(bytes.NewReader(content)), tempFile, err}tempFile, err = f.readTemp(name) //缓存中没有,就读取相应临时文件内容return true, f.xmlNewDecoder(tempFile), tempFile, err
}

Columns

func (rows *Rows) Columns(opts ...Options) ([]string, error) {//...省略for {if rows.token != nil {token = rows.token} else if token, _ = rows.decoder.Token(); token == nil {break}switch xmlElement := token.(type) { //SAX方式解析xmlcase xml.StartElement:rowIterator.inElement = xmlElement.Name.Localif rowIterator.inElement == "row" {rowNum := 0if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 {rows.curRow = rowNum} else if rows.token == nil {rows.curRow++}rows.token = tokenrows.seekRowOpts = extractRowOpts(xmlElement.Attr)if rows.curRow > rows.seekRow {rows.token = nilreturn rowIterator.cells, rowIterator.err}}if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {//解析cell值rows.token = nilreturn rowIterator.cells, rowIterator.err}rows.token = nilcase xml.EndElement:if xmlElement.Name.Local == "sheetData" {return rowIterator.cells, rowIterator.err}}}return rowIterator.cells, rowIterator.err
}
func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) {//...省略if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil {//获取cell值rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val)}//...省略
}
func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {f.Lock()defer f.Unlock()switch c.T {case "b":return c.getCellBool(f, raw)//布尔case "d":return c.getCellDate(f, raw)//日期case "s": //字符串if c.V != "" {xlsxSI := 0xlsxSI, _ = strconv.Atoi(strings.TrimSpace(c.V))//获取索引值if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {//到xl/sharedstrings.xml文件,第一次直接将文件中有效数据以SAX方式加载到内存,存在数组中return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw)//按索引匹配}if len(d.SI) > xlsxSI {return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw)}}return f.formattedValue(c.S, c.V, raw)case "inlineStr":if c.IS != nil {return f.formattedValue(c.S, c.IS.String(), raw)}return f.formattedValue(c.S, c.V, raw)default:if isNum, precision, decimal := isNumeric(c.V); isNum && !raw {if precision > 15 {c.V = strconv.FormatFloat(decimal, 'G', 15, 64)} else {c.V = strconv.FormatFloat(decimal, 'f', -1, 64)}}return f.formattedValue(c.S, c.V, raw)}
}
3.3、释放资源

最后需要通过Close手动释放资源,先释放sheet级别的,即row.Close(),在释放excel实例级别的,即f.Close()。
row.Close

func (rows *Rows) Close() error {tempFile := rows.tempFilerows.tempFile = nilif tempFile != nil {//sheet文件如果因为过大存了一份临时文件,最后要被关闭return tempFile.Close()}return nil
}

f.Close

func (f *File) Close() error {var firstErr errorif f.sharedStringTemp != nil {firstErr = f.sharedStringTemp.Close()//把xl/sharedstrings.xml临时文件File实例释放f.sharedStringTemp = nil}for _, stream := range f.streams {_ = stream.rawData.Close()}f.streams = nilf.tempFiles.Range(func(k, v interface{}) bool {//把临时文件删除了if err := os.Remove(v.(string)); err != nil && firstErr == nil {firstErr = err}return true})f.tempFiles.Clear()return firstErr
}

四、小节

通过第三章分析,可以在使用xuri/excelize库读取excel文件内容时要注意的地方:
1:可以在打开excel文件时用Options参数设置UnzipSizeLimit和UnzipXMLSizeLimit两个配置值;

UnzipSizeLimit:如果xlsx文件大于该值,直接报错;
UnzipXMLSizeLimit:如果sheet文件大于该值,会将文件临时存储下,转为SAX解析,否则直接将文件内容全部加载到内存。

2:一定要关闭资源,即执行row.Close()和f.Close(),报错要重试几次,如果临时文件删除失败,就是留下来了,消耗磁盘。

五、参考

1]:open-xml
2]:Java一分钟之-XML解析:DOM, SAX, StAX
3]:玩转Excel,一定要懂点儿运行逻辑和结构
4]:OOXML:Excel(xlsx)是什么
5]:聊聊Excel解析:如何处理百万行EXCEL文件

相关文章:

  • 【PyTorch项目实战】CycleGAN:无需成对训练样本,支持跨领域图像风格迁移
  • 开关电源:BUCK和BOOST
  • NotePad++ 怎么没有找到插件管理?
  • C++ 友元:打破封装边界的“特殊权限”
  • LangChain赋能RAG:从构建到评估优化的一体化实战指南
  • 跨平台多路RTSP/RTMP转RTMP推送模块深度解析
  • Python函数实战:从基础到高级应用
  • ABP VNext + gRPC 双向流:实时数据推送与订阅场景实现
  • 量化-因子处理
  • 原创模板--微信小程序 实现的背单词程序
  • GESP C++ 各等级详细知识点汇总
  • 从单口相声到群口辩论:MultiTalk开源:多角色对话生成SOTA模型,语音-视觉对齐精度达98.7%!
  • Linux 下的 socket
  • [project-based-learning] 开源贡献指南 | 自动化链接验证 | Issue模板规范
  • 【机器学习】数学基础——张量(进阶篇)
  • JVM——Synchronized:同步锁的原理及应用
  • 顶顶通大模型电话机器人实现原理
  • [论文阅读] 软件工程 + 教学 | 软件工程项目管理课程改革:从传统教学到以学生为中心的混合式学习实践
  • ELMo 说明解析及用法
  • 高线性低噪放:精密ADC信号链的守护者
  • 怎么做网站的搜索功能/seo排名如何
  • wordpress展示型外贸网站/旺道seo优化软件
  • iis 设置网站不能访问/灯塔网站seo
  • 返利导购网站建设需求文档/公司网站设计制作
  • 用css3做酷炫网站/刷关键词排名系统
  • 网站ip地址 a记录/国内seo排名分析主要针对百度