将CSV记录在Go中解组为结构体

40

如何将CSV文件中的记录自动反序列化/解封到Go结构体中是一个问题。

例如,我有以下CSV文件:

type Test struct {
  Name string
  Surname string
  Age int
}

CSV文件包含记录。

John;Smith;42
Piter;Abel;50

除了使用“encoding/csv”包读取记录并执行类似操作的方式外,是否有一种简单的方法将这些记录解封为结构体?

record, _ := reader.Read()
test := Test{record[0],record[1],atoi(record[2])}

3
没错,你现在使用的这个模式是正确的。我猜 record, _ := Read() 只是为了在这里提供简洁的示例代码,但在你的实际代码中一定要处理错误,否则当程序某天出现问题而你不知道原因时,它会给你带来麻烦。 - twotwotwo
唉...我希望有一些包可以使用反射,就像xml/json解组一样。当然,忽略错误只是为了通过跳过非相关代码来最小化示例源。 - Valentyn Shybanov
我想知道为什么他们没有编写这样的软件包。自己编写一个可能会很有趣。 - Tyler
我知道这已经是多年之后的事了,我只是想知道有什么好的用例可以做到这一点?我觉得Go是强类型的,在大多数情况下,您会预先知道csv的模式,因此测试每行中每个字段的类型会很慢/冗余。也许设计一个工具来推断和建议用户模式是一个好主意?如果要避免样板文件或硬编码类型转换,也许将模式分离到一个带有方法进行转换的结构体中是一个解决方案? - Davos
5个回答

41

有一个叫做gocarina/gocsv的库,可以像encoding/json一样处理自定义结构体。你还可以为特定类型编写自定义的编组器和解组器。

例如:

type Client struct {
    Id      string `csv:"client_id"` // .csv column headers
    Name    string `csv:"client_name"`
    Age     string `csv:"client_age"`
}

func main() {
    in, err := os.Open("clients.csv")
    if err != nil {
        panic(err)
    }
    defer in.Close()

    clients := []*Client{}

    if err := gocsv.UnmarshalFile(in, &clients); err != nil {
        panic(err)
    }
    for _, client := range clients {
        fmt.Println("Hello, ", client.Name)
    }
}

请注意,此库仅支持 *os.File。如果您正在处理来自HTTP的表单数据(即multipart.File),请小心。 - vahdet

15

看起来我已经完成了将CSV记录自动编组为结构体的任务(仅限于字符串和整数)。希望这对你有用。

这是Playground的链接:http://play.golang.org/p/kwc32A5mJf

func Unmarshal(reader *csv.Reader, v interface{}) error {
    record, err := reader.Read()
    if err != nil {
        return err
    }
    s := reflect.ValueOf(v).Elem()
    if s.NumField() != len(record) {
        return &FieldMismatch{s.NumField(), len(record)}
    }
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        switch f.Type().String() {
        case "string":
            f.SetString(record[i])
        case "int":
            ival, err := strconv.ParseInt(record[i], 10, 0)
            if err != nil {
                return err
            }
            f.SetInt(ival)
        default:
            return &UnsupportedType{f.Type().String()}
        }
    }
    return nil
}

如果有人需要这个实现,我会尝试创建GitHub包。


解析CSV文件比你想象的要复杂一些,因为它可以包含带引号的字段或多行字段。作为自己项目的一部分,我编写了一个CSV解析器来进行映射:https://github.com/mcuadros/collector/blob/master/src/format/csv.go如果您有兴趣,我们可以合作开发一个CSV解析库。 - mcuadros
@mcuadros,我现在正在使用标准包encoding/csv进行解析,所以所有这些带有引号字段的问题都使用标准包解决了。我的问题主题是关于将CSV自动解组为静态结构体(而不是动态映射)。你为什么要编写自己的CSV解析包? - Valentyn Shybanov
哦,我错过了这一点。顺便说一下,标准包太慢了,而这个实现要快3-4倍,如果你正在使用另一个读取器或其他输入,你必须创建一个StringReader。另外一个CSV解析器的实现可以在https://github.com/gwenn/yacr找到。 - mcuadros
真的是很好的建议!我可以通过引入一个带有一个方法 Read() string[] 的接口来实现只从 CSV 中读取一行。这样,我就可以轻松地在不同的读取器实现之间进行切换! - Valentyn Shybanov
@ValentynShybanov,你可以使用io.Reader接口替换*csv.Reader - basebandit

1
使用csvutil,可以为列标题添加链接,参见example
在您的情况下,可以这样做:
package main

import (
    "encoding/csv"
    "fmt"
    "io"
    "os"

    "github.com/jszwec/csvutil"
)

type Test struct {
    Name    string
    Surname string
    Age     int
}

func main() {
    csv_file, _ := os.Open("test.csv")
    reader := csv.NewReader(csv_file)
    reader.Comma = ';'

    userHeader, _ := csvutil.Header(Test{}, "csv")
    dec, _ := csvutil.NewDecoder(reader, userHeader...)

    var users []Test
    for {
        var u Test
        if err := dec.Decode(&u); err == io.EOF {
            break
        }
        users = append(users, u)
    }

    fmt.Println(users)
}

1
您可以自己烘焙。也许可以尝试以下做法:
package main

import (
    "fmt"
    "strconv"
    "strings"
)

type Test struct {
    Name    string
    Surname string
    Age     int
}

func (t Test) String() string {
    return fmt.Sprintf("%s;%s;%d", t.Name, t.Surname, t.Age)
}

func (t *Test) Parse(in string) {
    tmp := strings.Split(in, ";")
    t.Name = tmp[0]
    t.Surname = tmp[1]
    t.Age, _ = strconv.Atoi(tmp[2])
}

func main() {

    john := Test{"John", "Smith", 42}
    fmt.Printf("john:%v\n", john)

    johnString := john.String()
    fmt.Printf("johnString:%s\n", johnString)

    var rebornJohn Test
    rebornJohn.Parse(johnString)
    fmt.Printf("rebornJohn:%v\n", rebornJohn)

}

是的,正如我在问题中所述,我使用手动编组编写了它,因此我正在寻找一些自动编组的方法,就像encoding/xml一样。但是要实现它,需要使用反射... - Valentyn Shybanov

0

解决这个问题的简单方法是使用JSON作为中间表示。

一旦你做到了这一点,你就有了各种工具可供使用。

你可以...

  • 直接将其反序列化为你的类型(如果它全是字符串)
  • 将其反序列化为map[string]interface{},然后进行必要的类型转换
  • 反序列化 -> 转换类型 -> 重新编排JSON -> 反序列化为你的类型

下面是一个简单的通用编组函数,它支持该流程...

pairToJSON := func(header, record []string) string {
    raw := ""
    for j, v := range record {
        if j != 0 {
            raw += ",\n"
        }
        raw += "\"" + header[j] + "\":\"" + v + "\""
    }
    raw = "{\n" + raw + "\n}"
    return raw
}

以上代码与标准csv库生成的[]string数据兼容。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接