用 WorkBuddy 处理表格:清洗、对账和分析如何留下可核验结果
用一组可重算的合成订单,走完字段对齐、数据清洗、差异对账、分组分析和交付验收。净差70元背后,仍有390元绝对差额值得核查。
- 先确认口径与主键,再做合并和对账
- 空值、重复和异常各留记录,不靠猜测填补
- 核对净差也看绝对差,结果可追到原行
1. 让表格任务交出一组可以相互核对的文件
收到几份销售表,最诱人的指令是“合并一下,再分析”。结果可能很漂亮:总额、趋势、排行榜都有了。可一旦有人问“这笔钱为什么多算了一次”,又得回到原始表重查。表格交付应同时保留明细、规则、异常和核对过程。
WorkBuddy 可以接收 Excel、CSV 等数据文件,并按要求做统计与图表。操作上,把数据放到当前任务可访问的工作空间,或拖入对话,再说明指标、维度和交付格式。官方数据分析示例采用的也是这一入口。先用小样本跑通规则,再处理完整数据。
本文提供三份原创合成 CSV:两批订单导出 orders-a.csv、orders-b.csv,以及 payments.csv。币种为人民币,日期为2026年9月30日,金额统一为元。这些参考结果已用离线脚本计算核对,未在 WorkBuddy 中实测;它们可以用来验收你的执行结果。
下面是可复制的完整输入。每段另存为所标注的UTF-8文件名,再与任务单一起交给WorkBuddy。
展开 orders-a.csv
订单号,日期,区域,件数,单价_元,应收金额_元 A001,2026/09/30,东区,2,120,240 A002 ,2026/09/30,东区,1,180,180 A003,2026/09/30,西区,3,100,300 A004,2026/09/30,西区,2,150,300 A004,2026/09/30,西区,2,150,300
展开 orders-b.csv
order_id,order_date,region,qty,unit_price_cny,receivable_cny A005,2026-09-30,东区,1,260,260 A006,2026-09-30,西区,2,90,180 A007,2026-09-30,东区,1,200, A008,2026-09-30,西区,1,140,140 A009,2026-09-30,东区,2,110,220
展开 payments.csv
订单编号,金额_元 A001,240 A002,180 A003,300 A004,300 A005,250 A006,180 A008,140 A010,160
展开 README.txt
全部记录为原创合成教学数据,不对应真实订单或客户。 日期:2026-09-30;时区:北京时间;币种:CNY;金额单位:元。 两份订单文件是两批导出,文件名不代表数据区域, 按region字段分组。应收金额包含税,不减支付手续费; 收款表使用同口径金额。本例不含退款, 禁止将真实含退款/净结算数据直接套入。 完全重复依据规范化后的六个业务字段判定, 本例已设定A004第二条为导出重复,可移入重复日志; 同键异值必须停下确认。空金额不是0,不用数量乘单价自动填。 差异方向为订单应收减收款;容差0.01元(绝对差<=0.01视为一致); 本例无分期或合并付款。对账仅覆盖可计算明细, A007另列缺金额异常。
2. 合并前先写清楚字段、口径和主键
orders-a.csv 用中文列名,orders-b.csv 用英文列名。把“订单号”和 order_id 映射到一起很容易;把应收金额与支付平台的净结算额相加,却会改变统计含义。开始前应逐项确认币种、含税口径、退款处理、日期范围以及每行代表什么。
本例每行代表一笔单品订单,应收金额含税,收款金额采用相同口径,未扣支付手续费,不含退款。订单号按文本处理,去掉两端空格,保留大小写和有意义的前导零。只有确认主键唯一,才允许一对一匹配。真实明细若含多商品、多次收款或分期,应先在相同业务层级汇总,或采用订单号加行号等复合键。
读取两份订单CSV和收款CSV,只做结构检查,暂不合并。 列出文件名、编码、行数、列名、金额单位、日期范围、空值和主键重复情况。 提出字段映射,区分已知口径与待确认口径。订单号保留文本类型。 检查是否存在多行表头、合并单元格、隐藏行、公式与缓存值等读取风险; CSV没有这些特性时如实说明。 输出字段映射和阻断问题,等我确认后执行清洗。
3. 缺失、重复、异常,各自有处理方式
A004 在同一批次出现两次,六个业务字段完全一致。这个练习明确设定第二条是重复导出,可以移入重复日志;如果真实订单号相同、金额却不同,要先检查是更正版本、拆单还是坏数据。名字相似、金额接近只能作为疑似重复线索,不能直接删行。
A007 的数量为1、单价200元,应收金额留空。不要顺手填200:真实交易可能存在优惠、附加费用或调整。空值应进入待确认表,同时从可计算金额中隔离。空白、零、缺失和不适用需要分别保存,否则后面的平均值和占比也会变。
异常值优先按业务规则找,例如负件数、订单金额与已知计算规则不符、超出时间范围。均值加减两倍标准差只是一种筛查方法,对偏斜分布、小样本或季节性数据都可能误报。异常先标记原因假设,确认后再更正;不要为了让图更平滑而删掉大单。
按已确认映射规范化日期和订单号,保留原始值、源文件与源行号。 本例只移出业务字段完全相同的第二条A004; 同键异值立即停止自动去重。 金额缺失的A007放入待确认表,不用0或数量乘单价填补, 不纳入金额汇总。 输出清洗明细、重复日志、待确认表和质量摘要。必须满足: 原始行数=有效行数+重复移出行数+待确认隔离行数。 不要修改原始CSV。
4. 对账先分组,再看差额的含义
清洗后有8笔可计算订单,应收合计1820元;收款表有8条记录,合计1750元。按订单号比较:6笔金额一致,A005 的应收260元、收款250元,差10元;A009 只在订单表中,金额220元;A010 只在收款表中,金额160元。A007 金额缺失,继续单列,不能悄悄归到匹配成功。
| 订单号 | 订单应收 | 收款 | 应收减收款 | 当前能确认的结论 |
|---|---|---|---|---|
| A005 | 260 | 250 | +10 | 同键金额不符 |
| A009 | 220 | 未匹配 | +220 | 仅订单表存在 |
| A010 | 未匹配 | 160 | −160 | 仅收款表存在 |
净差是70元,计算为10+220-160;绝对差额合计是390元,计算为10+220+160。净差会发生正负抵消,不能单独代表待处理工作量。390元只是本例逐单差额绝对值之和,也不能直接称为损失。缺口可能来自时间差、错记或业务调整,需要凭证才能确认。
按订单号对清洗后明细与收款表做全外连接。先验证双方主键唯一。 金额差=订单应收−收款,绝对差<=0.01元视为一致; 未匹配和缺金额分开。 输出一致、仅订单表、仅收款表、金额不符、无法比较五类。 差异行保留双方原值和源行号。 报告每类数量、净差和绝对差额合计。可能原因只写假设, 不把未匹配直接认定为漏款或损失。
5. 分组分析先算清指标,再解释变化
同一份明细按区域分组,东区应收900元、6件、4笔订单;西区920元、8件、4笔订单。若问“平均每件金额”,东区是900÷6=150元,西区是920÷8=115元;若问“客单价”,分母改成订单数,分别为225元和230元。两个指标不能共用一个“平均单价”标题。
西区总额略高,件数也更多,平均每件金额较低。这些是数据能支持的描述。要判断是否因为折扣、品类结构或渠道策略,还需商品、优惠与渠道数据。只有同一天的小样本,也无法谈增长趋势或推断整个业务的长期表现。
若进一步比较两个时期,应先核对天数、口径和覆盖范围,再计算变化。把销售额分解为数量和价格时,要注明所用分解方法及交互项归属;产品结构变化可能让整体均价改变,不能仅凭均价下降断定降价。
6. 文本反馈也要留下可复查的分类依据
问卷开放题可以另做分类任务,但先定义计数规则。一条“内容实用,时间太晚”的反馈可以同时进入“内容正面”和“时间负面”。允许多标签时,各主题占比相加可能超过100%;必须写清分母是有效回答人数、回答条数,还是标签总次数。
先让 WorkBuddy 用二三十条样本提出分类标准,再人工校准。保留反馈编号、原文、标签、情绪和判定理由,拿不准的放进待复核类别。不要把模型归类变成未经说明的事实,更不要把主动填写问卷的人当作全体用户的代表。
按“内容、时间、场地、其他”对开放反馈分类,允许多标签。 先对抽样记录提出定义、正反例和边界案例, 我确认后再处理全部记录。 逐条保留原文与编号,情绪分正面、负面、中性、混合、待复核。 主题占比=含该主题的有效回答条数÷有效回答总条数, 明确多标签导致占比和可能超过100%。 输出分类明细、汇总和最需要人工复核的记录。
7. 一条完整任务单,把处理边界写进去
可以把下面的任务单用于这个练习。为工作簿指定清楚的页签,也给不能完成的步骤留出口。要求脚本或公式可追溯,不等于默认相信执行正确;它使你有机会复算并找到错误位置。涉及真实财务、客户或员工数据时,应先按组织要求确认可处理范围,练习阶段优先用脱敏副本。
请完成一份可核验的订单整理与对账报告。 输入:orders-a.csv、orders-b.csv、payments.csv、README.txt; 这些是教学合成数据。 阶段1:盘点文件、字段、单位、范围和主键,列出阻断问题。 按README中的明确规则执行;未定义情形先问我。 阶段2:规范化后生成清洗明细、重复日志、待确认表; 保留溯源字段。 阶段3:按订单号对账,分类输出差异;分别计算净差与绝对差额。 阶段4: 按区域统计应收金额、件数、订单数、平均每件金额、客单价。 只写数据支持的结论。 交付:结果.xlsx,含“摘要、明细、差异、待确认、处理日志”; 另存计算脚本或公式说明、质量检查记录。图表标题写明范围与指标, 底层数据可编辑。 验收: 行数守恒、分组合计等于明细总额、双方总额差等于带符号差额之和。 若读取失败或未执行计算,直说哪一步失败,不用估算填报。 约束:不覆盖原文件,不擅自补数,不自动发送, 不连接支付系统或调整真实账目。
8. 验收从五个数开始,再抽查原行
先对照这组参考值:原始10行、重复1行、待确认1行、有效8行、有效应收1820元。然后检查收款1750元、净差70元、绝对差390元。行数总额都对,仍可能存在两个错误彼此抵消,所以必须抽查明细:一条普通记录、一条空格主键、一条重复、一条缺金额,以及每类对账差异。
下面的独立复核代码可另存为 check_reference.py,与三份CSV放在同一目录。先在终端用 cd 切换到这个目录,再运行 python3 check_reference.py;需要已有 Python 3 环境。它只使用标准库和十进制定点金额,避免把二进制浮点误差当作业务差异。它验证的是这套合成数据与规则,不证明任意真实文件都能得到相同质量。
如果 WorkBuddy 只输出一段结论,没有计算明细,就要求补交。若公式未重算,要在表格软件中打开检查;若图表没有单位、基线被截断或范围混用,修正后再用于汇报。无法生成 Excel 时,可先保留 CSV 和规则文档,再明确报告缺少的格式,不要把纯文本改个扩展名当作工作簿。
展开独立复核代码:check_reference.py
# Put beside the three CSV files; uses only Python's standard library.
import csv
from decimal import Decimal as D
def rows(name):
with open(name, encoding='utf-8-sig', newline='') as f:
return list(csv.reader(f))[1:]
raw = rows('orders-a.csv') + rows('orders-b.csv')
normalized = [(r[0].strip(), r[1].replace('/', '-'), r[2],
int(r[3]), D(r[4]), None if r[5] == '' else D(r[5]))
for r in raw]
unique = list(dict.fromkeys(normalized))
assert len({r[0] for r in unique}) == len(unique), 'Same key, different data'
valid = [r for r in unique if r[5] is not None]
missing = [r for r in unique if r[5] is None]
a = {r[0]: r[5] for r in valid}
pay_rows = rows('payments.csv')
assert len({r[0] for r in pay_rows}) == len(pay_rows), 'Duplicate payment key'
b = {r[0]: D(r[1]) for r in pay_rows}
diff = {k: a.get(k, D(0)) - b.get(k, D(0)) for k in a.keys() | b.keys()}
assert (len(raw), len(raw)-len(unique), len(missing), len(valid)) == (10, 1, 1, 8)
assert sum(a.values()) == D('1820') and sum(b.values()) == D('1750')
assert sum(diff.values()) == D('70')
assert sum(abs(v) for v in diff.values()) == D('390')
for region in ['东区', '西区']:
selected = [r for r in valid if r[2] == region]
amount = sum(r[5] for r in selected)
qty = sum(r[3] for r in selected)
print(region, 'amount', amount, 'qty', qty, 'orders', len(selected),
'per_item', amount/qty, 'per_order', amount/len(selected))
print('Reference checks passed; verify unmatched rows separately.')完整逐行参考:把总数拆回每个来源键
以下是本页三份合成输入的人工参考答案,提供清洗、隔离、逐键对账与汇总的完整文本。不是 WorkBuddy 运行产物,也不是 XLSX 工作簿。适合先核对数据,再按任务要求自行组装和验收工作簿;原任务约定的格式要求并未取消。
来源ID统一把表头记为第1行:OA02表示 orders-a.csv 第2行,OB04表示 orders-b.csv 第4行,P06表示 payments.csv 第6行,其余同理。CSV中空字段保留原缺失或该侧未匹配;结合 status 和 scope 区分,不能一律解释成0。逐键表含9个可比较范围内的键,外加单独隔离的A007,共10行。
清洗明细.csv
order_id,date,region,qty,unit_price_cny,receivable_cny,source_id,source_file,source_line,original_order_id,original_date A001,2026-09-30,东区,2,120,240,OA02,orders-a.csv,2,A001,2026/09/30 A002,2026-09-30,东区,1,180,180,OA03,orders-a.csv,3, A002 ,2026/09/30 A003,2026-09-30,西区,3,100,300,OA04,orders-a.csv,4,A003,2026/09/30 A004,2026-09-30,西区,2,150,300,OA05,orders-a.csv,5,A004,2026/09/30 A005,2026-09-30,东区,1,260,260,OB02,orders-b.csv,2,A005,2026-09-30 A006,2026-09-30,西区,2,90,180,OB03,orders-b.csv,3,A006,2026-09-30 A008,2026-09-30,西区,1,140,140,OB05,orders-b.csv,5,A008,2026-09-30 A009,2026-09-30,东区,2,110,220,OB06,orders-b.csv,6,A009,2026-09-30
重复日志.csv
source_id,source_file,source_line,order_id,original_date,region,qty,unit_price_cny,receivable_cny,retained_source_id OA06,orders-a.csv,6,A004,2026/09/30,西区,2,150,300,OA05
待确认.csv
source_id,source_file,source_line,order_id,date,region,qty,unit_price_cny,receivable_cny,reason,next_step OB04,orders-b.csv,4,A007,2026-09-30,东区,1,200,,应收金额缺失,核实原凭证后再决定是否纳入对账
逐键对账.csv
order_id,status,receivable_cny,payment_cny,signed_diff_cny,absolute_diff_cny,order_source_id,payment_source_id,scope A001,一致,240,240,0,0,OA02,P02,纳入 A002,一致,180,180,0,0,OA03,P03,纳入 A003,一致,300,300,0,0,OA04,P04,纳入 A004,一致,300,300,0,0,OA05,P05,纳入 A005,金额不符,260,250,10,10,OB02,P06,纳入 A006,一致,180,180,0,0,OB03,P07,纳入 A007,无法比较,,,,,OB04,,隔离且不计入差额 A008,一致,140,140,0,0,OB05,P08,纳入 A009,仅订单表,220,,220,220,OB06,,纳入 A010,仅收款表,,160,-160,160,,P09,纳入
区域汇总.csv
region,receivable_cny,qty,order_count,amount_per_item_cny,amount_per_order_cny,order_source_ids 东区,900,6,4,150,225,OA02|OA03|OB02|OB06 西区,920,8,4,115,230,OA04|OA05|OB03|OB05 合计,1820,14,8,130,227.5,OA02|OA03|OA04|OA05|OB02|OB03|OB05|OB06
摘要.csv
item,value,unit,scope 原始订单行数,10,行,两份订单数据不含表头 重复移出,1,行,OA06 缺金额隔离,1,行,OB04 可计算订单,8,行,清洗明细 订单应收合计,1820,元,仅可计算订单 收款记录,8,行,P02至P09 收款合计,1750,元,全部收款 一致,6,个键,可计算订单与收款的键并集 金额不符,1,个键,A005 仅订单表,1,个键,A009 仅收款表,1,个键,A010 无法比较,1,个键,A007另列隔离 净差,70,元,纳入对账的9个键 绝对差合计,390,元,纳入对账的9个键
处理日志.csv
action,source_id,source_file,source_line,order_id,original_value,normalized_value,reason,retained_source_id 规范化,OA02,orders-a.csv,2,A001,2026/09/30,2026-09-30,日期分隔符规范化,OA02 规范化,OA03,orders-a.csv,3,A002, A002 ,A002,仅去主键两端空格,OA03 规范化,OA03,orders-a.csv,3,A002,2026/09/30,2026-09-30,日期分隔符规范化,OA03 规范化,OA04,orders-a.csv,4,A003,2026/09/30,2026-09-30,日期分隔符规范化,OA04 规范化,OA05,orders-a.csv,5,A004,2026/09/30,2026-09-30,日期分隔符规范化,OA05 规范化,OA06,orders-a.csv,6,A004,2026/09/30,2026-09-30,规范化后比较六业务字段,OA05 重复移出,OA06,orders-a.csv,6,A004,300,300,本例确认六字段完全相同的导出重复,OA05 缺金额隔离,OB04,orders-b.csv,4,A007,,,原应收为空且禁止补算,
质量检查记录.txt
人工核对顺序与本例答案 1. 来源覆盖:OA02—OA06共5行,OB02—OB06共5行。清洗明细8行、重复日志OA06 1行、待确认OB04 1行,各源行只落一个去向,10=8+1+1。处理日志可多次引用一行,不能用其行数作守恒分母。 2. 规范化:OA03保留原订单号两端空格,匹配键为A002。日期统一为2026-09-30。其余金额、数量、区域没有改写。 3. 重复:OA05与OA06规范化后六业务字段相同,本例明确允许保留前者;收款P05只匹配OA05一次,不能生成两笔300元应收。 4. 缺值:OB04的A007金额仍为空。数量1×单价200不能自动填应收;不进入1820元、净差或绝对差。即使收款侧找不到A007,也应先报告无法比较,不归入220元的仅订单差异。 5. 逐键一致:A001、A002、A003、A004、A006、A008各行两侧差为0,均在0.01元容差内。金额合计1340元,两侧相同。 6. 三项差异:A005的OB02为260元、P06为250元,差+10;A009的OB06为220元、收款无对应键,差+220;A010的P09为160元、可计算订单无对应键,差−160。这里只在差额运算中把无对应键侧作0;CSV原金额仍为空且有类别标识,空金额A007不作0。 7. 交叉守恒:1820=1340+260+220;1750=1340+250+160;净差1820−1750=10+220−160=70;逐键绝对差0×6+10+220+160=390。净额抵消了部分待查金额,390不是损失或漏款结论。 8. 分组:东区900元/6件/4单,西区920元/8件/4单;合计1820元/14件/8单。按原region字段分组,不能把orders-a、orders-b误当东西区。每件均额和客单价分别按件数、订单数作分母。 9. 待查凭证:A005需核实金额差原因;A009需查是否收款时间差或漏记;A010需查订单覆盖范围或录入归属。它们都是待核方向,不是已证实原因。A007先补确认金额及口径,再重新纳入逐键比较。 10. 验证边界:这些是人工参考CSV与计算说明,没有交付结果.xlsx,不代表公式重算、图表、页签和文件打开已验收。原独立复核代码保留不变,其固定合计断言不替代上面逐键、类别与来源检查。实际任务若约定XLSX,仍须生成并验收真实工作簿后才可认定格式交付完成。
9. 重复任务固化规则,异常仍保留停点
月度任务稳定后,可以把字段映射、统计口径、输出模板和验收规则保存为可复用任务规范,再按需要整理成技能。月份、目录和币种应做成显式参数;新列名、重复主键、币种变化、缺文件或异常空值比例,应触发暂停和确认。
每次运行另存日期版本,并记录输入文件、规则版本、纳入与排除范围。上月的正确答案不能替代本月的检查,尤其不要写“以后同类文件无需确认全部处理”。当报表既能解释本次发生了什么,又能让下一位同事追到原行,才真正减少了反复核对的成本。
10. 补充案例:订单号导入碰撞,总额一致也要停止对账
前面的订单号带字母,看起来不容易被当作数值。实际导出常出现“00123”或18位纯数字订单号:金额列没变,订单身份已经变了。下面增加一个独立的五行练习,专门验收导入前后的主键,不与前面的1820元案例混算。
全部数据为原创合成订单,币种CNY,每行一笔订单,对应一笔同额收款,无退款、分期或合并付款。原始两侧各5行、5个不同订单号,金额均为1050元。受损CSV是人为写出的测试输入,表示前导零丢失、长数字尾部改变这类风险;它不是Excel或WorkBuddy的运行产物,也不是通用转换模拟器。本例完成了离线字符串检查和十进制复算,未运行Excel、Power Query或WorkBuddy。
完整输入:保留原行标识,才能知道哪一笔被改了
source_row是这次导入前固定的来源标识,R02与P02分别对应订单和收款源文件第2行,不能在排序后重新编号。order_id必须逐字符保留;本例没有去空格、补零、改大小写等规范化规则。金额以元计。先用文本编辑器检查原始CSV并保留副本,再开始练习。
orders-original.csv
source_row,order_id,amount_cny R02,00123,100 R03,123,200 R04,123456789012345671,300 R05,123456789012345679,400 R06,A001,50
payments-original.csv
source_row,order_id,amount_cny P02,00123,100 P03,123,200 P04,123456789012345671,300 P05,123456789012345679,400 P06,A001,50
人为设定的受损副本如下。00123变为123;两个不同的18位订单号变为同一个值。A001保持不变。不要通过删行让受损主键“重新唯一”。
orders-damaged.csv
source_row,order_id,amount_cny R02,123,100 R03,123,200 R04,123456789012345000,300 R05,123456789012345000,400 R06,A001,50
payments-damaged.csv
source_row,order_id,amount_cny P02,123,100 P03,123,200 P04,123456789012345000,300 P05,123456789012345000,400 P06,A001,50
完整预期结果:3行改变,4行受到碰撞影响
订单侧R02、R04、R05的标识改变;R03虽然仍为123,却与R02撞成同一个键。因此是3行改变、2组碰撞、4行受影响。收款侧P02、P04、P05相应改变,P03同样被卷入。两侧行数仍各为5,总额仍各为1050元,唯一键数却从5变成3。验收结论为BLOCKED_KEY_IDENTITY,暂停去重、合并与对账,保留全部原行。
下表记录订单侧每行的原值、受损值和处理停点。exact_equal表示逐字符相等;collision_member表示该行属于受损键重复组。true/false只描述这两项检查,不代表整批通过。
expected-identity-report.csv
source_row,original_id,imported_id,exact_equal,collision_member,next_step R02,00123,123,false,true,reimport_from_original R03,123,123,true,true,reimport_from_original R04,123456789012345671,123456789012345000,false,true,reimport_from_original R05,123456789012345679,123456789012345000,false,true,reimport_from_original R06,A001,A001,true,false,retain_but_batch_blocked
更隐蔽的错误是两边都损坏,再按受损订单号汇总。这样得到的三个组分别为300、700、50元,每组差额、净差、绝对差额都为0。下面的完整结果只用于展示错误放行,不能替代五笔原始订单的一对一核验,更不能称为“对账完成”。
rejected-grouped-reconciliation.csv
order_id,receivable_cny,payment_cny,difference_cny 123,300,300,0 123456789012345000,700,700,0 A001,50,50,0
恢复从未受损源文件开始
Microsoft说明,Excel数值精度上限为15位有效数字,长数字标识应按文本保存。把已损坏的数值改成文本、加引号、设置显示前导零,不能找回原来是哪一个订单号。科学计数法外观也不能单独证明是否损坏,要对照原始字符值。参见保留前导零与长数字。
- 打开未受损CSV的副本。按Microsoft的文本导入说明,从“数据 → 从文本/CSV”进入,在预览中选择“转换数据”,先检查再加载。
- 查看Power Query“应用的步骤”。若自动“更改的类型”已经把order_id转成数值,应回到内容仍完整的步骤,删除或替换错误转换,让order_id在首次类型转换时就是Text。不能只在受损步骤之后追加Text转换。
- 重新加载后,以固定source_row逐行与原始文本比较,两侧分别检查来源无缺失/新增、标识完全一致、主键唯一、每行金额不变。全部通过后才允许本例的一对一匹配。
- 未受损源文件找不到,或源文件本身已被覆盖损坏,就停下请求重新导出。不能按长度、相邻行或AI猜测补回零和末尾数字。
Power Query对CSV默认会自动检测类型,可能生成“更改的类型”步骤。但它的Decimal Number与64位Whole Number有不同精度范围,不能把Excel单元格的15位限制一概套到所有Power Query数值类型。文本标识也不该为了计算而转成整数。参见Power Query数据类型。
另一个预防选项是新版Excel的自动数据转换开关:Microsoft 365和Excel 2024的Windows/Mac版本提供相关设置;Windows在“文件 → 选项 → 数据”,Mac在“Excel → 偏好设置 → 编辑”。这些开关不直接控制Power Query导入,旧版或不同入口也不能照搬。参见自动数据转换设置与适用范围。以上为依据官方文档整理的操作路线,实际菜单与保留结果仍需在所用版本中核验。
下面两份restored-text文件是本次离线重读未受损源文件后保留的参考内容,字符内容与original文件相同。CSV自身不保存单元格类型,文件名含text也不会阻止再次误转换;重新导入仍需明确设置文本类型。这不是原生Excel恢复实测记录。
orders-restored-text.csv
source_row,order_id,amount_cny R02,00123,100 R03,123,200 R04,123456789012345671,300 R05,123456789012345679,400 R06,A001,50
payments-restored-text.csv
source_row,order_id,amount_cny P02,00123,100 P03,123,200 P04,123456789012345671,300 P05,123456789012345679,400 P06,A001,50
完整的正确匹配参考如下:5个不同文本键、5组一对一来源关系,每笔差额均为0,双方合计1050元。该结果只有在两侧身份检查通过后才可接受。
expected-restored-reconciliation.csv
order_id,order_source_row,payment_source_row,receivable_cny,payment_cny,difference_cny,status 00123,R02,P02,100,100,0,matched 123,R03,P03,200,200,0,matched 123456789012345671,R04,P04,300,300,0,matched 123456789012345679,R05,P05,400,400,0,matched A001,R06,P06,50,50,0,matched
可复制的只读复核
把以下代码另存为check_ids.py,与CSV一起放在本地目录,用已有Python 3运行。它只读取文件并输出JSON,不修改数据、不连接账户,也不模拟Excel。只接受本例的三列表头和非负、最多两位小数的元金额;不能直接套用到含退款、多币种或多行订单的真实账目。原始文件本身必须可靠;两份同样损坏的文件互相比较,不能证明原始身份正确。
python3 check_ids.py orders-original.csv orders-damaged.csv python3 check_ids.py payments-original.csv payments-damaged.csv python3 check_ids.py orders-original.csv orders-restored-text.csv python3 check_ids.py payments-original.csv payments-restored-text.csv
前两条应返回BLOCKED_KEY_IDENTITY且退出码为2;后两条应返回PASS_REFERENCE且退出码为0。PASS_REFERENCE仅表示这次三列参考比较通过,不代表业务对账或Excel操作已经完成。还应做一个反向测试:仅把A001改为A002,行数、唯一键数和总额都不变,程序仍必须因逐行标识不同而拒绝。
展开完整只读代码:check_ids.py
"""Read-only identity check for the three-column teaching CSV contract.
Usage: python3 check_ids.py untouched-source.csv candidate.csv
Exit 0 means this reference comparison passed; exit 2 means blocked.
Does not import into Excel, simulate conversions, repair data, or reconcile.
"""
import csv
import json
import re
import sys
from collections import defaultdict
from decimal import Decimal, localcontext
def read_rows(path):
with open(path, encoding="utf-8-sig", newline="") as stream:
reader = csv.reader(stream, strict=True)
if next(reader, None) != ["source_row", "order_id", "amount_cny"]:
raise ValueError("Expected source_row,order_id,amount_cny header")
rows = {}
for line, cells in enumerate(reader, 2):
if len(cells) != 3 or not cells[0].strip() or not cells[1].strip():
raise ValueError(f"Invalid width or blank identifier at line {line}")
source, identifier, amount = cells
if source in rows:
raise ValueError(f"Duplicate source_row: {source}")
if not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,2})?", amount):
raise ValueError(f"Expected nonnegative CNY with at most 2 decimals at {source}")
rows[source] = (identifier, Decimal(amount))
if not rows:
raise ValueError("Empty data")
return rows
def groups(rows):
result = defaultdict(list)
for source, (identifier, _) in rows.items():
result[identifier].append(source)
return dict(result)
def total(rows):
amounts = [value[1] for value in rows.values()]
with localcontext() as context:
context.prec = max(28, max(len(v.as_tuple().digits) for v in amounts)
+ len(str(len(amounts))) + 3)
return format(sum(amounts, Decimal(0)), ".2f")
def compare(original, candidate):
a, b = groups(original), groups(candidate)
missing = sorted(original.keys() - candidate.keys())
extra = sorted(candidate.keys() - original.keys())
shared = original.keys() & candidate.keys()
changed = [{"source_row": source, "original_id": original[source][0],
"candidate_id": candidate[source][0]}
for source in sorted(shared)
if original[source][0] != candidate[source][0]]
amount_changes = sorted(source for source in shared
if original[source][1] != candidate[source][1])
collisions = {key: value for key, value in b.items() if len(value) > 1}
key_blocked = bool(missing or extra or changed or collisions
or len(a) != len(original))
decision = ("BLOCKED_KEY_IDENTITY" if key_blocked else
"BLOCKED_DATA_VALUES" if amount_changes else "PASS_REFERENCE")
return {
"decision": decision,
"original_rows": len(original), "candidate_rows": len(candidate),
"original_unique_ids": len(a), "candidate_unique_ids": len(b),
"original_total_cny": total(original), "candidate_total_cny": total(candidate),
"missing_source_rows": missing, "extra_source_rows": extra,
"changed_identifiers": changed, "amount_changed_source_rows": amount_changes,
"candidate_collision_groups": collisions,
"collision_affected_rows": sum(map(len, collisions.values())),
"original_ids_unique": len(a) == len(original),
}
def main(argv):
if len(argv) != 3:
print("Usage: python3 check_ids.py untouched-source.csv candidate.csv", file=sys.stderr)
return 2
try:
report = compare(read_rows(argv[1]), read_rows(argv[2]))
except (OSError, UnicodeError, csv.Error, ValueError) as error:
report = {"decision": "BLOCKED_INPUT", "error": str(error)}
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if report["decision"] == "PASS_REFERENCE" else 2
if __name__ == "__main__":
sys.exit(main(sys.argv))订单受损输入的完整JSON输出
{
"decision": "BLOCKED_KEY_IDENTITY",
"original_rows": 5,
"candidate_rows": 5,
"original_unique_ids": 5,
"candidate_unique_ids": 3,
"original_total_cny": "1050.00",
"candidate_total_cny": "1050.00",
"missing_source_rows": [],
"extra_source_rows": [],
"changed_identifiers": [
{
"source_row": "R02",
"original_id": "00123",
"candidate_id": "123"
},
{
"source_row": "R04",
"original_id": "123456789012345671",
"candidate_id": "123456789012345000"
},
{
"source_row": "R05",
"original_id": "123456789012345679",
"candidate_id": "123456789012345000"
}
],
"amount_changed_source_rows": [],
"candidate_collision_groups": {
"123": [
"R02",
"R03"
],
"123456789012345000": [
"R04",
"R05"
]
},
"collision_affected_rows": 4,
"original_ids_unique": true
}收款受损输入的完整JSON输出
{
"decision": "BLOCKED_KEY_IDENTITY",
"original_rows": 5,
"candidate_rows": 5,
"original_unique_ids": 5,
"candidate_unique_ids": 3,
"original_total_cny": "1050.00",
"candidate_total_cny": "1050.00",
"missing_source_rows": [],
"extra_source_rows": [],
"changed_identifiers": [
{
"source_row": "P02",
"original_id": "00123",
"candidate_id": "123"
},
{
"source_row": "P04",
"original_id": "123456789012345671",
"candidate_id": "123456789012345000"
},
{
"source_row": "P05",
"original_id": "123456789012345679",
"candidate_id": "123456789012345000"
}
],
"amount_changed_source_rows": [],
"candidate_collision_groups": {
"123": [
"P02",
"P03"
],
"123456789012345000": [
"P04",
"P05"
]
},
"collision_affected_rows": 4,
"original_ids_unique": true
}恢复参考的完整JSON输出(两侧相同)
{
"decision": "PASS_REFERENCE",
"original_rows": 5,
"candidate_rows": 5,
"original_unique_ids": 5,
"candidate_unique_ids": 5,
"original_total_cny": "1050.00",
"candidate_total_cny": "1050.00",
"missing_source_rows": [],
"extra_source_rows": [],
"changed_identifiers": [],
"amount_changed_source_rows": [],
"candidate_collision_groups": {},
"collision_affected_rows": 0,
"original_ids_unique": true
}可给WorkBuddy追加这条验收要求:在做任何按订单号的去重、聚合或匹配前,保留未受损源文件及固定来源标识;逐行比较订单号字符串。发现来源缺失、标识改变或碰撞时停止,交出原值、当前值、来源与异常清单。不得根据金额相同或差额为0放行,不得猜测已丢失的数字。未实际执行的检查明确标注“未运行”。
这个补例回应的是具体导入需求:2026年8月15日的Microsoft Q&A提问描述了CSV标识前导零被去掉等问题。它只作为公开需求实例,不代表搜索热度、普遍发生率或产品规格;上述行为边界以官方产品文档为依据。
金额为空能否按数量乘单价补齐?
只有已确认的业务规则允许时才能补。优惠、退款和附加费都可能使简单乘积不等于应收。
两边总额一致是否就完成对账?
还需逐键比较,正负差额可能抵消。检查匹配覆盖率、差异明细与无法比较的记录。