D3 d3-dsv 深度指南:CSV/TSV 解析、格式化与 autoType 类型推断全解
本篇以 D3 官方文档中的 d3-dsv 模块说明 为主体,系统讲解 d3-dsv 的解析(parse / parseRows)、格式化(format / formatBody / formatRows 等)完整 API、autoType 类型推断的精确规则,以及内容安全策略(CSP)与 BOM 两个常见坑。读完后你将能够把任意图表化数据(CSV/TSV/管道分隔等)安全地解析为 JavaScript 对象、完成类型转换与再序列化,并理解 D3 主包中该模块的导出与版本边界。
d3-dsv 是什么:D3 数据入口的“前置模块”
d3-dsv 提供分隔符值(delimiter-separated values,DSV)格式的解析器与格式化器,最典型的是逗号分隔的 CSV 和制表符分隔的 TSV。这类表格格式在 Excel 等电子表格软件中非常普及,且通常比 JSON 更节省空间。该实现基于 RFC 4180 规范。
在 D3 主包(当前仓库,d3 v7.9.0)中,d3-dsv 是一个独立子模块,以依赖形式引入:
- package.json 声明
"d3-dsv": "^3.0.1",并要求node >= 12的运行环境; - src/index.js 第 10 行通过
export * from "d3-dsv";把该子模块的全部 API 提升到d3命名空间下,因此可以直接使用d3.csvParse、d3.csvFormat等全局形式; - test/d3-test.js 遍历
package.json中每个依赖模块并断言其所有导出(除version外)都存在于d3上,从测试层面保证了d3-dsv的 API 与主包一一对应。
注意:
d3-dsv的实现代码位于独立的 d3-dsv 子仓库中,本仓库作为伞形(umbrella)包通过依赖引用它;本文所有 API 语义均以本仓库 docs/d3-dsv.md 的文档描述为准。
最小示例:解析与格式化
最典型的用法——解析 CSV 字符串为对象数组:
d3.csvParse("foo,bar\n1,2") // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]
d3.tsvParse("foo\tbar\n1\t2") // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]
反向操作——把对象数组格式化为 DSV 字符串:
d3.csvFormat([{foo: "1", bar: "2"}]) // "foo,bar\n1,2"
d3.tsvFormat([{foo: "1", bar: "2"}]) // "foo\tbar\n1\t2"
返回值是一个“附加了 columns 属性的数组”:columns 记录列名及其在输入中的顺序(这一点比 Object.keys 更可靠,因为对象键的迭代顺序是任意的)。
dsvFormat(delimiter):构建自定义分隔符格式
除了内置的 csv*(逗号)与 tsv*(制表符)系列快捷方法,可以用 d3.dsvFormat 构建任意单字符分隔符的解析/格式化工具,例如管道符:
d3.dsvFormat("|").parse("foo|bar\n1|2") // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]
const csv = d3.dsvFormat(",");
约束条件:分隔符必须是单个字符(单个 16 位码元)。因此 ASCII 分隔符(,、|、;、\t 等)都没问题,但 emoji 等宽字符序列的分隔符不行。csvParse 等所有 csv* 快捷方法本质上都等价于 d3.dsvFormat(",").parse,tsv* 系列等价于 d3.dsvFormat("\t") 的对应方法。
dsv.parse(string, row):解析带表头的 DSV
*dsv*.parse 要求 DSV 内容的第一行是分隔符分隔的列名列表,这些列名会成为返回对象数组中每个对象的属性名。例如考虑如下 CSV:
Year,Make,Model,Length
1997,Ford,E350,2.34
2000,Mercury,Cougar,2.38
d3.csvParse("foo,bar\n1,2") // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]
解析后得到的 JavaScript 数组是:
[
{"Year": "1997", "Make": "Ford", "Model": "E350", "Length": "2.34"},
{"Year": "2000", "Make": "Mercury", "Model": "Cougar", "Length": "2.38"}
]
返回数组上的 columns 属性:
data.columns // ["Year", "Make", "Model", "Length"]
几个关键行为细节:
- 列名不唯一时,每个名字只保留最后一次出现的值。若需要访问全部值,请改用
*dsv*.parseRows。 - 默认不做类型转换:未指定
row转换函数时,所有字段值都是字符串。出于安全考虑,解析器不会自动把值转成数字、日期或其他类型。某些场景 JavaScript 会替你隐式转换(比如用+运算符),但更好的做法是显式指定row转换函数——d3.autoType是一个方便的、能推断并强制转换数字等常见类型的现成row函数(详见下文)。 row转换函数的调用签名为(d, i, columns):d是当前行的对象表示,i是从 0 开始的行索引(第一个非表头行为 0),columns是列名数组。若函数返回null或undefined,该行被跳过并从结果数组中省略;否则返回的值就是结果数组中对应的行对象。示例:
const data = d3.csvParse(string, (d) => {
return {
year: new Date(+d.Year, 0, 1), // 小写化并把 "Year" 转为 Date
make: d.Make, // 小写化
model: d.Model, // 小写化
length: +d.Length // 小写化并把 "Length" 转为数字
};
});
性能提示:用 + 或 Number 做字符串转数字通常比 parseInt / parseFloat 更快,但限制也更多——例如 "30px" 经 + 转换得到 NaN,而 parseInt/parseFloat 会得到 30。
CSP 注意:
*dsv*.parse因使用了(安全的)动态代码生成来加速解析,需要script-src指令中包含unsafe-eval的内容安全策略;受 CSP 限制的环境应改用*dsv*.parseRows(见“内容安全策略”一节)。
dsv.parseRows(string, row):解析无表头的 DSV
d3.csvParseRows("foo,bar\n1,2") // [["foo", "bar"], ["1", "2"]]
与 *dsv*.parse 不同,parseRows 把首行也当作普通数据行处理,适用于 DSV 内容不含表头的场景;每行以数组而非对象表示,且各行可以不等长。例如一个明显没有表头行的 CSV:
1997,Ford,E350,2.34
2000,Mercury,Cougar,2.38
[
["1997", "Ford", "E350", "2.34"],
["2000", "Mercury", "Cougar", "2.38"]
]
同样地,不指定 row 转换函数时字段值全部为字符串,不做自动类型转换。指定 row 函数时,其被传入的是数组形式的当前行(d),从 0 开始的索引(i),以及列名数组;返回 null/undefined 则跳过该行,否则返回值即结果行。示例:
const data = d3.csvParseRows(string, (d, i) => {
return {
year: new Date(+d[0], 0, 1), // 第一列转为 Date
make: d[1],
model: d[2],
length: +d[3] // 第四列转为数字
};
});
可以这样理解 row:它相当于对返回的每一行同时施加了 map(映射)和 filter(过滤)操作。
实战:解析本仓库的真实 CSV 数据
本仓库文档站自带一份真实 CSV 数据 docs/public/data/riia-us-revenue.csv,其内容为(节选):
format,group,year,revenue
8 - Track,Tape,1973-01-01,2815.68
8 - Track,Tape,1974-01-01,2848.01
8 - Track,Tape,1975-01-01,2770.41
结合上文知识,一次完整的“加载 + 解析 + 类型推断”可以写成:
// 浏览器中用 d3-fetch 的 d3.csv 获取(等价于先 d3.text 再 csvParse)
const data = await d3.csv("data/riia-us-revenue.csv", d3.autoType);
// data 中每行形如:
// {format: "8 - Track", group: "Tape", year: Date(1973-01-01), revenue: 2815.68}
其中 year 列(1973-01-01)会被 autoType 识别为 ECMAScript 日期字符串并推断为 Date(UTC 午夜),revenue 列(2815.68)会被推断为 number,而 format、group 保持字符串。在 Node 环境中读取本地文件后,可以直接把文件文本交给 d3.csvParse(text, d3.autoType),效果相同。
dsv.format(rows, columns):把对象数组序列化回 DSV
d3.csvFormat([{foo: "1", bar: "2"}]) // "foo,bar\n1,2"
d3.csvFormat([{foo: "1", bar: "2"}], ["foo"]) // "foo\n1"
这是 *dsv*.parse 的逆操作:把对象数组格式化为 DSV 字符串。行间以换行符(\n)分隔,行内各列以分隔符(如逗号 ,)分隔。包含分隔符、双引号(")或换行的值会自动用双引号转义。
columns 参数的行为:
- 未指定时,表头由 rows 中所有对象属性的并集决定,列顺序不确定;
- 指定时,
columns是字符串数组,列按给定顺序输出:
const string = d3.csvFormat(data, ["year", "make", "model", "length"]);
值处理规则:每行的所有字段都会被强制转为字符串;值为 null 或 undefined 时输出空字符串;值为 Date 时使用 ECMAScript 日期时间字符串格式(ISO 8601 的子集),例如 UTC 午夜日期会格式化为 YYYY-MM-DD。若需要更精细地控制“哪些字段、如何格式化”,先自行把 rows 映射成字符串的二维数组,再用 *dsv*.formatRows。
dsv.formatBody 与 dsv.formatRows:序列化变体
*dsv*.formatBody(*rows*, *columns*) 等价于 format,但省略表头行——在向已有文件追加数据时非常有用:
d3.csvFormatBody([{foo: "1", bar: "2"}]) // "1,2"
d3.csvFormatBody([{foo: "1", bar: "2"}], ["foo"]) // "1"
*dsv*.formatRows(*rows*) 把“字符串的二维数组”格式化为 DSV,是 *dsv*.parseRows 的逆操作;转义规则与 format 相同(含分隔符、双引号或换行的值用双引号包裹):
d3.csvFormatRows([["foo", "bar"], ["1", "2"]]) // "foo,bar\n1,2"
要把对象数组转成二维数组并显式指定列顺序,可用 Array.prototype.map:
const string = d3.csvFormatRows(data.map((d, i) => {
return [
d.year.getUTCFullYear(), // 假设 d.year 是 Date 对象
d.make,
d.model,
d.length
];
}));
还可以用 Array.prototype.concat 把列名数组拼在前面生成首行(表头):
const string = d3.csvFormatRows([[
"year",
"make",
"model",
"length"
]].concat(data.map((d, i) => {
return [
d.year.getUTCFullYear(), // 假设 d.year 是 Date 对象
d.make,
d.model,
d.length
];
})));
dsv.formatRow 与 dsv.formatValue:单行与单值格式化
*dsv*.formatRow(*row*) 把单个字符串数组格式化为 DSV 行:
d3.csvFormatRow(["foo", "bar"]) // "foo,bar"
*dsv*.formatValue(*value*) 格式化单个值,只处理转义:值中包含分隔符、双引号(")或换行时会用双引号转义:
d3.csvFormatValue("foo") // "foo"
autoType(object):确定性的类型推断
autoType 接收一个代表已解析行的对象(或数组),推断各值类型并强制转换,返回被修改后的原对象。它专门设计为与 *dsv*.parse / *dsv*.parseRows 配套的 row 访问器函数。仍以上文的汽车 CSV 为例,
d3.csvParse(string, d3.autoType)
得到的数组为:
[
{"Year": 1997, "Make": "Ford", "Model": "E350", "Length": 2.34},
{"Year": 2000, "Make": "Mercury", "Model": "Cougar", "Length": 2.38}
]
类型推断的精确规则如下:对对象中的每个 value,先计算其去除首尾空白(trim)后的值,然后按顺序重赋值:
- 若为空,则为
null; - 若恰为
"true",则为true; - 若恰为
"false",则为false; - 若恰为
"NaN",则为NaN; - 否则,若可强制转换为数字(按 ECMAScript ToNumber 对字符串类型的规则),则为数字;
- 否则,若是 ECMAScript 定义的“仅日期”或“日期时间”字符串,则为
Date; - 否则,保持为字符串(注意:保留的是未修剪的原始值)。
两个容易踩坑的边界:
- 前导零可能被推断为数字,例如
"08904"会变成8904;但逗号、货币单位等额外字符(如"$1.00"、"(123)"、"1,234"、"32px")会阻止数字推断,结果保持字符串; - 日期字符串必须是 ECMAScript 认可的 ISO 8601 子集。
YYYY-MM-DD这类“仅日期”字符串被推断的时间是 UTC 午夜;而YYYY-MM-DDTHH:MM这类不带时区的“日期时间”字符串则被当作本地时间。
自动类型推断的主要目的是为常见 JavaScript 类型提供与 *dsv*.format / *dsv*.formatRows 配合时的安全、可预期行为。如果你需要不同的行为,应当自己实现 row 访问器函数。
在浏览器中加载 DSV 文件:与 d3-fetch 的衔接
d3-dsv 本身只处理字符串;若要从 URL 直接加载 DSV 文件,应搭配 d3-fetch 模块的便捷方法(见 d3-fetch 文档):
d3.csv(input, init, row):以逗号为分隔符,等价于d3.dsv的快捷形式;d3.tsv(input, init, row):以制表符为分隔符;d3.dsv(delimiter, input, init, row):任意单字符分隔符,init 会透传给底层fetch(遵循 RequestInit),row 转换函数语义与*dsv*.parse完全一致。
const data = await d3.dsv(",", "example.csv", (d) => {
return {
year: new Date(+d.Year, 0, 1), // 把 "Year" 列转为 Date
make: d.Make,
model: d.Model,
length: +d.Length // 把 "Length" 列转为数字
};
});
若 init 与 row 只给了一个,只要它是函数就被解释为 row 转换函数,否则解释为 init 对象。
内容安全策略(CSP)与 BOM:两个必须知道的坑
CSP 与 unsafe-eval:如果部署环境启用了内容安全策略,*dsv*.parse 要求在 script-src 指令中包含 unsafe-eval——这是因其内部使用了(安全的)动态代码生成以加速解析。受严格 CSP 约束而无法开放 unsafe-eval 时,替代方案是改用 *dsv*.parseRows。
字节序标记(BOM):DSV 文件有时会带 BOM 开头——例如用 Microsoft Excel 以“CSV UTF-8”格式保存就会写入 BOM。在 Web 端通常不成问题,因为 Encoding 标准规定的 UTF-8 解码算法会自动去除 BOM;而 Node.js 解码 UTF-8 时不会去除 BOM。若 BOM 未被去除,文本首字符实际上是一个零宽不换行空格(zero-width non-breaking space):用 d3.csvParse 解析带 BOM 的 CSV 时,第一列的列名就会以这个不可见字符开头,打印时难以察觉,排障时极易被忽略。在 Node 端解析前,可考虑先用 strip-bom 之类的工具包去除 BOM。
API 速查表
除 dsvFormat 与 autoType 外,所有 csv* / tsv* 方法都只是基于相应分隔符的 dsvFormat 快捷封装:
| CSV(逗号分隔) | TSV(制表符分隔) | 等价定义 |
|---|---|---|
csvParse(string, row) |
tsvParse(string, row) |
d3.dsvFormat(",").parse / d3.dsvFormat("\t").parse |
csvParseRows(string, row) |
tsvParseRows(string, row) |
对应的 parseRows |
csvFormat(rows, columns) |
tsvFormat(rows, columns) |
对应的 format |
csvFormatBody(rows, columns) |
tsvFormatBody(rows, columns) |
对应的 formatBody(无表头) |
csvFormatRows(rows) |
tsvFormatRows(rows) |
对应的 formatRows |
csvFormatRow(row) |
tsvFormatRow(row) |
对应的 formatRow |
csvFormatValue(value) |
tsvFormatValue(value) |
对应的 formatValue |
小结:解析—转换—序列化的完整心智模型
把 d3-dsv 当作 D3 数据管线的第一环来使用:字符串进,对象出。csvParse/csvParseRows 负责“进”,autoType 或自定义 row 函数负责确定性的类型转换(默认全字符串、绝不隐式转换是其安全设计的核心),csvFormat/csvFormatBody/csvFormatRows 负责“出”(含分隔符、引号、换行的自动转义与 Date 的 ISO 8601 输出)。在 D3 主包 v7(本仓库 d3@7.9.0,依赖 d3-dsv@^3.0.1)中,这些 API 均可直接以 d3. 前缀调用,且由 test/d3-test.js 的导出完整性测试保障;在浏览器中取数据则通过 docs/d3-fetch.md 描述的 d3.csv / d3.tsv / d3.dsv 完成,两者拼合即是完整的 DSV 数据加载链路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00