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并没有过滤掉最右边的空列

参考资料

相关推荐
磊磊磊磊磊16 小时前
用AI做了个排版工具,分享一下如何高效省钱地用AI!
前端·后端·react.js
hgz071016 小时前
Spring Boot Starter机制
java·spring boot·后端
daxiang1209220516 小时前
Spring boot服务启动报错 java.lang.StackOverflowError 原因分析
java·spring boot·后端
我家领养了个白胖胖16 小时前
极简集成大模型!Spring AI Alibaba ChatClient 快速上手指南
java·后端·ai编程
一代明君Kevin学长17 小时前
快速自定义一个带进度监控的文件资源类
java·前端·后端·python·文件上传·文件服务·文件流
aiopencode17 小时前
上架 iOS 应用到底在做什么?从准备工作到上架的流程
后端
哈哈老师啊17 小时前
Springboot简单二手车网站qs5ed(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
JIngJaneIL17 小时前
基于Java+ vue图书管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
shenzhenNBA18 小时前
python用openpyxl操作excel-单元格样式操作
python·excel·openpyxl·单元格样式
teamlet18 小时前
Gear DNS - 一个go语言开发的小型dns系统
golang·dns·网络服务