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

处理客户数据

行数对不上 — 数的是记录,不是行

在 TT Lab 中继续学习

目标

构建守门程序 csvgate.py,按记录而不是按行来统计客户的 CSV。用自己的数据量出手工切分的解析器与标准解析器的结果相差多少,判别分隔符、行尾和是否有表头,并把无法使用的行分离到拒绝文件中。

为什么重要

在 CSV 中,行与记录是两回事。订单备注中如果有换行,一条记录在文件里就占两行;商号中如果有逗号,按逗号切分这一行时,列就会增多。这两种情况下,解析器都不会报错,而是给出看起来很像样的数字。所以这种错误会一直存活,直到客户看着数字说不对劲。 RFC 4180 规定了引号规则和行尾,但收到的并不都是完全遵守规范的文件。分隔符可能是分号或制表符,行尾可能在一个文件里混用,还可能没有表头。是否有表头并没有写在文件里,所以必须自己判别。 接下来还有一个问题:如何处理无法使用的行。悄悄跳过,销售额就会少掉对应的条数,之后没有依据可以解释。如果是银行接收文件,哪怕只有一行对不上,也要把整个文件退回去;而客户给的分析用数据没有地方可退:能用的行照用,只把不能用的行分离出来。 评分器不会相信你的文字。它在临时目录里摆好评分器生成的 feed,真实运行你的守门程序,并把记录数、金额合计和拒绝原因与它自己数出来的值进行核对。商号和金额每次运行都会变。

步骤

  1. 创建并运行 /root/csv/gen_feed.py,在 /root/csv/feed 下生成七个文件。同样的 20 条订单会以七种形态出现。
  2. 在 /root/csv/csvgate.py 中实现 naive,让它给出“把一行当作一条记录”的解析器的结果(rows、amount_total、bad_field_count)。
  3. 增加 parse,让它给出用 csv 模块正确统计的结果(records、amount_total),并把两个结果的差异写入 /root/csv/gap.json。
  4. 增加 sniff,让它把行尾判别为 CRLF、LF 或 MIXED。
  5. 让 sniff 从候选(逗号、分号、制表符、竖线)中判别分隔符。
  6. 让 sniff 判别是否有表头(header)和列数(fields)。
  7. 增加 check,让它按原因统计无法使用的行,并分离到拒绝文件中。拒绝文件以相同的名称写到输入文件所在目录下的 rejects/ 中。
  8. 一次性处理七个 feed 文件,生成 /root/csv/feed_report.json 和 /root/csv/feed_report.md。

参考

生成七种形态的同一份 feed

创建并运行 /root/csv/gen_feed.py,在 /root/csv/feed 下生成七个文件。同样的 20 条订单,只在分隔符、行尾、表头和是否损坏上有所不同。

用 Python csv 模块的 writer 来写,引号会自动处理。改变 lineterminator 来选择行尾,改变 delimiter 来选择分隔符。故意弄坏的文件(列数不同的行、没有闭合的引号)不要用 writer,而是直接用字符串写。

给出手工切分的解析器的结果

在 /root/csv/csvgate.py 中实现 naive <파일>(占位符为文件),让它把“一行就是一条记录”的解析器的结果以 JSON 输出。包含 rows、amount_total、bad_field_count 三个值。

这一步是故意做出一个错误的解析器:把文件整个读入,按换行切分,跳过第一行和空行,再把每一行按逗号切分。列数不是 5 的,不要累加金额,只计入 bad_field_count。之后要与正确统计的结果并排放在一起,所以规则要严格遵守。

正确读取引号内的逗号和换行

增加 parse <파일>(占位符为文件),让它给出用 csv 模块统计的结果(records、amount_total),并把针对 orders_rfc.csv 的两个结果的差异,以 file、naive_rows、csv_records、naive_amount、csv_amount 的形式写入 /root/csv/gap.json。

关键在于打开文件时传入 newline=""。csv.reader 会处理引号内的分隔符和换行,以及写成两个的引号。去掉表头行之后剩下的就是记录,金额只在列数正确的记录中累加。

找出行尾混用的文件

增加 sniff <파일>(占位符为文件),让它判别行尾。只有 CRLF 时为 CRLF,只有 LF 时为 LF,两者混用时为 MIXED。响应中必须包含 path、delimiter、newline。

行尾要按字节来数。以二进制方式读取文件,数出 CRLF 的个数,再用 LF 的总数减去它,就得到单独出现的 LF 的个数。两者都不是 0,就是混用。这一步中分隔符可以先设为逗号。

从候选中选出分隔符

让 sniff 从逗号、分号、制表符、竖线四个候选中判别分隔符。对每个候选把整个文件解析一遍,选择列数最一致且最宽的那个。

只看第一行来数,会被引号内的分隔符骗到。对每个候选用 csv.reader 整个解析,求出列数的众数,以及具有该众数的行所占的比例;只有一列的候选,得分设为 0。即使各行列数不同的文件,这个办法也撑得住。

判别是否有表头

给 sniff 的响应增加 fields(列数)和 header(是否有表头)。如果第一行没有任何一列能读成数字,而紧随其后的三行中有,就视为表头。

是否有表头并没有写在文件里。RFC 4180 也只是说要通过媒体类型的参数来告知。所以这不是判别而是推测,而推测需要一把尺子。把这把尺子写进代码,下次交付时如果出错,就能看到该改什么。

只分离无法使用的行

增加 check <파일>(占位符为文件),让它逐条记录检查,按原因统计空行、列数不一致和没有闭合的引号,并分离到拒绝文件中。拒绝文件以相同的名称写到输入文件所在目录的 rejects/ 中。给 sniff 的响应增加 unterminated。

没有闭合的引号不会报错。从那个位置到文件末尾成为一个字段,看起来只是“最后一条”。把文件扫一遍,看它是否以引号打开的状态结束,就能确切知道:写成两个的引号要跳过。被拒绝的行要同时写明它原本是第几条记录。

用一份 feed 报告

一次性处理七个 feed 文件,在 /root/csv/feed_report.json 中写入 files、accepted、rejected、amount_total,并在 /root/csv/feed_report.md 中分 ## 무엇을 받았나 ## 줄과 레코드는 다르다 ## 떼어 낸 줄 ## 보내는 쪽에 요청할 것 四节书写(韩文标题,依次意为“收到了什么”“行与记录不同”“分离出的行”“需要向发送方请求的内容”)。

files 是以文件名为键的对象,其中包含 delimiter、newline、header、records、accepted、rejected、amount_total。报告里要用数字写明分离出的行数:客户必须能在自己的文件里打开那一行,沟通才会更快。调用前面步骤中做好的函数即可。