一、先说这份 CSV表格 是什么
我自己在做一个浏览器扩展,叫多多开票助手,用来处理拼多多买家侧的订单和发票,订单导出是其中一块功能。这篇文章讲实现过程中绕不过去的几件事:CSV 这个格式本身有什么先天缺陷,36 个字段为什么这么分,以及一份语法完全正确的 CSV,为什么用 Excel 打开之后会变成错的。
字段清单和踩坑记录来自实际代码,不是抄的文档。
二、CSV 没有类型,也没有编码声明
CSV(Comma-Separated Values)是一种用纯文本存表格数据的格式,字段之间用分隔符隔开,一行一条记录。规范里没有位置可以声明数据类型,也没有位置声明字符编码。这两件事直接导致了后面所有的坑。
**所有值都是字符串。**一个 18 位的订单号、一句话、一个日期,在这份文件里地位一样,都是一串字符。至于"这是数字""这是日期",全靠打开它的软件自己猜。不同软件猜法不同,这就是问题的源头。
**文件不知道自己是什么编码。**Windows 上的 Excel 双击打开 CSV 时,会按系统 ANSI 编码解码,简体中文环境下就是 GBK。文件实际是 UTF-8 写的,中文就全乱。处理办法是在文件最前面放三个字节 EF BB BF,也就是 UTF-8 BOM,Excel 看到这个标记就按 UTF-8 处理:
const BOM = '\ufeff';
const csv = BOM + rows.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
BOM 不是 CSV 标准的一部分,是 Windows 系软件的识别习惯。代价是文件开头多了三个不可见的字节,如果用脚本去解析这份 CSV,第一个表头会带上一个零宽字符,需要自己剥掉。我现在的做法是导出时一律带 BOM,因为双击打开的人远多于写脚本解析的人。
三、36 个字段,按什么分五组
字段不是拍脑袋凑的,是按"拿到这张表的人要拿它干什么"归的类。五组分别是订单主干、商品与店铺、收货信息、物流轨迹、退款与备注。
第一组:订单主干(1 至 11 列)
| 序号 | 字段名 | 说明 |
|---|---|---|
| 1 | 拼多多账号 | 当前绑定的买家账号昵称,多账号场景用来区分来源 |
| 2 | 商品主图 | 图片地址,表格里基本不看 |
| 3 | 订单号 | 唯一标识,和商家、客服、财务对接都靠它 |
| 4 | 下单时间 | 排序、按月切分用 |
| 5 | 订单金额 | 实付金额,对账主字段 |
| 6 | 支付方式 | 支付渠道 |
| 7 | 支付时间 | 和下单时间分开,跨天支付能看出来 |
| 8 | 订单状态 | 待发货、已发货、已签收这类 |
| 9 | 发票状态 | 有没有申请过、开没开出来 |
| 10 | 发票下载状态 | 电子发票有没有下载过 |
| 11 | 发票下载时间 | 什么时候下载的 |
第 10、11 列要单独说明一下:这两列不是平台返回的字段,是扩展在本机记录的下载历史。你换了电脑或者清了浏览器数据,这两列就是空的,而平台上的发票其实早开好了。导出表跟平台对不上时,先想到这个原因。
第二组:商品与店铺(12 至 21 列)
| 序号 | 字段名 | 说明 |
|---|---|---|
| 12 | 商品名称 | 采购明细里的品名 |
| 13 | 商品链接 | 按商品 ID 拼出来的,商品下架后可能失效 |
| 14 | 商品ID | 比名称稳的标识,同名商品靠它区分 |
| 15 | 规格 | 颜色尺码这类 |
| 16 | 规格ID | 规格的唯一标识 |
| 17 | 数量 | 采购件数 |
| 18 | 单价 | 金额除以数量算出来的,不是平台给的字段 |
| 19 | 拼单链接 | 带规格和数量的下单直达地址 |
| 20 | 店铺名称 | 按店铺归类的主字段 |
| 21 | 店铺链接 | 按店铺路径拼出来的地址 |
第 13、19、21 列都是拼出来的,不是平台原样返回的。所以它们的格式是扩展定死的,页面改版后可能失效。真正需要长期留存的是商品 ID 和店铺名称。
第三组:收货信息(22 至 25 列)
| 序号 | 字段名 | 说明 |
|---|---|---|
| 22 | 收件人 | 缺值时填「未知」 |
| 23 | 收货电话 | 同上 |
| 24 | 收货地址 | 多仓或多收货点时用来区分货发到哪 |
| 25 | 上游系统识别码 | 目前是空的,留给对接内部 ERP 用 |
第 25 列现在恒为空值。它是预留位,想着以后订单能带上内部系统编号的话就填这儿。至今没用上,但表头留着,免得以后加列的时候下游对接的表全要跟着改。这种预留列在实际业务里很常见,导出表拿到手发现有列是空的,不一定就是坏了。
第四组:物流轨迹(26 至 32 列)
| 序号 | 字段名 | 说明 |
|---|---|---|
| 26 | 物流公司 | 快递方 |
| 27 | 物流单号 | 查物流、跟售后对接 |
| 28 | 发货时间 | 做供应商时效评估用 |
| 29 | 签收时间 | 账期起算点,也用来判断补开票的窗口 |
| 30 | 取件码 | 驿站取件用 |
| 31 | 最后轨迹 | 最新一条物流状态文本 |
| 32 | 最后轨迹时间 | 那条状态的时间 |
第 31、32 列有优先级:先取平台订单接口返回的最新物流状态,取不到才退回轨迹列表的最后一条。所以这两列可能有值,而同一行的详细轨迹是空的。
第五组:退款与备注(33 至 36 列)
| 序号 | 字段名 | 说明 |
|---|---|---|
| 33 | 退款原因 | 有退款才有值 |
| 34 | 申请金额 | 售后申请的金额 |
| 35 | 申请时间 | 售后发起的时间 |
| 36 | 订单备注 | 下单时自己填的 |
36 列里日常真正会看的大概十来列,剩下的是备用。备用字段的价值在于事后追查:等你要做供应商评估或者退货分析的时候,重新导一遍历史订单往往已经拿不到当时的状态了。
四、坑一:订单号被 Excel 改成了科学计数法
订单号十八位左右,全是数字。Excel 把所有数字按 IEEE 754 双精度浮点处理,有效精度 15 位。所以后三位会被舍入成 0,单元格显示成 1.23457E+18 这种样子。
这个错误不可逆。就算你事后把单元格格式改成文本,丢掉的那几位也回不来,因为存进去的时候就已经是另一个数了。重新导一份是唯一的办法。
两条路可以走:一是用「数据」菜单里的「从文本/CSV」导入,在向导里把订单号那一列手动指定成文本类型;二是在导出的时候就把它写成文本形式,也就是 ="123456789012345678" 这种写法。
我选了第二条,理由是双击打开 CSV 的人占绝大多数,指望每个人每次走导入向导不现实。代价也很明确:="..." 是 Excel 的公式语法,不是通用写法。WPS 表格和 Google Sheets 对它支持不一致,用 pandas 读的时候拿到的是字面量 ="123456789012345678",得自己剥掉外层。
// 纯数字且长度 >= 10 的字段,按 Excel 文本写入
if (/^\d+$/.test(s) && s.length >= 10) return `="${s}"`;
如果你的下游流程是脚本解析而不是人眼看,那这个处理反而给你添了麻烦,可以在导出后统一处理一遍。
五、坑二:字段里的逗号和换行
CSV 用逗号分隔字段。商品名本身带逗号的话,比如「收纳盒,中号,两只装」,一个名称就占掉了三个字段的位置,后面所有列整体右移,整行错位。
RFC 4180 规定的标准做法是用双引号把字段包起来,字段内部的双引号写成两个。我的实现在这个基础上多做了一步:把字段内部的半角逗号替换成全角逗号,再整体包引号。
const escaped = s.replace(/"/g, '""').replace(/,/g, ',');
return `"${escaped}"`;
这么做的问题是它改了原始数据。导出表里的商品名和平台上显示的不是同一个字符串,你在 Excel 里按商品名做匹配的时候,得按全角逗号写才匹配得上。现在回头看,这一步是多余且有害的,只用引号包裹就够了,字符替换完全可以不做。当时加它是因为遇到过某个表格软件对引号内逗号处理不好,用替换绕过去了,后来发现那是旧版本的问题。
引号包裹还解决另一个情况:字段内部有换行。物流详细轨迹是多行文本,包在引号里就是合法的单字段,不包的换行会被当成记录结束。
六、坑三:行尾用了 LF 而不是 CRLF
RFC 4180 规定行尾是 \r\n,我的实现是 rows.join('\n'),只有 LF。
主流的表格软件和编辑器都能容忍,Excel、WPS、LibreOffice 打开都正常。但极老的 Windows 工具会把整份文件当成一行显示,部分工业软件和老的 ERP 导入模块也会挑这个。如果导出文件要喂给这类系统,得在生成的时候换成 CRLF。
这是我知道但没有改的地方,因为改了对现有使用者没有任何收益,反而多一次回归测试。写出来是给有对接需求的人提个醒。
七、另外一件事:导出的是快照
导出的表是导出那一刻的数据,之后订单状态变化不会同步进去。刚发的货、刚退的款,这份文件里都还是旧值。
所以别把导出文件当数据源反复用。要做长期归档的话,我的习惯是每月导一次,文件名带月份,原始 CSV 留一份不删。等发现哪一列有问题,还能回去查当时到底是什么值。
八、这份表能拿来做什么
三个我自己在用的。
**给财务的对账表。**财务要的核心是三样:订单号、金额、发票状态。按发票状态筛一遍,已开的和没开的分成两块,没开的那块就是要催的清单。
**供应商发货时效。**用发货时间减去下单时间得到每笔的发货时长,再按店铺名称求平均。这个数据不精确,因为发货时间取的是平台记录的时间点,但横向比较不同供应商的相对快慢是够用的。
**退货集中度。**按退款原因分组,看退货是集中在某个商品还是某个规格上。一年看一次就够了。
九、常见问题
导出的文件和平台上的数据会不一致吗?
会。导出的是导出那一刻的快照,之后的状态变化不会同步。要最新的就重新导一份。另外「发票下载状态」和「发票下载时间」是本机记录,换电脑后会丢。
订单号已经变成科学计数法了,能恢复吗?
不能。Excel 存进去的时候就已经是另一个数了,改格式也找不回来。只能重新导,并且这次用导入向导指定文本,或者改用带 ="..." 标记的写法。
能直接导出成 xlsx 吗?
目前导出的是 CSV,用 Excel 打开另存一次就成 xlsx 了。直接生成 xlsx 需要在扩展里打包一个表格库,体积会涨不少,为了省一次另存不划算。
商品名里的逗号为什么变成全角了?
是导出时的替换处理。见第五节,现在看这一步做得不必要,但已经这样发了,改的话会让老用户的匹配规则失效,所以留着。
这个字段清单会变吗?
会。平台加字段或改字段含义的时候,导出的表会跟着变。以你实际导出文件里的表头为准。
十、小结
订单导出这件事,动作本身没什么可讲的,难点全在数据离开页面之后:编码靠约定、类型靠猜、精度靠软件的习惯、字段含义靠上游接口给什么。CSV 是个四十多年前的格式,它的简单既让它到处都能打开,也让它什么都不能保证。
我做的这个扩展叫多多开票助手,订单同步、发票申请、发票下载和导出是一套流程,导出只是其中一块。如果只需要导出这一个功能,也可以自己用脚本读接口数据生成 CSV,把上面几个坑按各自的场景处理掉就行。
关键词:拼多多订单导出,CSV 字段说明,订单数据导出,Excel 打开 CSV 乱码,浏览器扩展