在Go中读取CSV文件

49

这是一个读取CSV文件的代码片段:

func parseLocation(file string) (map[string]Point, error) {
    f, err := os.Open(file)
    defer f.Close()
    if err != nil {
        return nil, err
    }
    lines, err := csv.NewReader(f).ReadAll()
    if err != nil {
        return nil, err
    }
    locations := make(map[string]Point)
    for _, line := range lines {
        name := line[0]
        lat, laterr := strconv.ParseFloat(line[1], 64)
        if laterr != nil {
            return nil, laterr
        }
        lon, lonerr := strconv.ParseFloat(line[2], 64)
        if lonerr != nil {
            return nil, lonerr
        }
        locations[name] = Point{lat, lon}
    }
    return locations, nil
}

有没有办法提高这段代码的可读性?如果和 nil 的噪音。


7
defer语句必须放在if err != nil之后:如果此时出现错误,那么这是因为文件无法打开,因此不能在其上使用Close。请注意不要改变原来的意思。 - julienc
3
defer语句不必放在错误检查之后。你可以关闭一个你没有打开的文件:http://play.golang.org/p/vcxShZp00J - Dustin
5个回答

80

现在Go语言有一个用于此的csv包,它是encoding/csv。您可以在这里找到文档:https://golang.org/pkg/encoding/csv/

文档中有一些很好的示例。这里是我创建的帮助方法,用于读取csv文件并返回其记录。

package main

import (
    "encoding/csv"
    "fmt"
    "log"
    "os"
)

func readCsvFile(filePath string) [][]string {
    f, err := os.Open(filePath)
    if err != nil {
        log.Fatal("Unable to read input file " + filePath, err)
    }
    defer f.Close()

    csvReader := csv.NewReader(f)
    records, err := csvReader.ReadAll()
    if err != nil {
        log.Fatal("Unable to parse file as CSV for " + filePath, err)
    }

    return records
}

func main() {
    records := readCsvFile("../tasks.csv")
    fmt.Println(records)
}

我可能错了,但是这个包只能处理逗号分隔的文件。 - Artur
1
@Artur 我没有尝试过,但看起来你可以在 NewReader 中更改分隔符。参见这个例子:https://pkg.go.dev/encoding/csv#example-Reader-Options - SyntaxRules
2
是的,默认情况下它可以使用逗号。如果您想更改此行为,可以添加csv.NewReader().Comma=';',例如如果分隔符是;。 - Aghiad Alzein

35

Go是一种非常冗长的编程语言,但你可以像这样使用:

// predeclare err
func parseLocation(file string) (locations map[string]*Point, err error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close() // this needs to be after the err check

    lines, err := csv.NewReader(f).ReadAll()
    if err != nil {
        return nil, err
    }

    //already defined in declaration, no need for :=
    locations = make(map[string]*Point, len(lines))
    var lat, lon float64 //predeclare lat, lon
    for _, line := range lines {
        // shorter, cleaner and since we already have lat and err declared, we can do this.
        if lat, err = strconv.ParseFloat(line[1], 64); err != nil {
            return nil, err
        }
        if lon, err = strconv.ParseFloat(line[2], 64); err != nil {
            return nil, err
        }
        locations[line[0]] = &Point{lat, lon}
    }
    return locations, nil
}

//编辑

评论中,@Dustin发布了一个更高效和合适的版本,我在这里补充一下:

func parseLocation(file string) (map[string]*Point, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    csvr := csv.NewReader(f)

    locations := map[string]*Point{}
    for {
        row, err := csvr.Read()
        if err != nil {
            if err == io.EOF {
                err = nil
            }
            return locations, err
        }

        p := &Point{}
        if p.lat, err = strconv.ParseFloat(row[1], 64); err != nil {
            return nil, err
        }
        if p.lon, err = strconv.ParseFloat(row[2], 64); err != nil {
            return nil, err
        }
        locations[row[0]] = p
    }
}

playground


该段内容为HTML代码,其中包含一个超链接,超链接的文本内容是“playground”,点击该超链接可以跳转到指定网址"http://play.golang.org/p/3YE2pXSF3x"。

3
我建议不要为那些返回变量命名。即使给它们命名,你也没有使用它们,这只会造成额外的混乱。在解析之前一次性将整个内容读入内存是不必要的。应该始终避免这样做。经纬度预声明的作用域太高了,而且真的没必要。我会这样做:http://play.golang.org/p/3YE2pXSF3x - Dustin
@OneOfOne 你为什么会说 Go 是“非常啰嗦的语言”? - Rodrigo
@user3666882,因为它的特性,没有像其他大多数编程语言那样的“捷径”,甚至没有三元运算符。 - OneOfOne
我建议不要使用 if p.lat, err = strconv.ParseFloat(row[1], 64); err != nil {}...。当我在一行的开头看到 if 时,我认为这是一个条件语句。除非在特定场景下,这并不是一种很好的惯用法,你牺牲了可读性来节省一两行代码。现在,我必须在脑海中解析一个由两个部分组成的 if 语句,而不是清晰地看到解析已经完成的一行代码。 - arg20
非常感谢。您能否为像我这样的Golang新手解释一下为什么在检查err之后必须放置defer行? - Yu Chen
显示剩余2条评论

26

我基本上是从这里复制了我的答案:https://www.dotnetperls.com/csv-go。 对我来说,这比我在stackoverflow上找到的答案更好。

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

func ReadCsvFile(filePath string)  {
    // Load a csv file.
    f, _ := os.Open(filePath)

    // Create a new reader.
    r := csv.NewReader(f)
    for {
        record, err := r.Read()
        // Stop at EOF.
        if err == io.EOF {
            break
        }

        if err != nil {
            panic(err)
        }
        // Display record.
        // ... Display record length.
        // ... Display all individual elements of the slice.
        fmt.Println(record)
        fmt.Println(len(record))
        for value := range record {
            fmt.Printf("  %v\n", record[value])
        }
    }
}

对于每个值,使用 range 函数从读取器中获取,然后使用 fmt.Printf() 在控制台输出该值。 - i73

5

我也不喜欢默认的Reader过于冗长,因此我创建了一种新类型,类似于bufio#Scanner

package main
import "encoding/csv"
import "io"

type Scanner struct {
   Reader *csv.Reader
   Head map[string]int
   Row []string
}

func NewScanner(o io.Reader) Scanner {
   csv_o := csv.NewReader(o)
   a, e := csv_o.Read()
   if e != nil {
      return Scanner{}
   }
   m := map[string]int{}
   for n, s := range a {
      m[s] = n
   }
   return Scanner{Reader: csv_o, Head: m}
}

func (o *Scanner) Scan() bool {
   a, e := o.Reader.Read()
   o.Row = a
   return e == nil
}

func (o Scanner) Text(s string) string {
   return o.Row[o.Head[s]]
}

例子:

package main
import "strings"

func main() {
   s := `Month,Day
January,Sunday
February,Monday`

   o := NewScanner(strings.NewReader(s))
   for o.Scan() {
      println(o.Text("Month"), o.Text("Day"))
   }
}

https://golang.org/pkg/encoding/csv


-3

您还可以读取目录内容以加载所有CSV文件。然后使用goroutines逐个读取所有这些CSV文件。

csv文件:

101,300.00,11000901,1155686400
102,250.99,11000902,1432339200

main.go 文件:

const sourcePath string = "./source"

func main() {
    dir, _ := os.Open(sourcePath)
    files, _ := dir.Readdir(-1)

    for _, file := range files {
        fmt.Println("SINGLE FILE: ")
        fmt.Println(file.Name())
        filePath := sourcePath + "/" + file.Name()
        f, _ := os.Open(filePath)
        defer f.Close()
        // os.Remove(filePath)

        //func
        go func(file io.Reader) {
            records, _ := csv.NewReader(file).ReadAll()
            for _, row := range records {
                fmt.Println(row)
            }
        }(f)

        time.Sleep(10 * time.Millisecond)// give some time to GO routines for execute
    }
}

输出结果将会是:

$ go run main.go

SINGLE FILE:
batch01.csv
[101 300.00 11000901 1155686400]
[102 250.99 11000902 1432339200]

----------------- -------------- ---------------------- ------- ---------------- ------------------- ----------- --------------

以下是使用 Invoice 结构体 的示例

func main() {
    dir, _ := os.Open(sourcePath)
    files, _ := dir.Readdir(-1)

    for _, file := range files {
        fmt.Println("SINGLE FILE: ")
        fmt.Println(file.Name())
        filePath := sourcePath + "/" + file.Name()
        f, _ := os.Open(filePath)
        defer f.Close()

        go func(file io.Reader) {
            records, _ := csv.NewReader(file).ReadAll()
            for _, row := range records {
                invoice := new(Invoice)
                invoice.InvoiceNumber = row[0]
                invoice.Amount, _ = strconv.ParseFloat(row[1], 64)
                invoice.OrderID, _ = strconv.Atoi(row[2])
                unixTime, _ := strconv.ParseInt(row[3], 10, 64)
                invoice.Date = time.Unix(unixTime, 0)

                fmt.Printf("Received invoice `%v` for $ %.2f \n", invoice.InvoiceNumber, invoice.Amount)
            }
        }(f)

        time.Sleep(10 * time.Millisecond)
    }
}

type Invoice struct {
    InvoiceNumber string
    Amount        float64
    OrderID       int
    Date          time.Time
}

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