文章目录
概要
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,内容如下:
用解压工具解压后可以看到下图:
其中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)
}
}
}
三、xuri/excelize库
如第一章所说,xuri/excelize库读取excel文件分为若干步骤。
3.1、解压xlsx文件,得到其中的xml文件
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 = sheetCount
for 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
}
func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
var (
err error
docPart = map[string]string{
"[content_types].xml": defaultXMLPathContentTypes,
"xl/sharedstrings.xml": defaultXMLPathSharedStrings,
}
fileList = make(map[string][]byte, len(r.File))
worksheets int
unzipSize int64
)
for _, v := range r.File {
fileSize := v.FileInfo().Size()
unzipSize += fileSize
if 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库解析文件内容
func (f *File) Rows(sheet string) (*Rows, error) {
//...省略
var err error
rows := 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 []byte
err error
tempFile *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
}
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方式解析xml
case xml.StartElement:
rowIterator.inElement = xmlElement.Name.Local
if rowIterator.inElement == "row" {
rowNum := 0
if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 {
rows.curRow = rowNum
} else if rows.token == nil {
rows.curRow++
}
rows.token = token
rows.seekRowOpts = extractRowOpts(xmlElement.Attr)
if rows.curRow > rows.seekRow {
rows.token = nil
return rowIterator.cells, rowIterator.err
}
}
if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {//解析cell值
rows.token = nil
return rowIterator.cells, rowIterator.err
}
rows.token = nil
case 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 := 0
xlsxSI, _ = 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.tempFile
rows.tempFile = nil
if tempFile != nil {//sheet文件如果因为过大存了一份临时文件,最后要被关闭
return tempFile.Close()
}
return nil
}
func (f *File) Close() error {
var firstErr error
if f.sharedStringTemp != nil {
firstErr = f.sharedStringTemp.Close()//把xl/sharedstrings.xml临时文件File实例释放
f.sharedStringTemp = nil
}
for _, stream := range f.streams {
_ = stream.rawData.Close()
}
f.streams = nil
f.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(),报错要重试几次,如果临时文件删除失败,就是留下来了,消耗磁盘。
示例如下:
func excelRead(fileName string) {
f, err := excelize.OpenFile(fileName, excelize.Options{
UnzipSizeLimit: 8 * 1024 * 1024, //限制xlsx文件大小
UnzipXMLSizeLimit: 6 * 1024 * 1024, //sheet大小,超过转SAX模式读取
}) //打开文件
if err != nil {
fmt.Println("excelize.OpenFile:", err)
return
}
defer func() {
if err = f.Close(); err != nil { //失败了最好重试下,避免临时文件删除失败,导致磁盘空间浪费
fmt.Println("f.Close:", err)
}
}()
rows, err := f.Rows("Sheet1")
if err != nil {
fmt.Println("f.Rows:", err)
return
}
defer func() {
if err = rows.Close(); err != nil {
fmt.Println("rows.Close:", err)
}
}()
var row []string
for rows.Next() {
row, err = rows.Columns()
if err != nil {
fmt.Println("rows.Columns:", err)
return
}
fmt.Println(row) //处理一行数据
}
return
}
五、参考
1]:open-xml
2]:Java一分钟之-XML解析:DOM, SAX, StAX
3]:玩转Excel,一定要懂点儿运行逻辑和结构
4]:OOXML:Excel(xlsx)是什么
5]:聊聊Excel解析:如何处理百万行EXCEL文件