Go| excelize的流式迭代器

文章目录

基本操作

创建工作簿

go 复制代码
func NewFile(opts ...Options) *File

使用NewFile创建新的excel工作簿,默认包含一个名为Sheet1的工作表

go 复制代码
f := excelize.NewFile()  
defer func() {  
    if err := f.Close(); err != nil {  
       panic(err)  
    }  
}()

向单元格中写入数据

go 复制代码
func (f *File) SetCellValue(sheet, cell string, value interface{}) error
go 复制代码
// 设置单元格的值  
_ = f.SetCellValue("Sheet1", "A1", "云厂商编号")  
_ = f.SetCellValue("Sheet1", "B1", "云厂商名称")  
_ = f.SetCellValue("Sheet1", "A2", "1")  
_ = f.SetCellValue("Sheet1", "B2", "阿里云")  
_ = f.SetCellValue("Sheet1", "A3", "2")  
_ = f.SetCellValue("Sheet1", "B3", "火山引擎")  
_ = f.SetCellValue("Sheet1", "A4", "3")  
_ = f.SetCellValue("Sheet1", "B4", "腾讯云")  
_ = f.SetCellValue("Sheet1", "A5", "4")  
_ = f.SetCellValue("Sheet1", "B5", "百度云")

保存工作簿

go 复制代码
// 保存对excel文档的编辑
func (f *File) Save(opts ...Options) error


// 将excel文档另存为指定文件
func (f *File) SaveAs(name string, opts ...Options) error
go 复制代码
if err := f.SaveAs("test.xlsx"); err != nil {  
    panic(err)  
}

读取工作表

为了体现两种读取方式的区别,先合并一些单元格

go 复制代码
f, _ := excelize.OpenFile("test.xlsx")  
  
defer func() {  
    if err := f.Close(); err != nil {  
       panic(err)  
    }  
}()  
  
// 打开第一个工作表  
// 获取工作表名称  
sheetName := f.GetSheetName(0)  
fmt.Println("工作表名称是", sheetName)  
  
// 合并单元格  
_ = f.MergeCell(sheetName, "A5", "A7")  
_ = f.MergeCell(sheetName, "B5", "B7")

一次性读取

go 复制代码
// 一次性读取  
fmt.Println("一次性读取所有数据")  
rows, _ := f.GetRows(sheetName)  
fmt.Println("工作表中一共有", len(rows), "行数据,分别是")  
for _, row := range rows {  
    fmt.Println(row)  
}

结果如下

go 复制代码
工作表名称是 Sheet1
一次性读取所有数据
工作表中一共有 5 行数据,分别是
[云厂商编号 云厂商名称]
[1 阿里云]
[2 腾讯云]
[3 百度云]
[4 火山引擎]

流式读取

go 复制代码
// 流式读取器  
fmt.Println("流式迭代器读取数据")  
rowIter, _ := f.Rows(sheetName)  
var cou int  
for rowIter.Next() {  
    cou++  
    row, _ := rowIter.Columns()  
    fmt.Println("第", cou, "行数据是", row)  
}  
fmt.Println("流式迭代器读取数据完成,一共有", cou, "行数据")

结果如下:

go 复制代码
工作表名称是 Sheet1
流式迭代器读取数据
第 1 行数据是 [云厂商编号 云厂商名称]
第 2 行数据是 [1 阿里云]
第 3 行数据是 [2 腾讯云]
第 4 行数据是 [3 百度云]
第 5 行数据是 [4 火山引擎]
第 6 行数据是 []
第 7 行数据是 []
流式迭代器读取数据完成,一共有 7 行数据

可能的原因

可以发现,合并单元格之后,流式迭代器读取的数据要比一次性读取所有的数据,多出两个空的切片;

合并单元格之后,单元格的UseRange会扩展到实际上没有使用的单元格,流式迭代器会读取所有行包括空行,一次性读取会对这些空行进行紧凑处理

源码剖析

一次性读取GetRows

go 复制代码
// 返回的是一个string类型的二维切片

func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {  
	// 获取工作表sheet的读取器
    if _, err := f.workSheetReader(sheet); err != nil {  
       return nil, err  
    }  
    // 调用Rows
    rows, _ := f.Rows(sheet)  
    results, cur, maxVal := make([][]string, 0, 64), 0, 0  
    // 遍历每一行
    for rows.Next() {  
	   // cur记录当前读取到的行数
       cur++  
       row, err := rows.Columns(opts...)  
       if err != nil {  
          break  
       }  
       // 合并结果
       results = append(results, row)  
       // 过滤末尾的空行,记录实际上读取出来的行数
       if len(row) > 0 {  
          maxVal = cur  
       }  
    }  
    return results[:maxVal], rows.Close()  
}

GetRows内部调用了Rows,但做了一些额外的处理:

  • 将遍历到的每一行加入到结果集合中
  • 如果当前行的切片长度不为0,更新maxVal即实际上取出的行数

实际上,只能过滤掉末尾的空行,中间的空行还是可以读出;所以对每一行数据进行处理的时候需要手动过滤中间的空行

流式迭代器Rows

  • Rows的结构如下
go 复制代码
// Rows defines an iterator to a sheet.type Rows struct {  
    err                     error  
    curRow, seekRow         int  
    needClose, rawCellValue bool  
    sheet                   string  
    f                       *File  
    tempFile                *os.File  
    sst                     *xlsxSST  
    decoder                 *xml.Decoder  
    token                   xml.Token  
    curRowOpts, seekRowOpts RowOpts  
}
  • 获取迭代器
go 复制代码
func (f *File) Rows(sheet string) (*Rows, error) {  
	// 检查sheet的格式是否合法
    if err := checkSheetName(sheet); err != nil {  
       return nil, err  
    }  
    // 获取sheet的xml file path
    name, ok := f.getSheetXMLPath(sheet)  
    if !ok {  
       return nil, ErrSheetNotExist{sheet}  
    }  
    // 加载sheet
    if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {  
       ws := worksheet.(*xlsxWorksheet)  
       // 并发安全,上锁
       ws.mu.Lock()  
       defer ws.mu.Unlock()  
       // Flush data  
       output, _ := xml.Marshal(ws)  
       f.saveFileList(name, f.replaceNameSpaceBytes(name, output))  
    }  
    var err error  
    rows := Rows{f: f, sheet: name}  
    // 创建xml解码器
    rows.needClose, rows.decoder, rows.tempFile, err = f.xmlDecoder(name)  
    return &rows, err  
}
  • Next
go 复制代码
// Next will return true if it finds the next row element.
// 如果下一行有数据,则返回true
func (rows *Rows) Next() bool {  
    rows.seekRow++  
    if rows.curRow >= rows.seekRow {  
       rows.curRowOpts = rows.seekRowOpts  
       return true  
    }  
    for {  
    // Token​返回输入流中的下一个XML标记(token)。当到达输入流末尾时,Token返回nil, io.EOF**。
       token, _ := rows.decoder.Token()  
       // token为nil,表明已经读取到末尾
       if token == nil {  
          return false  
       }  
       switch xmlElement := token.(type) {  
       case xml.StartElement:  
          if xmlElement.Name.Local == "row" {  
             rows.curRow++  
             if rowNum, _ := attrValToInt("r", xmlElement.Attr); rowNum != 0 {  
                rows.curRow = rowNum  
             }  
             rows.token = token  
             rows.curRowOpts = extractRowOpts(xmlElement.Attr)  
             return true  
          }  
       case xml.EndElement:  
          if xmlElement.Name.Local == "sheetData" {  
             return false  
          }  
       }  
    }  
}

关键在于Token的获取,源码中对于Token的解释

  • Token ​ 返回输入流中的下一个 XML 标记(token)。当到达输入流末尾时,​Token ​ 返回 ​nil, io.EOF

  • 返回的标记数据中的字节切片(slices of bytes)引用的是解析器的内部缓冲区,这些字节仅在下次调用 Token 之前有效。如需获取字节的副本,请调用 CopyToken 或使用该标记的 Copy 方法。

  • Token ​ 会将自闭合元素(例如 <br>)展开为分别由连续调用返回的起始元素和结束元素。

  • Token ​ 保证它返回的 ​StartElement ​ 和 ​EndElement ​ 标记是正确嵌套和匹配的:如果 ​Token ​ 在遇到所有预期结束元素之前碰到意外的结束元素或 ​EOF,它将返回一个错误。

  • 如果调用了 ​Decoder.CharsetReader ​ 并返回了错误,该错误会被封装后返回。Token ​ 按照 https://www.w3.org/TR/REC-xml-names/ 的描述实现了 XML 命名空间。​

  • Token ​ 中包含的每个 ​Name ​ 结构体,当其命名空间已知时,其 ​Space ​ 字段会被设置为标识该命名空间的 URL。如果 ​Token ​ 遇到一个无法识别的命名空间前缀,它会将该前缀用作 ​Space​ 的值,而不是报错。

  • Columns

go 复制代码
// Columns return the current row's column values. This fetches the worksheet// data as a stream, returns each cell in a row as is, and will not skip empty  
// rows in the tail of the worksheet.  

// Columns返回当前行的列值。该方法以流式方式获取工作表数据,​逐单元格返回行内各单元格的原始值,并且不会跳过工作表尾部的空行。
func (rows *Rows) Columns(opts ...Options) ([]string, error) {  
    if rows.curRow > rows.seekRow {  
       return nil, nil  
    }  
    var rowIterator rowXMLIterator  
    var token xml.Token  
    rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue  
    if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {  
       return rowIterator.cells, rowIterator.err  
    }  
    for {  
       if rows.token != nil {  
          token = rows.token  
       } else if token, _ = rows.decoder.Token(); token == nil {  
          break  
       }  
       switch xmlElement := token.(type) {  
       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  
             }  
          }  
          // ​rowXMLHandler​ 用于解析工作表的行XML元素
          // // rowXMLHandler parse the row XML element of the worksheet.
          if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {  
             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  
}
  • Columns 返回当前行的列值 。该方法以流式 方式获取工作表 数据,逐单元格 返回行内各单元格的原始值 ,并且不会跳过 工作表尾部空行
go 复制代码
// 关键语句

for{
// .......
	if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {  
	    rows.token = nil  
	    return rowIterator.cells, rowIterator.err  
	}
// .....
}




// rowXMLHandler parse the row XML element of the worksheet.
// 
func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) {  
    if rowIterator.inElement == "c" {  
    // 逐个解析改行每一列的单元格
       rowIterator.cellCol++  
       
       colCell := xlsxC{}  
       _ = rows.decoder.DecodeElement(&colCell, xmlElement)  
       if colCell.R != "" {  
          if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil {  
             return  
          }  
       }  
       blank := rowIterator.cellCol - len(rowIterator.cells)  
       if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil {  
       // 将解析好的单元格添加到结果集中
          rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val)  
       }  
    }  
}

可以看出Rows不会跳空空行,末尾的和中间的都不会跳过

扩展-列式读取

go 复制代码
// 一次性读取  
fmt.Println("一次性读取所有数据")  
cols, _ := f.GetCols(sheetName)  
fmt.Println("工作表中一共有", len(cols), "列数据,分别是")  
for _, col := range cols {  
    fmt.Println(col)  
}  
  
//流式读取器  
fmt.Println("流式迭代器读取数据")  
colIter, _ := f.Cols(sheetName)  
var cou int  
for colIter.Next() {  
    cou++  
    col, _ := colIter.Rows()  
    fmt.Println("第", cou, "列数据是", col)  
}  
fmt.Println("流式迭代器读取数据完成,一共有", cou, "列数据")

源码

go 复制代码
func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) {  
    if _, err := f.workSheetReader(sheet); err != nil {  
       return nil, err  
    }  
    cols, err := f.Cols(sheet)  
    results := make([][]string, 0, 64)  
    for cols.Next() {  
       col, _ := cols.Rows(opts...)  
       results = append(results, col)  
    }  
    return results, err  
}

GetCols并没有过滤掉最右边的空列

参考资料

相关推荐
码事漫谈3 小时前
【C++必知必会】读取文件速记
后端
码事漫谈3 小时前
【C++必知必会】字符串操作速记
后端
q***48414 小时前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
joker丶牧羊人4 小时前
搭建Hyperf本地开发环境之Docker容器开发
后端·docker·php
Oriental4 小时前
URL解码踩坑记录
前端·后端
IT教程资源C4 小时前
(N_135)基于springboot,vue高校图书馆管理系统
vue.js·spring boot·后端
程序员西西4 小时前
Spring Boot 整合 Zookeeper实现分布式锁?
后端
库森学长4 小时前
多线程有序执行,九大方案!
后端·面试
z***67774 小时前
Spring EL 表达式的简单介绍和使用
java·后端·spring