夜雨聆风学习资料网

ARTICLE · 1266877

让AI改Excel报价表,一行成本被它乘了6:139行脚本锁公式,谁动谁现形

让AI改Excel报价表,一行成本被它乘了6:139行脚本锁公式,谁动谁现形

让AI改Excel报价表,一行成本被它乘了6:139行脚本锁公式,谁动谁现形

AI改Excel,30秒回你一句“改完了”。它没撒谎——它确实改完了,只是改的方向你没想到:把一行超时服务成本凭空乘了6,900块的活儿变成5400块。没拦住的话,这份报价就直接报出去了。

这篇讲我们怎么用三个命令把这种事故焊死:scan体检、lock上锁、diff比对。脚本139行,文末全贴,拿走就能跑。

别觉得这事离你远。预算表、工时统计、对账单、合伙分账、房租水电分摊——只要表里有公式,你就已经站在这个坑的边上。把表丢给AI改的人越来越多,回传时能逐格核完的没几个,事故只是还没轮到你头上。

一、活儿是怎么翻车的

真实场景,客户信息隐去:一份劳务报价表要按新人数重算。表不复杂,三行服务项,单价乘数量,底部合计。这种活儿以前自己改公式,现在习惯扔给AI助手,快。

回传的表肉眼看不出任何问题:格式没乱,数字也都对得上。问题藏在E4单元格里——超时值守那行,原公式是=C4D4,150元/小时乘6小时。AI回传时自作主张,写成了=C4D4*6。

它多半是把表里“6人”这个人数系数理解成要再乘一遍。一行900块的成本变成5400,合计跟着多出4500。这种错误肉眼查不出来:数字格式正确、表格没坏、合计照样算得通——只有把改动前后的公式逐格调出来对,才知道哪里被动了手脚。

那次是真翻车了,好在发现得早——是复核时顺手核了一遍合计,6700块的表核出11200,才顺着往下扒出E4的猫腻。要是直接把回传件接着往下传,这页纸就带着虚增的成本走出去了。

复盘的结论很直接:问题出在我们没法证明AI没改不该改的地方。人工逐格核对一张几十行的表,二十分钟起步,还保证不了漏网;靠“看一眼”守不住,得靠工具。另外我们把一条规矩写进了给AI的任务描述:公式区当只读,明确“只改数值格,公式一行别碰”。指令管住态度,工具管住结果,两个都要有。下面就是那套工具的三步。

二、第一步scan:先弄清楚哪些格子动不得

python excel_guard_lite.py scan 报价表_原件.xlsx 

实测输出:

[劳务报价] 公式 4 个:     E3 = =C3*D3     E4 = =C4*D4     E5 = =C5*D5     E6 = =SUM(E3:E5) [劳务报价] 带格式空白格 5 个(看着空,别让AI碰):F2, F3, F4, F5, F6 [劳务报价] 合并单元格 1 个 

体检扫出来的三类东西就是“禁区”:4个公式格、5个带格式空白格、1个合并单元格。带格式空白格最阴——看着是空的,其实带着边框,往往是留给别人填的位置。AI手滑在这里塞个东西、或者顺手清个格式,表就废了一半,澄清口径全乱。先把禁区数清楚,再谈交给谁改。我们现在的习惯是,拿到任何一张不是自己亲手做的表,第一动作就是scan一遍——别人的表里藏着什么,扫完才有底。

三、第二步lock:给公式上锁,副本拿去分发

python excel_guard_lite.py lock 报价表_原件.xlsx 

实测输出:

>> 护身副本已生成:报价表_原件.guarded.xlsx >> 锁定 9 个公式/带格式空白格;普通格照常可填。 >> 原始文件未动。把 guarded 副本发给AI,原件自己封存。 

锁定逻辑很朴素:公式格和带格式空白格全部锁死,整表开启保护,普通格照常能填能选。生成的是新文件,原件一个字节都不动。上锁效果我们回读验证过:

python -c "from openpyxl import load_workbook; ws=load_workbook('报价表_原件.guarded.xlsx')['劳务报价']; print('工作表保护:', bool(ws.protection.sheet)); print('E4(公式格) locked:', ws['E4'].protection.locked); print('B3(普通格) locked:', ws['B3'].protection.locked)" 
工作表保护: True E4(公式格) locked: True B3(普通格) locked: False 

再补一个反向验证——拿diff去比原件和上锁副本:

>> 完全一致:值/公式没有任何变动。 

上锁这个动作本身不改任何值。以后分发表格就发guarded副本:AI能读、人能填、公式动不了。等于给AI划了只读的工作面——它能看全表、能在允许的格子里填数,想碰公式先过锁这一关。

四、第三步diff:回传件先过安检,公式被动就报警

模拟一次事故现场——AI把E4擅自改成=C4D46后回传:

python excel_guard_lite.py diff 报价表_原件.xlsx 报价表_AI改后.xlsx 

实测输出:

======================================================== 逐格比对:   A = 报价表_原件.xlsx   B = 报价表_AI改后.xlsx ======================================================== [公式被改] 劳务报价!E4       '=C4*D4'    →  '=C4*D4*6' -------------------------------------------------------- >> 共 1 处变动,其中公式被改 1 处。 

就是这一下,900变5400当场现形。复跑时命令结束敲 echo $?,那个数字就是退出码——它是留给自动化用的:公式被改退出码2,干净退出码0。挂进流程里,回传件先跑diff,退出码不是0直接拦截,人只看打印出来的那几行。

还有一个容易漏掉的场景:AI没动公式,但“顺手”把数字改了。我们把C4单价从150改成155再diff:

[值变动] 劳务报价!C4       150    →  155 -------------------------------------------------------- >> 共 1 处变动,其中公式被改 0 处。 

值变动会列出来,但退出码给0——因为改值很多时候是正常业务动作:人数从5调到6、单价按新合同调整,本来就该改。公式才是红线,公式被改直接判死;值改得对不对,名单在这儿,人来核。

五、五个坑,都是实测踩出来的

  1. lock生成副本走的是openpyxl重存。我们拿带柱状图的表实测过,图表往返还在;但openpyxl对图片、部分数据验证这类复杂对象的往返历来有兼容风险——所以流程上原件永远封存。副本丢了顶多重做,原件没了没处哭。
  2. 工作表保护是无密码的弱保护,定位是防手滑、防AI擅动,挡不住存心破解的人。真要防高手,靠diff这道关卡,那把锁只是第一道门。
  3. 只认.xlsx。.xls老格式直接报错,先另存成xlsx再跑。
  4. 这份精简版diff只比对值和公式,格式被改它不报。要盯格式,思路是给每个单元格算一份格式指纹——数字格式、字体、边框、填充拼一起做哈希,新旧文件指纹对不上就列出来。完整版是这么做的,有需要可以照这个思路加。
  5. 加表删表,diff会列出来但照样放行,退出码仍是0——红线只有公式格。删表比改公式更狠:整张表没了,红线一格都查不着。所以底账永远要封存原件,回传件里表被人抽了,你手里得有原本能比。

六、这套流程适合谁

报价表只是我们的场景,任何带公式的表都适用:预算、工时、成本测算、对账单、分账表。分工口诀三句话:原件封存,guarded副本分发,回传件必过diff。

AI照样用,改得也确实快——但“改完了”三个字不算数,退出码0才算数。

Excel报价表这种东西,AI改十次对九次,错的那一次是钱。锁不锁,自己掂量。

完整脚本(excel_guard_lite.py,139行)

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ excel_guard_lite.py —— Excel公式护身工具(精简版)  用法:   python excel_guard_lite.py scan 报价表.xlsx            # 体检:列出公式格/带格式空白格/合并格   python excel_guard_lite.py lock 报价表.xlsx            # 上锁:生成 报价表.guarded.xlsx,公式锁定+保护工作表   python excel_guard_lite.py diff 原件.xlsx 回传件.xlsx   # 逐格比对:公式被改→打印差异→退出码2  依赖:pip install openpyxl(只认 .xlsx,.xls 老格式不支持) """ import sys import openpyxl from openpyxl.styles import Protection   def is_formula(cell):     return isinstance(cell.value, str) and cell.value.startswith("=")   def styled_blank(cell):     """看似空但带格式(边框/填充)的单元格——常是留给填写/备注的位置,不能乱动。"""     if cell.value not in (None, ""):         return False     b = cell.border     if b and any([b.left.style, b.right.style, b.top.style, b.bottom.style]):         return True     fill = cell.fill     if fill and fill.fgColor and fill.fgColor.rgb not in (None, "00000000"):         return True     return False   def guarded_name(path):     return path[:-5] + ".guarded.xlsx" if path.lower().endswith(".xlsx") else path + ".guarded.xlsx"   def cmd_scan(path):     wb = openpyxl.load_workbook(path, data_only=False)     for ws in wb.worksheets:         formulas, blanks = [], []         for row in ws.iter_rows():             for c in row:                 if is_formula(c):                     formulas.append((c.coordinate, c.value))                 elif styled_blank(c):                     blanks.append(c.coordinate)         print(f"[{ws.title}] 公式 {len(formulas)} 个:")         for co, f in formulas:             print(f"    {co} = {f}")         if blanks:             print(f"[{ws.title}] 带格式空白格 {len(blanks)} 个(看着空,别让AI碰):"                   + ", ".join(blanks))         if ws.merged_cells.ranges:             print(f"[{ws.title}] 合并单元格 {len(ws.merged_cells.ranges)} 个")         print()   def cmd_lock(path):     wb = openpyxl.load_workbook(path, data_only=False)     out = guarded_name(path)     locked = 0     for ws in wb.worksheets:         for row in ws.iter_rows():             for c in row:                 keep = is_formula(c) or styled_blank(c)                 c.protection = Protection(locked=keep)                 locked += 1 if keep else 0         ws.protection.sheet = True         ws.protection.enable()     wb.save(out)     print(f">> 护身副本已生成:{out}")     print(f">> 锁定 {locked} 个公式/带格式空白格;普通格照常可填。")     print(">> 原始文件未动。把 guarded 副本发给AI,原件自己封存。")   def cmd_diff(path_a, path_b):     wb_a = openpyxl.load_workbook(path_a, data_only=False)     wb_b = openpyxl.load_workbook(path_b, data_only=False)     names_a = {ws.title for ws in wb_a.worksheets}     names_b = {ws.title for ws in wb_b.worksheets}     changes = []     for added in sorted(names_b - names_a):         changes.append(("SHEET+", added, "", ""))     for removed in sorted(names_a - names_b):         changes.append(("SHEET-", removed, "", ""))     for ws_a in wb_a.worksheets:         if ws_a.title not in names_b:             continue         ws_b = wb_b[ws_a.title]         coords = set()         for row in ws_a.iter_rows():             for c in row:                 coords.add(c.coordinate)         for row in ws_b.iter_rows():             for c in row:                 coords.add(c.coordinate)         for co in sorted(coords):             va, vb = ws_a[co].value, ws_b[co].value             if va != vb:                 kind = "FORMULA" if (is_formula(ws_a[co]) or is_formula(ws_b[co])) else "VALUE"                 changes.append((kind, f"{ws_a.title}!{co}", va, vb))     formula_changes = [c for c in changes if c[0] == "FORMULA"]     print("=" * 56)     print(f"逐格比对:\n  A = {path_a}\n  B = {path_b}")     print("=" * 56)     if not changes:         print(">> 完全一致:值/公式没有任何变动。")     else:         mark = {"FORMULA": "[公式被改]", "VALUE": "[值变动]", "SHEET+": "[新增表]", "SHEET-": "[删表]"}         for kind, where, a, b in changes:             print(f"{mark.get(kind, kind)} {where}\n      {a!r}\n   →  {b!r}")         print("-" * 56)         print(f">> 共 {len(changes)} 处变动,其中公式被改 {len(formula_changes)} 处。")     sys.exit(2 if formula_changes else 0)   def main():     if len(sys.argv) < 3:         print(__doc__)         sys.exit(1)     cmd = sys.argv[1]     if cmd == "scan":         cmd_scan(sys.argv[2])     elif cmd == "lock":         cmd_lock(sys.argv[2])     elif cmd == "diff":         if len(sys.argv) < 4:             print(__doc__)             sys.exit(1)         cmd_diff(sys.argv[2], sys.argv[3])     else:         print(__doc__)         sys.exit(1)   if __name__ == "__main__":     main() 

相关学习资料