首页
/ D3 d3-dsv 深度指南:CSV/TSV 解析、格式化与 autoType 类型推断全解

D3 d3-dsv 深度指南:CSV/TSV 解析、格式化与 autoType 类型推断全解

2026-09-04 20:54:46作者:宣聪麟

本篇以 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.csvParsed3.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(",").parsetsv* 系列等价于 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"]

几个关键行为细节:

  1. 列名不唯一时,每个名字只保留最后一次出现的值。若需要访问全部值,请改用 *dsv*.parseRows
  2. 默认不做类型转换:未指定 row 转换函数时,所有字段值都是字符串。出于安全考虑,解析器不会自动把值转成数字、日期或其他类型。某些场景 JavaScript 会替你隐式转换(比如用 + 运算符),但更好的做法是显式指定 row 转换函数——d3.autoType 是一个方便的、能推断并强制转换数字等常见类型的现成 row 函数(详见下文)。
  3. row 转换函数的调用签名为 (d, i, columns)d 是当前行的对象表示,i 是从 0 开始的行索引(第一个非表头行为 0),columns 是列名数组。若函数返回 nullundefined,该行被跳过并从结果数组中省略;否则返回的值就是结果数组中对应的行对象。示例:
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,而 formatgroup 保持字符串。在 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"]);

值处理规则:每行的所有字段都会被强制转为字符串;值为 nullundefined 时输出空字符串;值为 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)后的值,然后按顺序重赋值:

  1. 若为空,则为 null
  2. 若恰为 "true",则为 true
  3. 若恰为 "false",则为 false
  4. 若恰为 "NaN",则为 NaN
  5. 否则,若可强制转换为数字(按 ECMAScript ToNumber 对字符串类型的规则),则为数字;
  6. 否则,若是 ECMAScript 定义的“仅日期”或“日期时间”字符串,则为 Date
  7. 否则,保持为字符串(注意:保留的是未修剪的原始值)。

两个容易踩坑的边界:

  • 前导零可能被推断为数字,例如 "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" 列转为数字
  };
});

initrow 只给了一个,只要它是函数就被解释为 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 速查表

dsvFormatautoType 外,所有 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 数据加载链路。

登录后查看全文
热门项目推荐
相关项目推荐