Golang 操作 CSV 及其开源库分享
前言
最近遇到了一个需求,需要写代码操作 CSV 文件,就想着写一套 CSV 工具,以后再遇到类似的需求可以重复利用。 写着写着没想到一个小小的 CSV 文件操作中也有几个“小坑”,这里做一点小总结分享给大家。 相关源码我上传到了我的 Github: https://github.com/kaolengmian7/go-kit ,使用方法很简单,看 readme 即可,大家感兴趣可以拉下来代码跑一跑单元测试玩。
Golang 封装 CSV 操作
go-kit 库中提供了 4 个基础函数:
// 根据坐标返回数据,rowNum == 0, column == 1 则返回坐标为(0,1)的数据
func (c *CSV) GetByCoordinate(rowNum int, columnNum int) (res string, err error)
// 在某一行末尾追加数据
func (c *CSV) AddToRow(rowNum int, data string) (err error)
// 读取 CSV 文件
func (c *CSV) Read(filePath string) (err error)
// 导出 CSV 文件到当前目录
func (c *CSV) Export(fileName string) (err error)
坑1:使用CSV.Read()函数无法读取非矩阵型的CSV文件
什么叫非矩阵型的CSV?就是在某一列,有些行有数据,有些行为 nil。 我们用表格举例子就很好懂:可以看到第 3 列,第 2、3 行的值为 nil,读取这种 CSV 文件会返回一个空数组。
列1 | 列2 | 列3 |
---|---|---|
xx | xx | xx |
xx | xx | nil |
xx | xx | nil |
xx | xx | xx |
所以强烈建议:在赋值时,某行如果没有值,填充一个空字符串。以防止其他程序读取到此 CSV 文件时得到一个空数组,产生诡异的 Bug。
在我 go-kit 库的单元测试(TestExport函数
)中可以看到:我使用的测试用例为
// 正确测试用例
csv.data = [][]string{{"1", "2", "3", "test"}, {"4", "5", "6", ""}, {"7", "8", "9", ""}}
// 反例
csv.data = [][]string{{"1", "2", "3", "test"}, {"4", "5", "6"}, {"7", "8", "9"}}
坑2:UTF-8 的 BOM 头
什么是 BOM?
BOM = Byte Order Mark 是 Unicode 规范中推荐的标记字节顺序的方法。比如说对于 UTF-16,如果接收者收到的 BOM 是FEFF
,表明这个字节流是大端序的;如果收到FFFE
,就表明这个字节流是小端序的。
关于什么是大端序和小端序,可以看我的这篇文章的第 6 小节: 斯坦福CS144计算机网络笔记一
UTF-8 不需要 BOM 来表明字节顺序,但可以用 BOM 来表明“我是 UTF-8 编码”。BOM 的 UTF-8 编码是EF BB BF
。所以如果接收者收到以EF BB BF
开头的字节流,就知道这是 UTF-8 编码了。
BOM 如何影响了我的代码?
正常情况下,Go 读取带有 BOM 头的 CSV 文件后,打印输出是不会出现异常的(不可见),比如:
func TestXXX(t *testing.T) {
csv := New()
err = csv.Read("./test.csv")
log.Printf("csvData:%+v", csv.data)
}
------------------------- console -------------------------
2022/08/25 20:38:09 csvData:[[1 2 3 test] [4 5 6 ] [7 8 9 ]]
但是在我的单元测试中,使用assert.ElementsMatch()
函数验证 input 与 output 时,就出现了问题:可以看到我代码的第 15 行,必须要在第一个数据那里加上\ufeff
才能通过测试。
func TestExport(t *testing.T) {
csv := New()
csv.data = [][]string{{"1", "2", "3", "test"}, {"4", "5", "6", ""}, {"7", "8", "9", ""}}
err := csv.Export("test_export")
assert.Nil(t, err)
fp := fmt.Sprintf("%s-%s.csv", "test_export", time.Now().Format("2006-01-02"))
log.Println(fp)
// 验证文件存在
_, err = os.Stat(fp)
assert.Nil(t, err)
// 验证内容一致
expectCsv := New()
err = expectCsv.Read(fp)
expect := [][]string{{"\ufeff1", "2", "3", "test"}, {"4", "5", "6", ""}, {"7", "8", "9", ""}} // 因为写入了 UTF-8 BOM 所以要加上 \ufeff
log.Printf("expectCSVData:%+v", expectCsv.data)
assert.ElementsMatch(t, expect, expectCsv.data)
// 删除测试文件
err = os.Remove(fp)
if err != nil {
panic(fmt.Sprintf("csv_util2 TestExport failed! os.Remove(%s) failed!", fp))
}
}
�