跳到主要内容
Tooletto

为什么电子表格导出的 CSV 一转换就出问题

CSV 导出文件看起来是简单、结构一目了然的数据。可一旦出现真实的地址、前导零,或者单元格里嵌入的换行符,这种"简单"就露出了它的假象。

· 阅读需 1 分钟

为什么 CSV 看起来很简单,实际上并不简单

CSV 文件看起来大概是数据格式里能有的最简单的一种了——按逗号拆分每一行,把第一行当作列标题,完事——而正是这种表面上的简单,让这个格式在真实世界的数据出现时特别容易出问题。这种天真的"按逗号拆分"方法在一个玩具级示例上运行得完美无缺,却会在第一行真实数据值恰好包含自己的逗号时立刻崩溃,而这根本不是什么边缘情况,而是家常便饭:像"123 Main St, Apt 4"这样的地址、像"Smith, Jones & Co"这样的公司名,或者一段恰好包含一个列表的产品描述,都是完全普通的取值,却恰好合法地包含了那个被简单解析器当作字段分隔符使用的字符。

CSV 实际上是怎么处理值内逗号的——以及简陋工具是怎么弄错的

CSV 规范通过允许字段用双引号包裹来解决"值内逗号"的问题,在双引号内部,逗号甚至换行符都只是字面数据而不是结构——一个正确加了引号的字段,比如 `"Smith, Jones & Co"`,会被理解为一个包含逗号的单一值,而不是在这个逗号处被拆成两个值。一个天真地在每个逗号处拆分、而不跟踪当前是否处于引号字段内部的解析器,完全没有办法做出这种区分,会悄悄地把那一个公司名拆成两个虚假的列——通常不会报出任何错误,这正是让这一类问题格外危险的原因:转换看起来是成功的,损坏只会在之后才被发现,而且往往是在这份已经损坏的数据已经被拿去用于某件事之后才被发现。

单元格内嵌入的换行符

一个电子表格单元格里包含多行备注或多行地址是完全正常的,当这个单元格被导出为 CSV 时,规范处理它的方式和处理内嵌逗号一样:整个字段会被引号包裹起来,字段内部的换行符是这个字段数据真正的一部分,而不是新一行开始的信号。一个假定文件中每一个换行符都标志着新记录开始、而不跟踪当前是否处于一个尚未闭合的引号字段内部的解析器,会把那一个多行单元格拆成看起来是好几个独立的行——悄悄地破坏了文件中它之后所有内容的行结构,因为后续的每一行现在都因为那个损坏的单元格引入了多少虚假的额外行而发生了错位。

类型歧义:前导零、电话号码,以及看起来是数字但其实不是的数字

在原始 CSV 文件中,每一个值都是以纯文本形式存储的,没有内置的方式来表明某个给定的值"真的"是数字、布尔值,还是恰好看起来像数字的字符串——这在转换工具试图"贴心地"自动推断类型的那一刻,就变成了一个真正的问题。像"007"这样的邮政编码,或者像"042"这样的 ID,其前导零是有语义意义的——它是一个固定宽度的标识符,"7"和"007"即使代表同一个数字,也是不同的标识符——但一个急于把看起来像数字的字符串解析成真正数字的转换工具,会悄悄丢掉这个前导零,因为数字 7 根本没有"之前曾经有多少个零"这个概念。像"+1 555 0100"这样的电话号码看起来部分像数字,但根本不是用来做算术的数据,把它转换成数字会产生真正毫无意义的结果。安全、正确的默认做法,是除非某个值明确且安全地是数字,否则一律把有歧义的值保留为文本,因为在这里猜错了,会悄悄损坏数据,而不是抛出一个可见、可被发现的错误。

为什么 Excel 特别会带来另一层完全不相关的麻烦

除了 CSV 格式本身真实存在的边缘情况之外,Microsoft Excel 还带来了与 CSV 规范本身无关的额外阻碍:许多欧洲及其他地区的 Excel 配置默认使用分号而不是逗号作为字段分隔符,因为在这些地区的设置里,逗号已经被用作小数点分隔符了,这意味着从这类 Excel 配置导出的 CSV 文件,用逗号分隔的解析器根本无法正确解析。Excel 还有一个众所周知的习惯,就是在打开一个 CSV 文件查看时,会自动重新格式化它"认为"自己认识的值——比如把一个很长的数字 ID 转换成科学计数法——这是 Excel 在显示时的一种误判,而不是底层 CSV 文件本身的缺陷,但它经常被误以为是导出过程中实际发生的数据损坏。

相关工具

博客中的更多文章