CSV 格式说明和应用
问题
我们常常将多个字符串 item 使用逗号拼接成一个字符串,用来表示数组,使用时再用逗号切割成为数组。比如安卓机型列表:
ALN-AL10,ALN-AL10,BRA-AL00,ALN-AL00/ALN-AL80
直到有一天,苹果设备也要用到这个机型列表,而它的每个机型都带着逗号,那我们使用逗号切割就得到了错误的数据。
iPhone15: iPhone15,4
iPhone15Plus: iPhone15,5
iPhone15Pro: iPhone16,1
iPhone15Pro_Max: iPhone16,2
为了解决这个问题,首先想到了换一个分隔符,比如 |,再比如用一些不可见字符 0x01。但我们不能保证这些字符串 item 一定不包含这些特殊字符,也许还有更好的方法。
既然是逗号分隔,首先想到的就是 CSV 格式,毕竟 CSV 的全称就是 Comma-Separated Values(逗号分隔值)。它是如何解决这个问题的?
CSV 格式(RFC 4180)
CSV 的正式说明文档:RFC 4180,2005 年由 Y. Shafranovich 发布,是 Informational 级别的标准。
7 条核心规则
- 每条记录占一行,以 CRLF(
\r\n)结尾 - 最后一条记录尾随换行符可选
- 可选表头行,与普通记录格式完全相同
- 字段以逗号分隔;行内字段数必须一致;空格是字段的一部分,不忽略;最后字段不能有尾随逗号
- 字段可以用双引号包裹,也可以不包裹;不包裹时,字段内不能出现双引号
- 字段含换行符、双引号、逗号时,必须用双引号包裹
- 双引号内出现双引号,必须用两个双引号转义(
"")
ABNF 语法
RFC 4180 定义的正式语法:
file = [header CRLF] record *(CRLF record) [CRLF]
header = name *(COMMA name)
record = field *(COMMA field)
field = (escaped / non-escaped)
escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE
non-escaped = *TEXTDATA
TEXTDATA = %x20-21 / %x23-2B / %x2D-7E
;即可见ASCII字符(0x20-0x7E),排除双引号(0x22)和逗号(0x2C)
示例说明
姓名,年龄,城市,备注
张三,30,北京,无备注
李四,25,上海,"喜欢, 打篮球"
王五,28,"广州, 广东",""
"李, 六",35,"""特别"" 市","这是一段
跨行的备注"
"陈, 七","40","深圳",
解读:
| 原始值 | CSV 表示 | 说明 |
|---|---|---|
喜欢, 打篮球 |
"喜欢, 打篮球" |
含逗号 → 引号包裹 |
广州, 广东 |
"广州, 广东" |
同上 |
""(空备注) |
"" |
两个双引号表示空字符串 |
李, 六 |
"李, 六" |
姓名含逗号 → 引号包裹 |
"特别" 市 |
"""特别"" 市" |
含双引号 → 双重转义 |
| 跨行字段 | "这是一段↵跨行的备注" |
含换行 → 引号包裹 |
使用 CSV 工具包可以非常方便地解析这些数据。后台表格导出 CSV 文件也应当使用 CSV 工具包,而非手动拼接字符串。
其他格式的对比
| 格式 | 分隔符 | 引号规则 | 适用场景 |
|---|---|---|---|
| CSV | 逗号 , |
双引号包裹,含 ," 需双重转义 |
表格数据交换 |
| TSV | 制表符 \t |
同 CSV 规则 | Excel 复制粘贴 |
| JSON 数组 | 无分隔符 | 无需转义(结构化) | API 响应、Web 前端 |
| Pipe-separated | | |
类似 CSV | 日志分析 |
如果数据中同时包含逗号、双引号和换行符,JSON 数组是更可靠的选择。如果只是简单的一维字符串列表,且确认不会含分隔符,才考虑手工拼接。
Go 语言示例
Go 标准库 encoding/csv 完整支持 RFC 4180:
package main
import (
"encoding/csv"
"fmt"
"io"
"io"
"log"
"strings"
)
func main() {
in := `姓名,年龄,城市,备注
张三,30,北京,无备注
李四,25,上海,"喜欢, 打篮球"
王五,28,"广州, 广东",""
"李, 六",35,"""特别"" 市","这是一段
跨行的备注"
"陈, 七","40","深圳","包含""双引号""和,逗号"`
r := csv.NewReader(strings.NewReader(in))
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", record)
}
}
输出:
[]string{"姓名", "年龄", "城市", "备注"}
[]string{"张三", "30", "北京", "无备注"}
[]string{"李四", "25", "上海", "喜欢, 打篮球"}
[]string{"王五", "28", "广州, 广东", ""}
[]string{"李, 六", "35", "\"特别\" 市", "这是一段\n跨行的备注"}
[]string{"陈, 七", "40", "深圳", "包含\"双引号\"和,逗号"}
常用工具函数
package csvutil
import (
"bytes"
"encoding/csv"
"strings"
)
// SliceToCsvString 将字符串切片转换为 CSV 字符串
func SliceToCsvString(slice []string) (string, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
if err := writer.Write(slice); err != nil {
return "", err
}
writer.Flush()
if err := writer.Error(); err != nil {
return "", err
}
return buf.String(), nil
}
// CsvStringToSlice 将 CSV 字符串转换为字符串切片
func CsvStringToSlice(csvString string) ([]string, error) {
reader := csv.NewReader(strings.NewReader(csvString))
reader.ReuseRecord = true
records, err := reader.Read()
if err != nil {
return nil, err
}
return records, nil
}
Python 示例
Python 内置 csv 模块,同样完整支持 RFC 4180:
import csv
from io import StringIO
data = """\
姓名,年龄,城市,备注
张三,30,北京,无备注
李四,25,上海,"喜欢, 打篮球"
王五,28,"广州, 广东",""
"李, 六",35,"""特别"" 市","跨行的
备注"
"""
reader = csv.reader(StringIO(data))
for row in reader:
print(row)
输出:
['姓名', '年龄', '城市', '备注']
['张三', '30', '北京', '无备注']
['李四', '25', '上海', '喜欢, 打篮球']
['王五', '28', '广州, 广东', '']
['李, 六', '35', '"特别" 市', '跨行的\n备注']
Python 的 csv.reader 默认按 RFC 4180 解析。若读取的文件含 BOM(\ufeff),需手动跳过:
with open('data.csv', 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
常见陷阱
1. CRLF 问题
Unix 系统用 LF(\n),旧版 Mac 用 CR(\r),CSV 标准是 CRLF(\r\n)。部分解析器对单 \n 兼容,但严格按 RFC 4180 应当处理 CRLF。
// Go: 强制使用 CRLF 行结尾
reader := csv.NewReader(f)
reader.LazyQuotes = true // 允许引号跨行
2. BOM 导致首字段前多出一个字符
UTF-8 with BOM 的文件,\ufeff 会被 csv.reader 当作字段内容读取。始终用 utf-8-sig 编码打开文件,或先手动 strip。
# Python
with open('file.csv', encoding='utf-8-sig') as f:
reader = csv.reader(f)
// Go
import "golang.org/x/text/encoding/unicode"
// 或者读取后手动去掉 \ufeff
3. Excel 兼容性
Microsoft Excel 导出 CSV 时:
- 对含逗号字段直接裸写,不加引号(不符合 RFC 4180 第5条规则)
- 数值类型自动去掉前导零(如
00123→123) - 用系统默认编码而非 UTF-8
如果导出的 CSV 要给 Excel 用户,打开时建议另存为 UTF-8 with BOM。
4. 字段数不一致
RFC 4180 要求每行列数相同。解析时发现字段数突变,通常是:
- 前一行某字段含未转义的换行符
- 某字段引号不匹配
- 文件编码问题导致字符被截断
5. 转义字符 \ 不起作用
CSV 的转义只有一种方式:引号包裹,内部双引号用 ""。\ 不是 CSV 的转义字符。如果看到 "like\"this",这不是标准 CSV。
各语言工具包推荐
| 语言 | 工具包 | 备注 |
|---|---|---|
| Go | encoding/csv |
标准库,完整 RFC 4180 |
| Python | csv |
标准库,csv.QUOTE_MINIMAL 等多种模式 |
| JavaScript | csv-parse / papaparse |
浏览器和 Node 都支持 |
| Java | OpenCSV / Apache Commons CSV | |
| Rust | csv crate |
标准库级别质量 |
| PHP | str_getcsv() / fgetcsv() |
内置函数 |
总结
当你的数据中可能出现逗号、双引号或换行符时,CSV 的 RFC 4180 标准通过"引号包裹 + 双引号双重转义"机制提供了可靠的解决方案。
核心规则只有两条:
- 含特殊字符的字段 → 双引号包裹
- 引号内的双引号 → 用两个双引号
""表示
使用标准库的工具包处理,不要手写字符串拼接——手写永远会有遗漏的边界情况。