CSV转JSON JavaScript指南
直接在浏览器中使用免费的 CSV转JSON,无需安装。
在线试用 CSV转JSON →我遇到的大多数 CSV 数据都以平面字符串的形式出现——来自文件上传、数据库导出,或者仍在使用 1970 年代格式的 API。 要在 JavaScript 中 将 CSV 转换为 JSON,你需要语言本身提供的两样东西: 用于解析行的字符串拆分,以及用于序列化结果的 JSON.stringify()。 基础功能无需任何 npm 包——本指南涵盖从可复用的 csvToJson() 工具函数到 PapaParse 和 Node.js 文件 I/O 的完整流程。如需快速转换而无需编写代码, 在线 CSV 转 JSON 工具 可即时完成转换。所有示例适用于 Node.js 18+ 及现代浏览器。
- ✓按换行符拆分 CSV,从第 0 行提取标题,将剩余行映射为对象,然后用 JSON.stringify(array, null, 2) 获得格式化输出。
- ✓JSON.stringify() 生成字符串;JSON.parse() 将其转换回活跃的 JavaScript 数组——操作前务必确认你持有的是哪种类型。
- ✓Map 实例不会自动序列化为 JSON——需先调用 Object.fromEntries(map) 将其转换为普通对象。
- ✓对于包含引号字段、值中含有逗号或单元格内有换行符的 CSV,请使用 PapaParse 或 csv-parse,而不是手动拆分。
- ✓csvtojson(npm)通过单次调用处理类型转换、流式处理和 RFC 4180 边缘情况。
什么是 CSV 转 JSON 转换?
CSV 转 JSON 转换将平面的逗号分隔文本格式转变为结构化的对象数组,其中每一行成为一个以列标题为键的 JavaScript 对象。 CSV 格式没有数据类型——所有内容都是字符串。JSON 增加了结构、嵌套和显式类型(数字、布尔值、null)。 这种转换是几乎所有以电子表格导出、旧系统数据转储或用户上传文件为起点的数据管道的第一步。 底层数据保持不变;格式从基于位置的列变为命名属性。
name,email,role,active 陈明,chenming@nexuslabs.io,工程负责人,true 李芳,lifang@nexuslabs.io,产品经理,true
[
{
"name": "陈明",
"email": "chenming@nexuslabs.io",
"role": "工程负责人",
"active": "true"
},
{
"name": "李芳",
"email": "lifang@nexuslabs.io",
"role": "产品经理",
"active": "true"
}
]csvToJson() — 构建可复用的转换函数
JavaScript 中完整的 CSV 转 JSON 流程分为三步:按换行符拆分 CSV 字符串以获取行, 用 split(',') 从第一行提取标题,然后将每个剩余行映射为普通 JavaScript 对象,键来自标题,值来自对应列的位置。 最后调用 JSON.stringify() 将对象数组转换为 JSON 字符串。以下是一个最简可用版本:
function csvToJson(csv) {
const lines = csv.trim().split('\n')
const headers = lines[0].split(',').map(h => h.trim())
const rows = lines.slice(1)
.filter(line => line.trim() !== '')
.map(line => {
const values = line.split(',')
return Object.fromEntries(
headers.map((header, i) => [header, values[i]?.trim()])
)
})
return JSON.stringify(rows, null, 2)
}
const csv = `server,port,region,status
api-gateway,8080,us-east-1,healthy
auth-service,8443,eu-west-1,degraded
payments-api,9090,ap-south-1,healthy`
console.log(csvToJson(csv))
// [
// { "server": "api-gateway", "port": "8080", "region": "us-east-1", "status": "healthy" },
// { "server": "auth-service", "port": "8443", "region": "eu-west-1", "status": "degraded" },
// { "server": "payments-api", "port": "9090", "region": "ap-south-1", "status": "healthy" }
// ]这个函数处理了基本情况:尾部换行符、空行、值周围的空白字符。 每个 CSV 字段都以字符串形式传递。注意 port 是 "8080" (字符串),而不是 8080 (数字)。如果 JSON 输出中需要正确的类型,你必须自行进行类型转换。 以下是带有类型检测的扩展版本:
function coerceValue(val) {
if (val === undefined || val === '') return null
if (val === 'true') return true
if (val === 'false') return false
if (val === 'null') return null
const num = Number(val)
if (!isNaN(num) && val.trim() !== '') return num
return val
}
function csvToJson(csv, { coerce = false } = {}) {
const lines = csv.trim().split('\n')
const headers = lines[0].split(',').map(h => h.trim())
const rows = lines.slice(1)
.filter(line => line.trim() !== '')
.map(line => {
const values = line.split(',')
return Object.fromEntries(
headers.map((header, i) => [
header,
coerce ? coerceValue(values[i]?.trim()) : values[i]?.trim()
])
)
})
return JSON.stringify(rows, null, 2)
}
const csv = `endpoint,port,max_connections,debug
/api/v2/orders,8443,500,true
/api/v2/health,8080,100,false`
console.log(csvToJson(csv, { coerce: true }))
// [
// { "endpoint": "/api/v2/orders", "port": 8443, "max_connections": 500, "debug": true },
// { "endpoint": "/api/v2/health", "port": 8080, "max_connections": 100, "debug": false }
// ]coerce 标志是可选启用的,因为自动类型检测可能适得其反——像邮政编码("07302")这样的字段在转换为数字时会丢失前导零。 默认关闭类型转换,仅在你掌控数据结构时才启用。快速提示: JSON.stringify() 接受第三个 space 参数用于缩进。传入 2 表示两个空格, 4 表示四个空格,或 "\t" 表示制表符。完全省略则输出紧凑单行——当将 JSON 字符串作为 API 请求体发送时很有用,因为空白字符只会浪费带宽。
JSON.parse() 会抛出 SyntaxError。你必须先用 csvToJson() 函数将 CSV 转换为 JavaScript 对象,该函数内部调用 JSON.stringify() 来生成实际的 JSON 字符串。处理 CSV 数据中的 Map、Date 和自定义对象
并非每次 CSV 转换都以普通对象的平面数组结束。有时你需要从标题-值对构建 Map, 将日期字符串解析为 Date 对象,或在序列化之前附加计算属性。 JavaScript 有一个会让人出错的特性:Map 实例无法用 JSON.stringify() 序列化。你会得到一个空对象。解决方法是使用 Object.fromEntries() 在序列化之前将 Map 转换回普通对象。
Map 序列化为 {} 的原因是 JSON.stringify() 会遍历对象自身的可枚举属性。Map 将其条目存储在内部槽中,而不是对象本身的可枚举属性上, 因此序列化器看到的是一个没有键的对象。Map 原型也缺少 toJSON() 方法,而这正是 JSON.stringify() 在决定如何序列化任何值之前首先调用的钩子。如果一个值有 toJSON(), 则该方法的返回值才是被序列化的内容——而非对象本身。这就是为什么 Date 对象能正确序列化:Date.prototype.toJSON 返回 ISO 8601 字符串,因此 JSON.stringify(new Date()) 生成带引号的时间戳而不是空对象。理解这个钩子后,你可以在自己的类上定义相同的行为——如下方的 EmployeeRecord 示例所示——精确控制哪些 CSV 派生字段出现在最终 JSON 输出中。
将 Map 转换为 JSON
// 从 CSV 标题→值对构建 Map
const headers = ['server', 'port', 'region']
const values = ['payments-api', '9090', 'ap-south-1']
const rowMap = new Map(headers.map((h, i) => [h, values[i]]))
// Map 不能直接序列化
console.log(JSON.stringify(rowMap))
// "{}" — 空对象,数据丢失!
// 先转换为普通对象
const rowObj = Object.fromEntries(rowMap)
console.log(JSON.stringify(rowObj, null, 2))
// {
// "server": "payments-api",
// "port": "9090",
// "region": "ap-south-1"
// }日期字符串与 toJSON()
CSV 日期字段以字符串形式出现。如果在处理过程中将它们解析为 Date 对象, 这些 Date 对象可以正确序列化,因为 Date 有内置的 toJSON() 方法,返回 ISO 8601 字符串。你也可以在自己的类上定义自定义 toJSON() 来控制哪些 CSV 列出现在序列化输出中——例如,省略像 _rowIndex 这样的内部跟踪字段。
class EmployeeRecord {
constructor(csvRow) {
this._rowIndex = csvRow._rowIndex // 内部字段,不输出到 JSON
this.employeeId = csvRow.employee_id
this.name = csvRow.name
this.hiredAt = new Date(csvRow.hired_date)
this.salary = Number(csvRow.salary)
}
toJSON() {
// 仅暴露我们希望出现在 JSON 输出中的字段
return {
employee_id: this.employeeId,
name: this.name,
hired_at: this.hiredAt, // Date.toJSON() → 自动输出 ISO 字符串
salary: this.salary,
}
}
}
const csvRow = {
_rowIndex: 42,
employee_id: 'EMP-2847',
name: '陈明',
hired_date: '2024-03-15',
salary: '128000',
}
const record = new EmployeeRecord(csvRow)
console.log(JSON.stringify(record, null, 2))
// {
// "employee_id": "EMP-2847",
// "name": "陈明",
// "hired_at": "2024-03-15T00:00:00.000Z",
// "salary": 128000
// }
// 注意:_rowIndex 被排除,salary 为数字,日期为 ISO 格式使用 reviver 反序列化 Date
将 CSV 派生的 JSON 写入文件或通过网络发送后, JSON.parse() 会返回普通对象——Date 对象再次变为字符串。使用 reviver 函数在解析时将 ISO 8601 字符串转换回 Date 对象:
const jsonString = '{"employee_id":"EMP-2847","name":"陈明","hired_at":"2024-03-15T00:00:00.000Z","salary":128000}'
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
const parsed = JSON.parse(jsonString, (key, value) => {
if (typeof value === 'string' && isoDatePattern.test(value)) {
return new Date(value)
}
return value
})
console.log(parsed.hired_at instanceof Date) // true
console.log(parsed.hired_at.getFullYear()) // 2024JSON.stringify() 对包含函数、Symbol 或 undefined 属性的值返回 undefined(而不是字符串)。如果 CSV 派生的对象因缺失列而意外获得 undefined 值,该属性会从 JSON 输出中静默消失。 始终将缺失值默认设为 null。JSON.stringify() 参数参考
函数签名为 JSON.stringify(value, replacer?, space?)。replacer 和 space 参数均为可选——仅需缩进时,为 replacer 传入 null。
JSON.parse() 参数:
JSON.parse() — 消费 JSON 输出
一旦你从 csvToJson() 获得了 JSON 字符串,下一步通常是将其解析回活跃的 JavaScript 数组,用于过滤、映射或传入 API。 JSON 字符串(typeof === "string")和 JavaScript 对象之间的区别很重要。你不能对字符串调用 .filter() 或访问 [0].name ——你需要先调用 JSON.parse()。 这种往返操作(先 stringify 再 parse)也可用作验证技术:如果 CSV 转换产生了无效 JSON,parse 会抛出异常。 可选的 reviver 参数允许你在解析时转换每个键值对——适用于从 ISO 字符串恢复 Date 对象或无需额外遍历即可重命名键。
const csv = `endpoint,method,avg_latency_ms,error_rate
/api/v2/orders,POST,342,0.02
/api/v2/health,GET,12,0.00
/api/v2/payments,POST,890,0.15
/api/v2/users,GET,45,0.01`
// 第一步:将 CSV 转换为 JSON 字符串
const jsonString = csvToJson(csv, { coerce: true })
// 第二步:解析回 JavaScript 数组
const endpoints = JSON.parse(jsonString)
// 验证为数组
console.log(Array.isArray(endpoints)) // true
// 过滤高延迟端点
const slow = endpoints.filter(ep => ep.avg_latency_ms > 200)
console.log(slow.map(ep => ep.endpoint))
// ["/api/v2/orders", "/api/v2/payments"]
// 解构第一行
const [first, ...rest] = endpoints
console.log(first.endpoint) // "/api/v2/orders"
console.log(rest.length) // 3在对下游处理进行转换输出验证时,为 JSON.parse() 编写一个安全包装器很有用。如果 CSV 转换因任何原因(输入截断、编码错误)产生了格式错误的 JSON, 这可以在不崩溃的情况下捕获错误:
function safeParse(jsonString) {
try {
return { data: JSON.parse(jsonString), error: null }
} catch (err) {
return { data: null, error: err.message }
}
}
// 有效输出
const result = safeParse(csvToJson(csv))
if (result.error) {
console.error('CSV 转换产生了无效 JSON:', result.error)
} else {
console.log(`已解析 ${result.data.length} 行`)
}
// 误将原始 CSV 传入 JSON.parse — 这会失败
const bad = safeParse('name,email\n陈明,chenming@nexuslabs.io')
console.log(bad.error) // "Unexpected token '陈', "name,email"... is not valid JSON"使用 reviver 重命名键和验证结构
reviver 函数在解析时从最内层属性向外逐一接收每个键值对。对某个键返回 undefined 会将其从结果中完全删除;返回不同的值则会替换它。 reviver 适用于重命名标题(驼峰命名转下划线命名)、去除内部字段或检查必需列是否存在。 它最后以根值调用(空字符串键),如果结果不是数组,可在此处抛出错误。
const jsonString = csvToJson(`employeeId,firstName,hiredDate
EMP-2847,陈明,2024-03-15
EMP-3012,李芳,2023-11-01`, { coerce: false })
const camelToSnake = str => str.replace(/[A-Z]/g, c => '_' + c.toLowerCase())
const employees = JSON.parse(jsonString, function(key, value) {
// 根值——验证结构
if (key === '') {
if (!Array.isArray(value)) throw new Error('Expected JSON array from CSV')
return value
}
// 将驼峰命名的标题键重命名为下划线命名
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [camelToSnake(k), v])
)
}
return value
})
console.log(employees[0])
// { employee_id: 'EMP-2847', first_name: '陈明', hired_date: '2024-03-15' }从文件和 API 响应转换 CSV
CSV 数据在生产环境中实际来自两个地方:磁盘上的文件和 HTTP 响应。 两种场景都需要错误处理,因为输入是外部的且不受控制的。
读取 CSV 文件、转换并写入 JSON
import { readFileSync, writeFileSync } from 'node:fs'
function csvToJsonFromFile(inputPath, outputPath) {
let csvText
try {
csvText = readFileSync(inputPath, 'utf8')
} catch (err) {
throw new Error(`读取 ${inputPath} 失败:${err.message}`)
}
const lines = csvText.trim().split('\n')
if (lines.length < 2) {
throw new Error(`${inputPath} 没有数据行(只有 ${lines.length} 行)`)
}
const headers = lines[0].split(',').map(h => h.trim())
const rows = lines.slice(1)
.filter(line => line.trim() !== '')
.map(line => {
const values = line.split(',')
return Object.fromEntries(headers.map((h, i) => [h, values[i]?.trim()]))
})
const jsonOutput = JSON.stringify(rows, null, 2)
writeFileSync(outputPath, jsonOutput, 'utf8')
console.log(`已转换 ${rows.length} 行 → ${outputPath}`)
return rows
}
// 用法
const data = csvToJsonFromFile('inventory.csv', 'inventory.json')
console.log(data[0])
// { sku: "WDG-2847", warehouse: "us-east-1", quantity: "150", ... }从 API 端点获取 CSV
async function fetchCsvAsJson(url) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('text/csv') && !contentType.includes('text/plain')) {
console.warn(`意外的 content-type:${contentType}`)
}
const csvText = await response.text()
const lines = csvText.trim().split('\n')
const headers = lines[0].split(',').map(h => h.trim())
const rows = lines.slice(1)
.filter(line => line.trim() !== '')
.map(line => {
const values = line.split(',')
return Object.fromEntries(headers.map((h, i) => [h, values[i]?.trim()]))
})
return rows
}
// 示例:从数据提供商获取汇率 CSV
try {
const rates = await fetchCsvAsJson('https://data.ecb.internal/rates/daily.csv')
console.log(JSON.stringify(rates.slice(0, 3), null, 2))
// 以 JSON 形式发送给下游服务
await fetch('https://api.internal/v2/rates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: rates }),
})
} catch (err) {
console.error('汇率同步失败:', err.message)
}JSON.stringify() 中的 replacer 参数允许你从 CSV 中白名单特定列。 传入标题名称数组以仅包含这些字段: JSON.stringify(rows, ['name', 'email', 'department'])。 不在数组中的属性会从输出中静默排除。命令行 CSV 转 JSON 转换
Node.js 可以运行内联脚本,同时也有专用的 CLI 工具可以在不编写脚本的情况下处理 CSV 转 JSON 转换。
# 将 CSV 管道到 Node.js 内联脚本
cat servers.csv | node -e "
const lines = require('fs').readFileSync('/dev/stdin','utf8').trim().split('\n');
const h = lines[0].split(',');
const rows = lines.slice(1).map(l => Object.fromEntries(h.map((k,i) => [k.trim(), l.split(',')[i]?.trim()])));
console.log(JSON.stringify(rows, null, 2));
"# Miller 是结构化数据的瑞士军刀 # 安装:brew install miller (macOS) 或 apt install miller (Debian/Ubuntu) mlr --icsv --ojson cat inventory.csv # 转换时过滤行 mlr --icsv --ojson filter '$quantity > 100' inventory.csv # 选择特定列 mlr --icsv --ojson cut -f sku,warehouse,quantity inventory.csv
# 全局安装 npm install -g csvtojson # 转换文件 csvtojson servers.csv > servers.json # 从标准输入管道 cat exports/q1-metrics.csv | csvtojson > q1-metrics.json
对于大型文件,Miller 通常是比 csvtojson 更好的选择。Miller 用 C 实现,以流的方式处理 CSV 而无需将整个文件加载到内存中, 这意味着它能以恒定的内存使用量处理数 GB 的导出文件。它还支持就地字段级操作——重命名列、转换值类型、过滤行——在数据变为 JSON 之前完成, 从而避免两步解析-转换管道。另一方面,csvtojson 运行在 Node.js 中,当你的工具链其余部分是 JavaScript 时更为方便: 你可以将其输出直接管道到 Node 流、作为库导入,或使用其 colParser API 在代码中进行每列类型转换。需要原始吞吐量和 shell 管道时优先使用 Miller;需要与 Node.js 应用紧密集成时优先使用 csvtojson。
jq 不能原生解析 CSV。如果管道中需要 jq, 请先用 csvtojson 或 mlr 转换为 JSON, 然后将 JSON 输出管道到 jq 进行过滤和转换。高性能替代方案 — PapaParse
手动 split(',') 方法在真实世界的 CSV 文件上会失效。包含逗号的引号字段、嵌入的换行符、转义的双引号——这些都会破坏简单的拆分器。 当 CSV 来自未知来源时,PapaParse 是我首选的库。它处理所有 RFC 4180 边缘情况,自动检测分隔符,在 Node.js 和浏览器中都能工作。
npm install papaparse
import Papa from 'papaparse'
const csv = `product,description,price,in_stock
"小部件,大号","一个带有 ""额外"" 功能的优质部件",29.99,true
螺栓套件,标准 M8 螺栓套件,4.50,true
"密封圈套装","包含密封圈、封条和 O 形圈",12.75,false`
const { data, errors, meta } = Papa.parse(csv, {
header: true,
dynamicTyping: true, // 自动转换数字和布尔值
skipEmptyLines: true,
transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, '_'),
})
if (errors.length > 0) {
console.error('解析错误:', errors)
}
console.log(JSON.stringify(data, null, 2))
// [
// {
// "product": "小部件,大号",
// "description": "一个带有 \"额外\" 功能的优质部件",
// "price": 29.99,
// "in_stock": true
// },
// ...
// ]
console.log(`已解析 ${data.length} 行,分隔符:"${meta.delimiter}"`)PapaParse 的 dynamicTyping 选项自动进行类型转换——数字变为数字,"true"/"false" 变为布尔值。transformHeader 回调将列名规范化为下划线命名,避免处理来自不同 CSV 导出的不一致标题。如需快速转换而无需编写任何解析代码, CSV 转 JSON 工具 在浏览器中处理所有这些操作。
带语法高亮的终端输出
将大型 JSON 数组输出到终端会让人眼花缭乱。为输出添加语法高亮使其在调试和开发过程中可读性大大提升。cli-highlight 包可在 Node.js 终端中为 JSON 输出着色。
npm install cli-highlight
import { highlight } from 'cli-highlight'
// 将 CSV 转换为 JSON 数组后
const jsonOutput = JSON.stringify(rows, null, 2)
// 带语法高亮打印
console.log(highlight(jsonOutput, { language: 'json' }))
// 键、字符串、数字和布尔值各自获得不同颜色彩色输出在交互式检查大型转换结果时发挥重要作用。JSON 键、字符串值、数字和布尔值各自获得不同的 ANSI 颜色, 使得发现类型错误的字段变得容易——例如,一个应为 8080 数字但因类型转换未启用而被高亮为字符串的端口号。这在调试从电子表格工具导出的 CSV 文件时特别有用, 因为这些文件的列类型在行间往往不一致。没有颜色时,在 50 行 JSON 中扫描一个类型错误的字段意味着逐个读取每个值。 有了颜色,字符串颜色的数字立刻就能跳入眼帘。
处理大型 CSV 文件
用 readFileSync() 将 500 MB 的 CSV 文件加载为字符串会消耗大量内存并可能导致进程崩溃。对于大型文件, 应逐行流式处理 CSV,并在数据到达时发出 JSON 对象。 csv-parse 包(npm 上 csv 生态系统的一部分)提供了一个与 Node.js 流配合使用的流式解析器。
使用 csv-parse 将 CSV 流式传输为 NDJSON
NDJSON(换行符分隔 JSON)是一种输出文件中每行都是独立 JSON 对象的格式。 与需要将整个文件加载到内存才能开始读取的单个大型 JSON 数组不同——NDJSON 文件可以逐行处理。 这使得 NDJSON 非常适合将被日志处理器、流式管道或具有批量导入 API 的数据库消费的大型数据集。 csv-parse 包在对象模式下每个 CSV 行发出一个 JavaScript 对象,因此你可以将其直接管道到转换流, 该转换流在每个 JSON.stringify(row) 后追加 \n。
import { createReadStream, createWriteStream } from 'node:fs'
import { parse } from 'csv-parse'
import { Transform } from 'node:stream'
import { pipeline } from 'node:stream/promises'
// 将每个 CSV 行对象转换为 JSON 行
const toNdjson = new Transform({
objectMode: true,
transform(record, encoding, callback) {
callback(null, JSON.stringify(record) + '\n')
},
})
await pipeline(
createReadStream('telemetry-2026-03.csv'),
parse({
columns: true, // 使用第一行作为标题
skip_empty_lines: true,
trim: true,
cast: true, // 自动转换数字和布尔值
}),
toNdjson,
createWriteStream('telemetry-2026-03.ndjson')
)
console.log('流式转换完成')
// 输出文件中每行是一个 JSON 对象:
// {"timestamp":"2026-03-15T08:22:00Z","service":"gateway","latency_ms":42,"status":200}
// {"timestamp":"2026-03-15T08:22:01Z","service":"auth","latency_ms":156,"status":401}
// ...PapaParse 在浏览器和 Node.js 中的流式处理
PapaParse 的流式模式使用 step 回调,每行触发一次而不是将所有行收集到内存中。你传入一个 Node.js ReadStream(在 Node.js 中) 或 File 对象(在浏览器中),PapaParse 在内部处理分块。无需连接流管道——只需一个回调。 当你需要 RFC 4180 兼容性但不想引入 csv-parse 时使用它。
import Papa from 'papaparse'
import { createReadStream } from 'node:fs'
let rowCount = 0
const errors = []
const fileStream = createReadStream('warehouse-inventory.csv')
Papa.parse(fileStream, {
header: true,
dynamicTyping: true,
step(result) {
// 每次处理一行——内存占用恒定
rowCount++
if (result.data.quantity === 0) {
errors.push(`第 ${rowCount} 行:${result.data.sku} 缺货`)
}
},
complete() {
console.log(`已处理 ${rowCount} 行`)
if (errors.length > 0) {
console.log(`发现问题:${errors.length} 个`)
errors.forEach(e => console.log(` ${e}`))
}
},
error(err) {
console.error('解析失败:', err.message)
},
})常见错误
问题: CSV 不是 JSON。将原始 CSV 字符串传给 JSON.parse() 会抛出 SyntaxError,因为逗号和换行符不是有效的 JSON 语法。
修复: 先用 split() 或库将 CSV 解析为 JavaScript 对象,然后用 JSON.stringify() 生成 JSON。只对已经是有效 JSON 的字符串调用 JSON.parse()。
const csv = 'name,email\n陈明,chenming@nexuslabs.io' const data = JSON.parse(csv) // SyntaxError: Unexpected token '陈'
const csv = 'name,email\n陈明,chenming@nexuslabs.io'
const lines = csv.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map(line =>
Object.fromEntries(headers.map((h, i) => [h, line.split(',')[i]]))
)
const json = JSON.stringify(rows, null, 2) // 有效的 JSON 字符串问题: 对 JavaScript 对象调用 toString() 会返回毫无意义的字符串 '[object Object]',而不是实际数据。这会静默破坏你的 CSV 转 JSON 输出。
修复: 始终使用 JSON.stringify() 将 JavaScript 对象转换为 JSON 字符串。toString() 用于原始值到字符串的转换,而非序列化。
const row = { server: 'api-gateway', port: 8080 }
const output = row.toString()
// "[object Object]" — 数据丢失const row = { server: 'api-gateway', port: 8080 }
const output = JSON.stringify(row, null, 2)
// '{"server":"api-gateway","port":8080}'问题: 简单的 split(",") 在 CSV 值包含引号字段内的逗号时会出错:"小部件,大号" 会变成两个独立的值而不是一个。
修复: 对任何你不完全掌控的 CSV 数据使用 PapaParse 或 csv-parse。如果必须手动解析,请实现一个状态机解析器来跟踪当前位置是否在引号字段内。
const line = '"小部件,大号","优质品",29.99'
const values = line.split(',')
// ["\"小部件", " 大号\"", "\"优质品\"", "29.99"]
// 4 个值而不是 3 个——第一个字段被错误拆分import Papa from 'papaparse'
const { data } = Papa.parse('"小部件,大号","优质品",29.99')
// data[0] = ["小部件,大号", "优质品", "29.99"]
// 3 个值,正确解析问题: 未进行类型转换时,port: "8080" 在 JSON 中仍保留为字符串而不是数字。期望数字类型的下游系统会拒绝或错误处理数据。
修复: 在映射步骤中应用显式类型转换,或使用带 dynamicTyping: true 的 PapaParse。始终明确哪些字段应为数字类型。
const row = { port: '8443', debug: 'true', workers: '4' }
JSON.stringify(row)
// {"port":"8443","debug":"true","workers":"4"} — 全是字符串const row = {
port: Number('8443'), // 8443
debug: 'true' === 'true', // true
workers: Number('4'), // 4
}
JSON.stringify(row)
// {"port":8443,"debug":true,"workers":4} — 正确的类型手动解析与库的对比
对于你掌控 CSV 格式且确认没有引号字段的快速脚本,内置的 split() + JSON.stringify() 方案有效且零依赖。对于处理用户上传 CSV 文件的生产系统,在浏览器中使用 PapaParse, 在 Node.js 中使用 csv-parse ——两者都能正确处理 RFC 4180 并支持流式处理。 csvtojson 包是唯一直接输出 JSON 的库,通过单次调用同时处理解析和序列化。当你需要从粘贴到结果的最快路径时, CSV 转 JSON 工具在浏览器中无需任何设置即可完成所有操作。
常见问题
如何在不使用库的情况下用 JavaScript 将 CSV 转换为 JSON?
按换行符拆分 CSV 字符串以获取行,用 split(",") 从第一行提取标题,然后将剩余行映射为以标题为键的对象。最后调用 JSON.stringify(array, null, 2) 生成格式化的 JSON 字符串。这种方法适用于格式可控、不含引号字段或嵌入逗号的简单 CSV 文件。对于来自电子表格导出或第三方系统的数据,引号字段和多行值会破坏简单的拆分逻辑——此时应改用 PapaParse 或 csv-parse。对于在浏览器中处理的小型文件,这种零依赖方案是理想选择。
const csv = `name,email,department
陈明,chenming@nexuslabs.io,工程部
李芳,lifang@nexuslabs.io,产品部`
const lines = csv.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map(line => {
const values = line.split(',')
return Object.fromEntries(headers.map((h, i) => [h.trim(), values[i]?.trim()]))
})
console.log(JSON.stringify(rows, null, 2))
// [
// { "name": "陈明", "email": "chenming@nexuslabs.io", "department": "工程部" },
// { "name": "李芳", "email": "lifang@nexuslabs.io", "department": "产品部" }
// ]JSON.stringify() 和 toString() 对对象有什么区别?
对普通对象调用 toString() 会返回毫无意义的字符串 "[object Object]"——它不包含任何实际数据信息。JSON.stringify() 则会生成一个包含所有键值对的合法 JSON 字符串,包括嵌套对象和数组。当需要将 JavaScript 对象(例如 CSV 行数据)转换为 JSON 字符串时,始终使用 JSON.stringify()。要执行反向操作——从 JSON 字符串重建活跃的 JavaScript 对象——使用 JSON.parse(),它是 JSON.stringify() 的精确逆操作。这两个函数构成完整的往返流程:stringify 用于持久化或传输数据,parse 用于消费数据。
const row = { name: '陈明', role: '工程师' }
console.log(row.toString()) // "[object Object]"
console.log(JSON.stringify(row)) // '{"name":"陈明","role":"工程师"}'如何处理包含逗号或引号的 CSV 字段?
RFC 4180 规定,包含逗号、换行符或双引号的字段必须用双引号括起来,嵌入的引号通过重复来转义(引号字段内的 "" 表示单个字面引号)。手动使用 split(",") 在这类文件上会出错——字段边界不再是简单的逗号。对于生产数据,请使用 PapaParse 或 csv-parse,或者编写一个状态机解析器来跟踪当前位置是否在引号字段内。从头编写正确的状态机出乎意料地复杂:你必须处理字段开头的引号、字段中间的转义引号、引号字段内的换行符,以及不同的行尾约定(CRLF 与 LF)。对于任何超出简单 CSV 数据的场景,请使用经过充分测试的库。
// PapaParse 正确处理 RFC 4180
import Papa from 'papaparse'
const csv = `product,description,price
"小部件,大号","一个带有 ""额外"" 功能的优质部件",29.99
螺栓,标准螺栓,1.50`
const { data } = Papa.parse(csv, { header: true })
console.log(JSON.stringify(data, null, 2))
// description 字段正确包含:一个带有 "额外" 功能的优质部件可以直接在浏览器中将 CSV 转换为 JSON 吗?
可以。JSON.stringify() 和 JSON.parse() 都内置于每个浏览器引擎中。对于 CSV 解析步骤,简单文件可以按换行符和逗号拆分,或者通过 CDN 引入 PapaParse(它没有 Node.js 依赖)。整个转换过程在客户端完成,无需服务器往返,这对文件隐私非常有用——原始 CSV 数据永远不会离开用户的机器。当用户通过 <input type="file"> 元素上传 CSV 文件时,File API 的 file.text() 方法返回一个 Promise,解析后得到文件内容字符串,可直接传入转换函数。这使得纯浏览器端 CSV 转 JSON 转换在仪表盘、开发者工具以及任何需要在没有后端的情况下处理上传数据的应用中都切实可行。
// 浏览器:用户上传 CSV 文件
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0]
const text = await file.text()
const lines = text.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map(line =>
Object.fromEntries(headers.map((h, i) => [h.trim(), line.split(',')[i]?.trim()]))
)
console.log(JSON.stringify(rows, null, 2))
})如何从 CSV 中解析数字和布尔值,而不是将所有内容保留为字符串?
CSV 没有类型系统——每个字段都是字符串。你必须在映射步骤中对值进行类型转换。用 Number() 或 parseFloat() 检查数字模式,将 "true"/"false" 转换为布尔值,并将空字符串处理为 null。注意那些看起来像数字但必须保留为字符串的字段:邮政编码(如 "07302")在用 Number() 转换时会丢失前导零,含有数字字符的电话号码或产品编码同样脆弱。csvtojson 库通过 colParser 选项实现自动类型转换,允许你为每列指定转换函数并覆盖问题列的自动检测。PapaParse 的 dynamicTyping 选项则对所有列全局应用相同的转换。
function coerceValue(val) {
if (val === '') return null
if (val === 'true') return true
if (val === 'false') return false
const num = Number(val)
if (!isNaN(num) && val.trim() !== '') return num
return val
}
// 在 CSV 转对象映射时应用
const row = { port: coerceValue('8443'), debug: coerceValue('true'), host: coerceValue('api.internal') }
// { port: 8443, debug: true, host: "api.internal" }如何在 Node.js 中将 CSV 转 JSON 的输出写入文件?
使用 fs.writeFileSync() 配合 JSON.stringify() 的输出。JSON.stringify 的第三个参数控制缩进——传入 2 表示两个空格,传入 "\t" 表示制表符。要读回文件,使用 JSON.parse(fs.readFileSync(path, "utf8")),它会重建活跃的 JavaScript 对象数组。如果在异步上下文中(async 函数内部或 ES 模块顶层)写入文件,优先使用 fs.promises.writeFile() 以避免在文件写入完成时阻塞事件循环。对于超过几兆字节的大型 JSON 输出,考虑将 JSON 流管道到 WriteStream,而不是在写入前在内存中构建整个字符串。
import { writeFileSync, readFileSync } from 'node:fs'
// 写入
const jsonOutput = JSON.stringify(rows, null, 2)
writeFileSync('employees.json', jsonOutput, 'utf8')
// 读回
const parsed = JSON.parse(readFileSync('employees.json', 'utf8'))
console.log(Array.isArray(parsed)) // true
console.log(parsed[0].name) // "陈明"相关工具
Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.
Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.