文章目录
基本操作
创建工作簿
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并没有过滤掉最右边的空列