1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# -*- coding: utf-8 -*-
"""汇总大表差异比对:生成件 vs 手工定稿
用法: python 汇总大表差异比对.py <生成件.xlsx> <手工定稿.xlsx> <输出前缀>
说明: 逐页按"行标签/列表头"做序列对齐(difflib),可正确处理多列/多行/重复表头;
      每个不一致格归入 结构位移|取整尾差|文本型数字|草稿/辅助列|仅程序有|仅手工有|文本不同|错误值|实质数值差异。
"""
import datetime, io, os, sys
from difflib import SequenceMatcher
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
 
def col_label(ws, c, hdr_rows=6):
    for r in range(1, hdr_rows+1):
        v = ws.cell(row=r, column=c).value
        if isinstance(v, (int, float)) and not isinstance(v, bool) and 20000 < v < 60000:
            d = datetime.date(1899,12,30) + datetime.timedelta(days=int(v))
            return "%d-%02d" % (d.year, d.month)
        if isinstance(v, str) and v.strip() and not v.startswith("="):
            return v.strip()[:20]
    return ""
 
def row_label(ws):
    out = []; cur = ""; seen = {}
    for r in range(1, ws.max_row+1):
        a = ws.cell(row=r, column=1).value; b = ws.cell(row=r, column=2).value
        if isinstance(a, str) and a.strip(): cur = a.strip()
        lab = ("%s|%s" % (cur, b.strip())) if (isinstance(b, str) and b.strip()) else (cur or "")
        seen[lab] = seen.get(lab, 0) + 1
        out.append("%s#%d" % (lab, seen[lab]))
    return out
 
def num(v):
    return isinstance(v, (int, float)) and not isinstance(v, bool)
 
def text_num(v):
    if isinstance(v, str):
        s = v.strip().replace(",", "")
        try:
            return float(s)
        except Exception:
            return None
    return None
 
def match_labels(a, b):
    sm = SequenceMatcher(None, a, b, autojunk=False)
    pairs = {}
    for op, i1, i2, j1, j2 in sm.get_opcodes():
        if op == "equal":
            for k in range(i2-i1): pairs[i1+k] = j1+k
    return pairs
 
def main(gen, dgf, prefix):
    g = load_workbook(gen, data_only=True); d = load_workbook(dgf, data_only=True)
    rows_out = []
    per_sheet = []
    for sh in g.sheetnames:
        if sh not in d.sheetnames: continue
        wg, wd = g[sh], d[sh]
        rl_g, rl_d = row_label(wg), row_label(wd)
        cl_g = [col_label(wg, c) for c in range(1, wg.max_column+1)]
        cl_d = [col_label(wd, c) for c in range(1, wd.max_column+1)]
        colmap = match_labels(cl_g, cl_d); rowmap = match_labels(rl_g, rl_d)
        cnt = {}
        def bump(k):
            cnt[k] = cnt.get(k, 0) + 1
        # 列/行的结构性缺口
        unmatched_cols_g = [c-1 for c in range(1, len(cl_g)+1) if c-1 not in colmap]
        unmatched_cols_d = [c-1 for c in range(1, len(cl_d)+1) if c-1 not in set(colmap.values())]
        unmatched_rows_g = [r-1 for r in range(1, len(rl_g)+1) if r-1 not in rowmap]
        unmatched_rows_d = [r-1 for r in range(1, len(rl_d)+1) if r-1 not in set(rowmap.values())]
        for c in unmatched_cols_g:
            if c >= 2: bump("仅程序有(列)"); rows_out.append([sh, "%s*" % get_column_letter(c+1), "(整列)", cl_g[c], "仅程序有(列)", "", "", "生成件多出此列"])
        for c in unmatched_cols_d:
            if c >= 2: bump("仅手工有(列)"); rows_out.append([sh, "(无)", "(整列)", cl_d[c], "仅手工有(列)", "", "", "手工定稿多出此列"])
        for r in unmatched_rows_g:
            if r >= 4: bump("仅程序有(行)"); rows_out.append([sh, "第%d行" % (r+1), rl_g[r], "(整行)", "仅程序有(行)", "", "", "生成件多出此区段"])
        for r in unmatched_rows_d:
            if r >= 4: bump("仅手工有(行)"); rows_out.append([sh, "(无)", rl_d[r], "(整行)", "仅手工有(行)", "", "", "手工定稿多出此区段"])
        for i, j in sorted(colmap.items()):
            for p, q in sorted(rowmap.items()):
                vg = wg.cell(row=p+1, column=i+1).value; vd = wd.cell(row=q+1, column=j+1).value
                if vg == vd: continue
                if vg is None and isinstance(vd, str) and vd.startswith("="): vd = None
                if vd is None and isinstance(vg, str) and vg.startswith("="): vg = None
                if (vg is None) and (vd is None): continue
                cat = None; note = ""
                tg, td = text_num(vg), text_num(vd)
                if num(vg) and num(vd):
                    diff = abs(vg - vd)
                    if diff <= 1e-6: continue
                    rel = diff / max(abs(vd), 1e-9)
                    if diff <= 0.01 or rel <= 1e-4: cat = "取整尾差"
                    else: cat = "实质数值差异"
                    note = "差=%.6g" % diff
                elif num(vg) and (vd is None):
                    cat = "仅程序有"; 
                elif num(vd) and (vg is None):
                    cat = "仅手工有"
                elif tg is not None and num(vd) and abs(tg - vd) <= 1e-9:
                    cat = "文本型数字"; note = "手工为文本'%s'" % vg
                elif td is not None and num(vg) and abs(td - vg) <= 1e-9:
                    cat = "文本型数字"; note = "程序为文本'%s'" % vd
                elif isinstance(vg, str) and isinstance(vd, str):
                    cat = "文本不同"; note = "程序'%s' / 手工'%s'" % (vg[:18], vd[:18])
                elif isinstance(vg, str) and vg.startswith("#") :
                    cat = "错误值(程序)"; note = vg
                elif isinstance(vd, str) and vd.startswith("#"):
                    cat = "错误值(手工)"; note = vd
                else:
                    cat = "其它"; note = "程序=%s 手工=%s" % (str(vg)[:14], str(vd)[:14])
                if cat:
                    bump(cat)
                    rows_out.append([sh, "%s%d" % (get_column_letter(i+1), p+1), rl_g[p][:38], cl_g[i][:18], cat,
                                     ("" if vg is None else str(vg)[:20]), ("" if vd is None else str(vd)[:20]), note])
        per_sheet.append((sh, cnt))
    # 输出 CSV
    csv = prefix + ".csv"
    with io.open(csv, "w", encoding="utf-8-sig", newline="") as f:
        f.write("页签,生成件单元格,行标签,列表头,类别,生成件值,手工值,备注\n")
        for x in rows_out:
            f.write(",".join('"%s"' % str(v).replace('"', "'") for v in x) + "\n")
    # 输出 MD
    cats = ["实质数值差异", "仅手工有", "仅程序有", "取整尾差", "文本型数字", "文本不同", "错误值(程序)", "错误值(手工)", "仅程序有(列)", "仅手工有(列)", "仅程序有(行)", "仅手工有(行)", "其它"]
    md = prefix + ".md"
    with io.open(md, "w", encoding="utf-8", newline="\n") as f:
        f.write("# 汇总大表差异明细(生成件 vs 手工定稿)\n\n")
        f.write("- 生成件:`%s`\n- 手工定稿:`%s`\n- 比对时间:%s\n- 明细数据:`%s`\n\n" % (gen, dgf, datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), os.path.basename(csv)))
        f.write("## 逐页汇总\n\n| 页签 | " + " | ".join(cats[:7]) + " |\n|---|" + "---|"*7 + "\n")
        for sh, cnt in per_sheet:
            f.write("| %s | %s |\n" % (sh, " | ".join(str(cnt.get(c, 0)) for c in cats[:7])))
        f.write("\n## 实质数值差异(全部)\n\n")
        subs = [x for x in rows_out if x[4] == "实质数值差异"]
        if not subs: f.write("(无)\n")
        for x in subs[:200]:
            f.write("- %s %s [%s] 生成件=%s 手工=%s(%s)\n" % (x[0], x[1], x[2], x[5], x[6], x[7]))
        f.write("\n## 仅手工有(前 200 条)\n\n")
        for x in [y for y in rows_out if y[4] in ("仅手工有", "仅手工有(列)")][:200]:
            f.write("- %s %s [%s] %s 手工=%s\n" % (x[0], x[1], x[2], x[3], x[6]))
        f.write("\n## 仅程序有(前 200 条)\n\n")
        for x in [y for y in rows_out if y[4] in ("仅程序有", "仅程序有(列)", "仅程序有(行)")][:200]:
            f.write("- %s %s [%s] %s 生成件=%s\n" % (x[0], x[1], x[2], x[3], x[5]))
    print("已输出:", md, "|", csv, "| 明细条数:", len(rows_out))
    for sh, cnt in per_sheet:
        if cnt: print("   %-12s %s" % (sh, dict(cnt)))
 
if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3])