Go 中 encoding/csv 读写 CSV 文件
Go 中 encoding/csv 读写 CSV 文件
为什么需要 encoding/csv
encoding/csv 是 Go 标准库中的包,专门用于读写 CSV(逗号分隔值)格式的数据。它遵循 RFC 4180 标准,并提供了灵活的配置,可以处理各种方言的 CSV 文件(如自定义分隔符、引号转义等)。相比于手动解析字符串,使用该包可以避免大量边界条件问题,例如字段内嵌逗号、换行符、引号转义等。
读取 CSV 文件
基本读取:使用 csv.NewReader
csv.NewReader 接受一个 io.Reader,返回一个 *csv.Reader。通过 Read 方法可以逐行读取记录,每次返回 []string。
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"strings"
)
func main() {
data := "name,age,city\nAlice,30,New York\nBob,25,London"
r := csv.NewReader(strings.NewReader(data))
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Println(record) // [name age city] 等
}
}
一次性读取所有记录:ReadAll
当 CSV 数据较小时,可以使用 ReadAll 将所有行读入内存。
r := csv.NewReader(strings.NewReader(data))
records, err := r.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, record := range records {
fmt.Println(record)
}
配置 csv.Reader
csv.Reader 提供了多个导出字段用于自定义解析行为:
Comma:字段分隔符,默认为','。Comment:注释字符,如果设置,以该字符开头的行将被忽略。FieldsPerRecord:每条记录的字段数。0表示使用第一行的字段数作为预期值;负数表示不检查。LazyQuotes:允许引号内的引号可以不转义(非标准但常见)。TrimLeadingSpace:忽略字段前的空白字符。
示例:读取以制表符分隔的文件,并忽略空行开头的空格。
r := csv.NewReader(file)
r.Comma = '\t'
r.TrimLeadingSpace = true
r.Comment = '#'
写入 CSV 文件
基本写入:使用 csv.NewWriter
csv.NewWriter 接受一个 io.Writer,通过 Write 方法写入一条记录(字符串切片)。
package main
import (
"encoding/csv"
"os"
)
func main() {
file, _ := os.Create("output.csv")
defer file.Close()
w := csv.NewWriter(file)
defer w.Flush() // 确保所有缓存数据写入
w.Write([]string{"name", "age", "city"})
w.Write([]string{"Alice", "30", "New York"})
w.Write([]string{"Bob", "25", "London"})
// 检查写入过程中的错误
if err := w.Error(); err != nil {
panic(err)
}
}
重要:调用 Flush 后应检查 Error(),因为写入错误可能是延迟的。
一次性写入所有记录:WriteAll
records := [][]string{
{"name", "age", "city"},
{"Alice", "30", "New York"},
{"Bob", "25", "London"},
}
w := csv.NewWriter(os.Stdout)
w.WriteAll(records) // 内部会调用 Flush
if err := w.Error(); err != nil {
log.Fatal(err)
}
配置 csv.Writer
Comma:分隔符,默认','。UseCRLF:是否使用\r\n作为行终止符(默认只用\n)。
例如,输出 Excel 兼容的 CSV(分号分隔,CRLF 换行):
w.Comma = ';'
w.UseCRLF = true
处理特殊字符与引号
CSV 标准规定字段内如果包含分隔符、双引号或换行符,必须用双引号包裹,且里面的双引号需要转义(写成两个双引号)。encoding/csv 会自动处理这些规则:
- 写入时:
Write会根据内容决定是否添加引号,并对双引号进行转义。 - 读取时:
Read会自动去除外层引号并将转义的双引号还原为一个单引号。
自定义引号行为可以通过 LazyQuotes 处理非法格式的 CSV,但建议仅在处理非标准输入时使用。
错误处理最佳实践
- 逐行读取时:检查
err == io.EOF来结束循环,其他错误应停止处理。 - 写入后:务必调用
Flush()并检查w.Error()。 - 字段数验证:设置
FieldsPerRecord为正数来强制校验每行字段数,防止数据错位。 - 处理 BOM:如果 CSV 文件可能带有 UTF‑8 BOM,需要在
io.Reader上包装一个去除 BOM 的 reader,或者使用golang.org/x/text/encoding等包。
func readCSVWithBOM(filename string) ([][]string, error) {
f, _ := os.Open(filename)
defer f.Close()
// 简单去除 BOM 的方式(仅适用于 UTF‑8)
br := bufio.NewReader(f)
rune, _, _ := br.ReadRune()
if rune != '\uFEFF' {
br.UnreadRune() // 不是 BOM,放回
}
r := csv.NewReader(br)
return r.ReadAll()
}
参数与容错示例
忽略空行
Reader 默认会跳过完全不包含字符的空行(所有字段都空的行)。如果需要保留空行(例如空行代表缺失记录),需降低处理层级或自行实现分割。
指定每行字段数
r := csv.NewReader(file)
r.FieldsPerRecord = 5 // 要求严格5个字段
如果行字段数不符,Read 将返回错误 ErrFieldCount。
允许字段前空格
r.TrimLeadingSpace = true
这一选项会去掉字段前的空格,对人为对齐的 CSV 很有用。
性能提示
- 对于大文件,使用逐行
Read而不是ReadAll以降低内存占用。 - 写入时,可以适当调整缓冲区大小(
csv.Writer底层使用bufio.Writer,可通过Set Writer方式自定义缓冲大小)。 - 如果需要并发读取,建议将文件分段(注意行对齐)分别构造
Reader。
完整示例:过滤并转换 CSV 数据
下面的程序读取一个 CSV,只保留年龄大于 20 的记录,并写入新文件,使用分号分隔且使用 CRLF 换行。
package main
import (
"encoding/csv"
"io"
"log"
"os"
"strconv"
)
func main() {
input, _ := os.Open("input.csv")
defer input.Close()
output, _ := os.Create("output.csv")
defer output.Close()
reader := csv.NewReader(input)
reader.TrimLeadingSpace = true
writer := csv.NewWriter(output)
writer.Comma = ';'
writer.UseCRLF = true
defer func() {
writer.Flush()
if err := writer.Error(); err != nil {
log.Fatal(err)
}
}()
// 读取并写入标题
header, err := reader.Read()
if err != nil {
log.Fatal(err)
}
writer.Write(header)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
// 假设年龄在第二列
age, err := strconv.Atoi(record[1])
if err != nil {
continue // 跳过无法解析年龄的行
}
if age > 20 {
writer.Write(record)
}
}
}
常见问题排查
- 读取到空字段或字段拆分错误:检查分隔符设置是否正确(如 TSV 应设置
Comma = '\t')。 - 写入后文件末尾缺少换行:
csv.Writer会在每条记录后写入换行符,文件末尾可能没有最后的新的一行,这是正常的。 - 中文乱码:确保文件编码为 UTF‑8,或者使用
golang.org/x/text转换编码。 - 引号处理异常:如果输入 CSV 中引号不成对,可能是数据错误,尝试设置
LazyQuotes = true容忍。
小结
encoding/csv 包提供了简洁、标准兼容且可配置的 CSV 读写能力,是处理 CSV 数据的首选方案。掌握它的读写方法、配置项和错误处理模式,可以安全且高效地完成绝大多数 CSV 相关任务。