TT Lab
开始
学习 学习路径 课程

处理客户数据

数行数就会算错 — CSV 没那么简单

在 TT Lab 中继续学习

一句话总结

在 CSV 中,行(line)与记录(record)是两回事,一旦认为两者相同,行数和金额都会悄无声息地出错。

为什么需要它

打开客户给的销售文件,运行 wc -l,得到 431。可客户说是 402 条。为了找出这 29 条差异,半天就过去了。

差异通常来自两个原因。一个是引号内的换行:订单备注如果分了行,这条记录在文件里就占两行。另一个是引号内的分隔符:如果商号是“Moonshop, 股份有限公司”,按逗号切分这一行时,列就多出一个。金额列整体错位,这一行的金额要么从合计中漏掉,要么把一个不相干的值加了进去。

可怕的是不会报错。解析器一声不吭地给出数字,而这个数字看起来还很像样。这类错误会一直存活,直到客户说“我们的销售额比这个多”。

工作原理

RFC 4180 写下了 CSV 的通用规则,核心是四条。

由此马上就会引出实际工作中遇到的三个问题。

第一,这份规范只是建议,而现实中并非人人遵守。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 会消失。所以“原始文件”必须是人打开之前的那一份。

第三,把损坏的行直接丢掉。悄悄跳过列数对不上的行,销售额就会少掉对应的条数,之后没有依据可以解释。无法使用的行要分离到拒绝文件中,并同时写明原始行号。这样客户才能在自己的文件里打开那一行。

第四,是拒绝整个文件,还是只去掉那一行。像银行接收文件这种必须让对方的合计与我们的账本对上的地方,哪怕只有一行对不上,也要把整个文件退回去。反过来,客户给的分析用数据没有地方可退:能用的行照用,只把不能用的行分离出来。哪一种正确,由业务而不是数据决定。

实际工作中真正重要的事

下一项实验要做什么

亲手做出一个把同样的 20 条订单导出成七种形态的 feed,然后把守门程序 csvgate.py 一步步做大。先给出手工切分的解析器的结果,与 csv 模块的结果并排放在一起,用数字看它们相差多少。接着判别行尾、分隔符和是否有表头,把列数对不上的行、空行和没有闭合的引号分离到拒绝文件。评分器会在每次用不同的商号和金额生成自己的 feed,真实运行你的守门程序并核对结果。