在 Golang 中导出一个 excel 可识别的 csv
好久没有更新博客了,稍微写个几行吧……不然都快不会写东西了 emmm。
众所周知,导出表格是中后台一个非常普遍的需求,而 Golang 官方库中就提供了 encoding/csv
最简单的应用在官方里写的很明白:
package main
import (
"encoding/csv"
"log"
"os"
)
func main() {
records := [][]string{
{"first_name", "last_name", "username"},
{"Rob", "Pike", "rob"},
{"Ken", "Thompson", "ken"},
{"Robert", "Griesemer", "gri"},
}
w := csv.NewWriter(os.Stdout)
for _, record := range records {
if err := w.Write(record); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
// Write any buffered data to the underlying writer (standard output).
w.Flush()
if err := w.Error(); err != nil {
log.Fatal(err)
}
}
看上去思路好像没什么问题,我们不是只要依次写入就可以了吗?
似乎是这样,说干就干。
首先考虑一点:对于函数的用户,不应该自己去处理成 string,应该由内部自己处理,因此传入的类型应当是一个 interface{}
或者 []interface{}
,因此我们现在先写一个函数,把任何类型转成 string,这样官方 csv 函数才能被使用:
func interfaceSliceToStringSlice(dataList []interface{}) (strList []string) {
for _, data := range dataList {
strList = append(strList, fmt.Sprintf("%v", data))
}
return
}
然后,再调用,将表头和每行内容挨个处理,似乎就完事儿了?
func (s *Service) toCsv(ctx context.Context, header []interface{}, dataList [][]interface{}) (result []byte, err error) {
buffer := bytes.NewBuffer(nil)
record := csv.NewWriter(buffer)
if header != nil {
err = record.Write(interfaceSliceToStringSlice(header))
if err != nil {
log.Error("toCsv Header Error: %v", err)
return
}
}
for _, data := range dataList {
writeErr := record.Write(interfaceSliceToStringSlice(data))
if writeErr != nil {
err = writeErr
log.Error("toCsv Content Error: %v", err)
return
}
}
record.Flush()
if err = record.Error(); err != nil {
log.Error("toCsv Flush error: %v", err)
return
}
result = buffer.Bytes()
return
}
看上去仿佛是这样,结果运营一用,嘿,Excel 打不开,WPS 和 Numbers 明明都是好好的,在网上查了一下 Excel 乱码的问题,说的是什么默认用的 unicode 解析,而我们打包出来的是 UTF8,编码识别有问题所以不对。当时就很迷惑:UTF8 他喵的不就是一种 unicode 吗?
解决方案其实就是为了做一件事情,那就是压入 BOM 头,BOM 头是一个什么东西呢?——其实就是指定文件编码。
如果是 UTF8,在头部加上 \xef\xbb\xbf
就可以在 Excel 中被识别了。
对应的 BOM 头关系可见下表:
Bytes | Encoding Form |
---|---|
00 00 FE FF | UTF-32, big-endian |
FF FE 00 00 | UTF-32, little-endian |
FE FF | UTF-16, big-endian |
FF FE | UTF-16, little-endian |
EF BB BF | UTF-8 |
说到这里了,既然 BOM 头这么好,为什么不大家都加上 BOM 头呢,就跟 content/type 一样,就再也没有不知道识别成啥的烦恼了,原因有以下几点:
- BOM 头增加了无用的空间
- 协议限制(主要出现在把 BOM 头识别成字符,还是头上)
基于此,添加 BOM 头还是需要考虑一些兼容性的问题,尤其是如果是在程序之间相互通信的时候,怎么处理 BOM 头或许是一个大难题,还是要因地制宜的去选择「是否添加 BOM 头」。
因此,最终还是加上了一个参数 options,拿来配置是否需要使用 BOM 头,来让函数变得更加通用,适用于不同的场景。
参考资料:
植入部分
如果您觉得文章不错,可以通过赞助支持我。
如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。