4

在 Golang 中导出一个 excel 可识别的 csv

 2 years ago
source link: https://www.codesky.me/archives/go-to-csv.wind
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 Golang 中导出一个 excel 可识别的 csv

好久没有更新博客了,稍微写个几行吧……不然都快不会写东西了 emmm。

众所周知,导出表格是中后台一个非常普遍的需求,而 Golang 官方库中就提供了 encoding/csv

最简单的应用在官方里写的很明白:

  1. package main
  2. import (
  3. "encoding/csv"
  4. "log"
  5. "os"
  6. )
  7. func main() {
  8. records := [][]string{
  9. {"first_name", "last_name", "username"},
  10. {"Rob", "Pike", "rob"},
  11. {"Ken", "Thompson", "ken"},
  12. {"Robert", "Griesemer", "gri"},
  13. }
  14. w := csv.NewWriter(os.Stdout)
  15. for _, record := range records {
  16. if err := w.Write(record); err != nil {
  17. log.Fatalln("error writing record to csv:", err)
  18. }
  19. }
  20. // Write any buffered data to the underlying writer (standard output).
  21. w.Flush()
  22. if err := w.Error(); err != nil {
  23. log.Fatal(err)
  24. }
  25. }

看上去思路好像没什么问题,我们不是只要依次写入就可以了吗?

似乎是这样,说干就干。

首先考虑一点:对于函数的用户,不应该自己去处理成 string,应该由内部自己处理,因此传入的类型应当是一个 interface{} 或者 []interface{},因此我们现在先写一个函数,把任何类型转成 string,这样官方 csv 函数才能被使用:

  1. func interfaceSliceToStringSlice(dataList []interface{}) (strList []string) {
  2. for _, data := range dataList {
  3. strList = append(strList, fmt.Sprintf("%v", data))
  4. }
  5. return
  6. }

然后,再调用,将表头和每行内容挨个处理,似乎就完事儿了?

  1. func (s *Service) toCsv(ctx context.Context, header []interface{}, dataList [][]interface{}) (result []byte, err error) {
  2. buffer := bytes.NewBuffer(nil)
  3. record := csv.NewWriter(buffer)
  4. if header != nil {
  5. err = record.Write(interfaceSliceToStringSlice(header))
  6. if err != nil {
  7. log.Error("toCsv Header Error: %v", err)
  8. return
  9. }
  10. }
  11. for _, data := range dataList {
  12. writeErr := record.Write(interfaceSliceToStringSlice(data))
  13. if writeErr != nil {
  14. err = writeErr
  15. log.Error("toCsv Content Error: %v", err)
  16. return
  17. }
  18. }
  19. record.Flush()
  20. if err = record.Error(); err != nil {
  21. log.Error("toCsv Flush error: %v", err)
  22. return
  23. }
  24. result = buffer.Bytes()
  25. return
  26. }

看上去仿佛是这样,结果运营一用,嘿,Excel 打不开,WPS 和 Numbers 明明都是好好的,在网上查了一下 Excel 乱码的问题,说的是什么默认用的 unicode 解析,而我们打包出来的是 UTF8,编码识别有问题所以不对。当时就很迷惑:UTF8 他喵的不就是一种 unicode 吗?

解决方案其实就是为了做一件事情,那就是压入 BOM 头,BOM 头是一个什么东西呢?——其实就是指定文件编码。

如果是 UTF8,在头部加上 \xef\xbb\xbf 就可以在 Excel 中被识别了。

对应的 BOM 头关系可见下表:

BytesEncoding Form00 00 FE FFUTF-32, big-endianFF FE 00 00UTF-32, little-endianFE FFUTF-16, big-endianFF FEUTF-16, little-endianEF BB BFUTF-8

说到这里了,既然 BOM 头这么好,为什么不大家都加上 BOM 头呢,就跟 content/type 一样,就再也没有不知道识别成啥的烦恼了,原因有以下几点:

  1. BOM 头增加了无用的空间
  2. 协议限制(主要出现在把 BOM 头识别成字符,还是头上)

基于此,添加 BOM 头还是需要考虑一些兼容性的问题,尤其是如果是在程序之间相互通信的时候,怎么处理 BOM 头或许是一个大难题,还是要因地制宜的去选择「是否添加 BOM 头」。

因此,最终还是加上了一个参数 options,拿来配置是否需要使用 BOM 头,来让函数变得更加通用,适用于不同的场景。

参考资料:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK