Golang - 解决 excelize 流式读调用 io.ReadAll 的问题

#excel

2025-02-22 16:12:44

最近在给业务侧做二开包,功能:Excel 的读和写,要求,内存占用极低。经过调研发现 excelize star 最多,且仍在维护,所以底座选用这个库,但在压测时发现该库并不能真正的流式读,因为里面用到了 io.ReadAll ,进一步阅读源码发现,其实没必要 io.ReadAll,于是有了此文。

excelize 存在的问题

excelize 读取 Excel 的代码大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
file, err := excelize.OpenFile(reader.filePath)
if err != nil {
	return err
}

rows, err := file.Rows(reader.sheetName)
if err != nil {
	return err
}

for rows.Next() {
	row, err := rows.Columns()
	if err != nil {
		fmt.Println(err)
	}
	for _, colCell := range row {
		fmt.Print(colCell, "\t")
	}
	fmt.Println()
}
if err = rows.Close(); err != nil {
	fmt.Println(err)
}

其中 excelize.OpenFile 的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func OpenFile(filename string, opts ...Options) (*File, error) {
	file, err := os.Open(filepath.Clean(filename))
	if err != nil {
		return nil, err
	}
	f, err := OpenReader(file, opts...)
	if err != nil {
		if closeErr := file.Close(); closeErr != nil {
			return f, closeErr
		}
		return f, err
	}
	f.Path = filename
	return f, file.Close()
}

这串代码并无问题,但 OpenReader 就有问题了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// OpenReader read data stream from io.Reader and return a populated
// spreadsheet file.
func OpenReader(r io.Reader, opts ...Options) (*File, error) {
	b, err := io.ReadAll(r)
	if err != nil {
		return nil, err
	}
	f := newFile()
	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)))
	if err != nil {
		if len(f.options.Password) > 0 {
			return nil, ErrWorkbookPassword
		}
		return nil, err
	}
	file, sheetCount, err := f.ReadZipReader(zr)
	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
}

这里 b, err := io.ReadAll(r) 会导致将所有数据都加载到内存,从而使流式读取失去意义。且 b 仅用于以下代码:

1
2
3
4
5
6
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)))

这串代码完全可以使用下面代码替代:

zr, err := zip.OpenReader(name)

然后调整代码:

file, sheetCount, err := f.ReadZipReader(zr)

调整为下面代码即可:

file, sheetCount, err := f.ReadZipReader(&zr.Reader)

至于解密的代码:

1
2
3
if b, err = Decrypt(b, f.options); err != nil {
	return nil, ErrWorkbookFileFormat
}

经确认,应该是解密加密的 Excel 2003,oleIdentifier 用于判断文件是否为 cfb 格式,这里 cfb 格式指的是 Excel 2003;
该问题已提交给官方:https://github.com/qax-os/excelize/issues/2086

我的解决办法

我们公司内部有维护 goproxy,和 sumdb,改了代码并提交到公司 gitlab 即可解决问题,我新增了如下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// StreamRead 流式读取,不支持加密的 Excel 2003
func StreamRead(filename string, opts ...Options) (*File, error) {
	file, err := os.Open(filepath.Clean(filename))
	if err != nil {
		return nil, err
	}
	f, err := openByName(file.Name(), opts...)
	if err != nil {
		if closeErr := file.Close(); closeErr != nil {
			return f, closeErr
		}
		return f, err
	}
	f.Path = filename
	return f, file.Close()
}

// 不支持 加密的 Excel 2003 格式!
// openByName read data stream from io.Reader and return a populated
// spreadsheet file.
func openByName(name string, opts ...Options) (*File, error) {
	f := newFile()
	f.options = f.getOptions(opts...)
	if err := f.checkOpenReaderOptions(); err != nil {
		return nil, err
	}
	zr, err := zip.OpenReader(name)
	if err != nil {
		if len(f.options.Password) > 0 {
			return nil, ErrWorkbookPassword
		}
		return nil, err
	}
	file, sheetCount, err := f.ReadZipReader(&zr.Reader)
	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
}
最后更新于