CSV 与 JSON 互转指南:方法、陷阱与最佳实践
运营团队给你发了一份 CSV 导出文件,而你的 API 只接受 JSON。打开文件一看,10,000 行逗号分隔的数据摆在面前,你不禁想:怎样才能最快地把 CSV 转成 JSON 而不丢失数据?
本指南涵盖四种转换方法(浏览器工具、JavaScript、Python、CLI)、反向转换(JSON 转 CSV)、五个会悄悄破坏数据的常见陷阱,以及如何处理无法完整载入内存的大文件。
CSV 与 JSON:何时使用哪种格式
在开始转换之前,先了解两种格式各自的优势。
| 维度 | CSV | JSON |
|---|---|---|
| 结构 | 扁平表格(行和列) | 嵌套层级(对象、数组) |
| 数据类型 | 所有值都是字符串 | string、number、boolean、null |
| 可读性 | 电子表格友好 | 开发者友好 |
| 主要用途 | 数据导出/导入、报表、ETL | API、配置文件、NoSQL 存储 |
| 文件大小 | 更小(无重复键名) | 更大(每条记录都重复键名) |
| Schema | 隐式(表头行) | 显式(或使用 JSON Schema) |
经验法则: 当数据是表格形式且使用方是电子表格或数据管道时,选择 CSV。当数据有层级结构或使用方是 API 时,选择 JSON。你可以随时用 JSON Formatter 验证输出的 JSON,尽早发现结构问题。
如果你的项目使用 JSON5 或 JSONC 等宽松 JSON 格式来管理配置,请参阅我们的 JSON5 与 JSONC 格式化指南,了解语法差异与工具支持。
4 种 CSV 转 JSON 的方法
方法 1 — 浏览器在线工具
对于一次性转换,浏览器工具是最快的途径。将 CSV 粘贴到在线转换器中,得到 JSON 输出,然后用 JSON Formatter 验证结果,确认结构正确。
优势在于:数据始终留在浏览器中。无需上传、无服务器处理、无隐私顾虑。当你处理内部数据、导出文件中嵌入的 API 密钥,或任何不希望发送到第三方服务器的内容时,这一点尤为重要。
最适合:小文件(10 MB 以下)、快速的一次性转换,以及非技术团队成员。
方法 2 — JavaScript / Node.js
浏览器端(原生 JS):
function csvToJson(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
return lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, i) => {
obj[header] = values[i]?.trim() ?? '';
return obj;
}, {});
});
}
const csv = `name,age,city
Alice,30,New York
Bob,25,London`;
console.log(JSON.stringify(csvToJson(csv), null, 2));
这段代码适用于没有引号字段的简单 CSV。如果在生产环境中需要处理值内的逗号、字段中的换行符或带引号的字符串,请使用专业的解析器。
Node.js(csv-parser + 流):
import { createReadStream } from 'fs';
import { parse } from 'csv-parse';
const records = [];
createReadStream('data.csv')
.pipe(parse({ columns: true, trim: true, skip_empty_lines: true }))
.on('data', (row) => records.push(row))
.on('end', () => {
console.log(JSON.stringify(records, null, 2));
});
columns: true 选项使用第一行作为键名。trim 选项去除值两端的空白字符。该方案能正确处理引号字段、转义逗号和多行值。
方法 3 — Python
标准库(零依赖):
import csv
import json
with open('data.csv', encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = list(reader)
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(rows, f, indent=2, ensure_ascii=False)
csv.DictReader 使用表头行作为键,将每一行映射为字典。ensure_ascii=False 参数保留 Unicode 字符(中文、日文、带重音字符),而不是将它们转义为 \uXXXX。
pandas(数据科学家的一行代码):
import pandas as pd
df = pd.read_csv('data.csv')
df.to_json('data.json', orient='records', indent=2, force_ascii=False)
如何选择:
- csv + json:轻量级脚本、Lambda 函数、希望最小化依赖的容器环境。
- pandas:当你在转换之前还需要清洗、过滤或转换数据时。在不仅仅是格式转换的场景下,导入 pandas 的开销是值得的。
方法 4 — CLI 工具
适用于 Shell 脚本和自动化管道:
csvkit:
# Install: pip install csvkit
csvjson data.csv > data.json
Miller (mlr):
# Install: brew install miller (macOS) or apt install miller (Ubuntu)
mlr --csv --json cat data.csv > data.json
配合 jq 进行过滤:
# Convert and filter in one pipeline
csvjson data.csv | jq '[.[] | select(.age | tonumber > 25)]'
Miller 尤为强大,因为它原生支持 CSV、JSON、TSV 等多种格式。你可以在转换过程中同时进行数据变换:
# Convert CSV to JSON, rename a field, add a computed field
mlr --csv --json rename name,fullName then put '$age_group = ($age > 30) ? "senior" : "junior"' data.csv
JSON 转 CSV:处理反向转换
将 JSON 转为 CSV 会引入正向转换中不存在的挑战。
展平嵌套对象
CSV 本质上是扁平的。当 JSON 包含嵌套对象时,你需要一种展平策略:
{
"name": "Alice",
"address": {
"city": "New York",
"zip": "10001"
}
}
转换后变为:
| name | address.city | address.zip |
|---|---|---|
| Alice | New York | 10001 |
点号表示法(address.city)是最常见的方式。用 Python 实现:
import pandas as pd
data = [
{"name": "Alice", "address": {"city": "New York", "zip": "10001"}},
{"name": "Bob", "address": {"city": "London", "zip": "EC1A"}}
]
df = pd.json_normalize(data)
df.to_csv('output.csv', index=False)
# Columns: name, address.city, address.zip
处理数组
数组字段需要做出决策:
| 策略 | 示例输入 | CSV 输出 | 适用场景 |
|---|---|---|---|
| 拼接为字符串 | ["admin","editor"] | admin;editor | 简单列表,可重新导入 |
| 展开为多列 | ["admin","editor"] | role_0: admin, role_1: editor | 固定长度数组 |
| 展开为多行 | ["admin","editor"] | 每个角色一行,共两行 | 关系型分析 |
根据下游消费者的需求来选择。如果 CSV 最终要导回数据库,展开为多行通常最合理。
类型信息丢失
CSV 没有类型系统。将 JSON 转为 CSV 时:
true变成字符串"true"— 它是布尔值还是字符串?null变成空单元格 — 与空字符串""无法区分42变成"42"— 它是数字还是字符串?
如果需要保证往返转换的保真度(CSV -> 处理 -> JSON),请在注释行或配套的 Schema 文件中记录你的类型约定。
5 个常见陷阱及其解决方法
这些问题会悄无声息地破坏数据。大多数教程都跳过了它们。不要等到生产环境中才学到教训。
1. 编码地雷
问题: 你打开同事发来的 CSV 文件,看到 é 而不是 é,或者 锟斤拷 而不是中文字符。
原因: 文件以某种编码(Windows-1252、GBK、Shift_JIS)保存,但解析器默认使用 UTF-8。Windows 上的 Excel 通常以 Windows-1252 保存 CSV,或者添加 UTF-8 BOM(字节顺序标记 — 文件开头不可见的 \xEF\xBB\xBF)。
解决方法:
# Detect encoding first
import chardet
with open('data.csv', 'rb') as f:
result = chardet.detect(f.read(10000))
print(result) # {'encoding': 'Windows-1252', 'confidence': 0.73}
# Then read with the correct encoding
with open('data.csv', encoding=result['encoding']) as f:
reader = csv.DictReader(f)
# ...
在 Node.js 中,需要显式去除 BOM:
import { readFileSync } from 'fs';
let content = readFileSync('data.csv', 'utf-8');
// Strip UTF-8 BOM if present
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
2. 分隔符混淆
问题: 解析器输出了一个巨大的单列,而不是多个字段。
原因: 在许多欧洲地区(法国、德国、西班牙),Excel 使用分号(;)作为 CSV 分隔符,因为逗号被用作小数点分隔符(例如 3,14 而非 3.14)。制表符分隔文件(.tsv)是另一种变体。
解决方法: 通过采样前几行自动检测分隔符:
import csv
with open('data.csv') as f:
sample = f.read(8192)
dialect = csv.Sniffer().sniff(sample, delimiters=',;\t|')
f.seek(0)
reader = csv.DictReader(f, dialect=dialect)
3. 前导零消失
问题: 邮政编码 00501 变成了 501。产品编码 007 变成了 7。
原因: 解析器(或 Excel)将字段解释为数字并去掉了前导零。这对于邮政编码、电话号码和 ID 编码尤其危险。
解决方法: 强制字符串类型。在 pandas 中:
df = pd.read_csv('data.csv', dtype={'zip': str, 'product_code': str})
在 JavaScript 中,检查原始字符串是否与数值解析结果不同:
function preserveLeadingZeros(value) {
if (/^0\d+$/.test(value)) return value; // Keep as string
const num = Number(value);
return isNaN(num) ? value : num;
}
4. 大数精度丢失
问题: ID 9007199254740993 在 JSON 中变成了 9007199254740992。
原因: JavaScript 的 Number 是 64 位浮点数(IEEE 754)。超过 Number.MAX_SAFE_INTEGER(2^53 - 1 = 9007199254740991)的整数会丢失精度。这会影响数据库 ID、Snowflake ID 和 Twitter/X 推文 ID。
解决方法: 在 JSON 中将大数保留为字符串,或在处理代码中使用 BigInt:
// Parse with string preservation for large numbers
function safeParseNumber(value) {
const num = Number(value);
if (Number.isInteger(num) && !Number.isSafeInteger(num)) {
return value; // Keep as string to preserve precision
}
return isNaN(num) ? value : num;
}
5. 空值歧义
问题: CSV 中有空单元格。转换后,无法分辨原始值是空字符串 ""、null,还是根本缺失。
原因: CSV 无法区分这三种状态。两个逗号之间的空字段(Alice,,30)可能表示其中任何一种。
解决方法: 定义约定并一致地执行:
def parse_value(value):
if value == '':
return None # or '' — pick one convention
if value == 'NULL' or value == 'null':
return None
return value
如果数据中使用了 NULL、N/A 或 - 等标记值,请明确记录并处理它们。
流式处理大文件
当 CSV 文件超过 100 MB 时,将其完整载入内存不是可行的方案。使用流式处理。
Node.js(流管道):
import { createReadStream, createWriteStream } from 'fs';
import { parse } from 'csv-parse';
import { Transform } from 'stream';
import { pipeline } from 'stream/promises';
let first = true;
const toJsonArray = new Transform({
objectMode: true,
transform(record, encoding, callback) {
const prefix = first ? '[\n' : ',\n';
first = false;
callback(null, prefix + JSON.stringify(record));
},
flush(callback) {
callback(null, '\n]');
}
});
await pipeline(
createReadStream('large.csv'),
parse({ columns: true, trim: true }),
toJsonArray,
createWriteStream('large.json')
);
Python(生成器):
import csv
import json
def csv_rows(path):
with open(path, encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
yield row
# Stream to JSON Lines format (one JSON object per line)
with open('large.jsonl', 'w', encoding='utf-8') as out:
for row in csv_rows('large.csv'):
out.write(json.dumps(row, ensure_ascii=False) + '\n')
对于超大文件,建议使用 JSON Lines(.jsonl)格式而非单个 JSON 数组。每一行都是一个独立的 JSON 对象,这意味着你也可以逐行处理输出文件,无需一次性解析整个文件。
常见问题
CSV 和 JSON 有什么区别?
CSV(Comma-Separated Values,逗号分隔值)以扁平表格形式存储数据,所有值都是字符串。JSON(JavaScript Object Notation)存储结构化数据,支持嵌套对象、数组以及类型化的值(字符串、数字、布尔值、null)。CSV 文件更小且对电子表格友好;JSON 表达能力更强且对 API 友好。
如何在 JavaScript 中将 CSV 转为 JSON?
在 Node.js 中使用 csv-parse 包:通过 createReadStream 读取文件,用管道传入 parse({ columns: true }),然后收集结果。在浏览器端,使用 FileReader 读取文件,按换行符分割,然后以表头行作为键名将各行映射为对象。
如何在 Python 中将 CSV 转为 JSON?
使用标准库中的 csv.DictReader 将行读取为字典,然后用 json.dump() 将其写为 JSON 数组。如果需要在转换前进行数据处理,pandas.read_csv() 加上 df.to_json(orient='records') 是一行代码的替代方案。
嵌套 JSON 可以转为 CSV 吗?
可以,但需要一种展平策略。最常见的方式是使用点号表示法:像 address.city 这样的字段会变成列标题。在 Python 中,pandas.json_normalize() 可以自动处理此问题。数组需要额外的决策 — 拼接为字符串、展开为多列,或展开为多行。
为什么 CSV 转换后出现乱码?
编码不匹配。文件可能以 Windows-1252 或 GBK 编码保存,但解析器默认使用 UTF-8。使用 chardet(Python)等检测库识别编码,然后在读取时显式指定。同时检查某些工具自动添加的 UTF-8 BOM。
如何处理超过 100 MB 的 CSV 文件?
使用流式处理,而非将整个文件载入内存。在 Node.js 中,通过流管道传入 csv-parse。在 Python 中,使用生成器配合 csv.DictReader 逐行迭代。建议输出 JSON Lines(.jsonl)格式而非单个 JSON 数组,以便下游更容易处理。
如何验证转换后的 JSON 是否有效?
将输出粘贴到在线 JSON Formatter 中,检查语法、结构和嵌套关系。如需自动化验证,在 JavaScript 中使用 JSON.parse(),在 Python 中使用 json.loads() — 两者都会对无效输入抛出明确的错误。如需 Schema 验证,定义 JSON Schema 并以编程方式进行校验。
要点总结
- 选择适合场景的方法:浏览器工具适合快速的一次性转换,代码适合自动化,CLI 适合管道处理。
- 警惕编码问题 — 始终显式指定编码,而非依赖默认值。
- 有意识地保留类型 — 前导零、大整数和 null 值都需要显式处理。
- 流式处理大文件,而非将其载入内存。对于超大数据集,考虑使用 JSON Lines 格式。
- 验证输出 — 将转换后的 JSON 通过 JSON Formatter 检查,在上线前捕获结构问题。