数行数就会算错 — CSV 没那么简单
一句话总结
在 CSV 中,行(line)与记录(record)是两回事,一旦认为两者相同,行数和金额都会悄无声息地出错。
为什么需要它
打开客户给的销售文件,运行 wc -l,得到 431。可客户说是 402 条。为了找出这 29 条差异,半天就过去了。
差异通常来自两个原因。一个是引号内的换行:订单备注如果分了行,这条记录在文件里就占两行。另一个是引号内的分隔符:如果商号是“Moonshop, 股份有限公司”,按逗号切分这一行时,列就多出一个。金额列整体错位,这一行的金额要么从合计中漏掉,要么把一个不相干的值加了进去。
可怕的是不会报错。解析器一声不吭地给出数字,而这个数字看起来还很像样。这类错误会一直存活,直到客户说“我们的销售额比这个多”。
工作原理
RFC 4180 写下了 CSV 的通用规则,核心是四条。
- 记录之间用 CRLF 分隔。最后一条记录末尾的 CRLF 可有可无。
- 字段中含有分隔符、引号或换行时,该字段要用双引号包起来。
- 被包起来的字段中的双引号,要写成两个(
"")。 - 是否有表头行,通过
text/csv的header参数来告知:文件里并没有这个标记。
由此马上就会引出实际工作中遇到的三个问题。
第一,这份规范只是建议,而现实中并非人人遵守。RFC 4180 自己在开头也说明,CSV 长期以来没有正式规范,这份文档只是记录了广泛使用的形式。实际收到的文件,分隔符可能是分号或制表符,行尾可能是 LF,也可能在一个文件里混用,还可能没有表头。
第二,是否有表头,光看文件是判断不出来的。所以只能推测。第一行没有数字而后面的行有,是一把可用的尺子。不过它只是尺子,而不是保证:对于第一条数据行整行都是字符串的数据,它会出错。
第三,不能手工切分。line.split(",") 不认识引号。Python 的 csv 模块会处理引号内的分隔符和换行,以及写成两个的引号。而且打开文件时必须传入 newline="",这是文档明确要求的,漏掉它,引号内的换行就会把记录截断。
원본 한 레코드
A-1002,"달빛상사","고객 요청:
오전 배송",1,12000
split(",") 의 눈에는 csv 모듈의 눈에는
줄 2개 · 필드 3개와 3개 레코드 1개 · 필드 5개
该代码块中的韩文依次说明:原始的一条记录;用按逗号切分的眼光看,是 2 行,字段数分别为 3 个和 3 个;用 csv 模块的眼光看,是 1 条记录、5 个字段。
在现场相遇的样子
第一,没有闭合的引号。只要在某条记录里漏掉一个引号,从那个位置到文件末尾就成了一个字段。解析器不会报错,结果看起来像是“最后一条”。只要把文件扫一遍,看它是否以引号打开的状态结束,就能确切知道:数引号时,写成两个的("")要跳过。
第二,Excel 会改动内容。人如果用 Excel 打开再保存,分隔符会跟着区域设置变成分号,长数字会变成科学计数法,开头的 0 会消失。所以“原始文件”必须是人打开之前的那一份。
第三,把损坏的行直接丢掉。悄悄跳过列数对不上的行,销售额就会少掉对应的条数,之后没有依据可以解释。无法使用的行要分离到拒绝文件中,并同时写明原始行号。这样客户才能在自己的文件里打开那一行。
第四,是拒绝整个文件,还是只去掉那一行。像银行接收文件这种必须让对方的合计与我们的账本对上的地方,哪怕只有一行对不上,也要把整个文件退回去。反过来,客户给的分析用数据没有地方可退:能用的行照用,只把不能用的行分离出来。哪一种正确,由业务而不是数据决定。
实际工作中真正重要的事
- 同时报告行数和记录数。两者不同,这个差异本身就是信息。
- 不要手工切分。使用标准解析器,打开文件时传入
newline=""。 - 把分隔符、行尾和表头的判别结果记录下来。下一次交付时如果变了,这份记录就是依据。
- 不要丢弃,而要分离。留下拒绝文件、原因和原始行号。
下一项实验要做什么
亲手做出一个把同样的 20 条订单导出成七种形态的 feed,然后把守门程序 csvgate.py 一步步做大。先给出手工切分的解析器的结果,与 csv 模块的结果并排放在一起,用数字看它们相差多少。接着判别行尾、分隔符和是否有表头,把列数对不上的行、空行和没有闭合的引号分离到拒绝文件。评分器会在每次用不同的商号和金额生成自己的 feed,真实运行你的守门程序并核对结果。