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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# -*- coding: utf-8 -*-
"""汇总大表差异比对:生成件 vs 手工定稿
用法: python 汇总大表差异比对.py <生成件.xlsx> <手工定稿.xlsx> <输出前缀>
说明: 逐页按"行标签/列表头"做序列对齐(difflib),可正确处理多列/多行/重复表头;
      每个不一致格归入 结构位移|取整尾差|文本型数字|草稿/辅助列|仅程序有|仅手工有|文本不同|错误值|实质数值差异。
"""
import datetime, io, os, re, 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()
        if isinstance(b, str) and b.strip():
            lab = "%s|%s|%s" % (block_of(ws, r), norm_region(cur), norm_indicator(b))
        else:
            lab = "%s|%s|" % (block_of(ws, r), norm_region(cur))
        seen[lab] = seen.get(lab, 0) + 1
        out.append("%s#%d" % (lab, seen[lab]))
    return out
 
def norm(s):
    """标签归一化:去空白、去全角空格、统一引号、去掉‘市/省/州’后缀差异"""
    if not isinstance(s, str): return ""
    t = s.replace("\u3000", " ").replace("(", "(").replace(")", ")").strip()
    t = re.sub(r"[\s]+", "", t)
    return t
 
def norm_region(s):
    t = norm(s)
    for suf in ("市", "省", "自治州", "州"):
        if t.endswith(suf) and len(t) > len(suf): t = t[:-len(suf)]
    return t
 
def norm_indicator(s):
    t = norm(s).replace("(万吨公里)", "").replace("(万吨)", "").replace("(万人公里)", "").replace("(万人)", "")
    t = t.replace("其中规上", "规上").replace("其中规下", "规下")
    return t
 
def block_of(ws, r):
    """向上找最近一个"标题行"(B 列为空且 A/文字较长)判断所属块:累计 / 当月 / 其它"""
    for rr in range(r, 0, -1):
        a = ws.cell(row=rr, column=1).value; b = ws.cell(row=rr, column=2).value
        txt = " ".join(str(x) for x in (a, b) if isinstance(x, str))
        if not isinstance(b, str) or not b.strip():
            if len(norm(txt)) >= 6:
                if "累计" in txt: return "累计块"
                if "各市州" in txt or "排名" in txt or "明细表" in txt: return "当月块"
    return "其它"
 
# 已知设计差异:不计入问题(按用户 2026-09-28 口径)
KNOWN_ROWS = {" 货运": ("规上+规下货运量", "规下货运量")}
KNOWN_SHEETS_ANY_BLOCK = ()   # 排名表的当月块由 block_of 处理
 
def is_known_extra_row(sheet, indicator, side):
    if sheet in KNOWN_ROWS and side == "程序":
        for pat in KNOWN_ROWS[sheet]:
            if norm_indicator(indicator) == norm_indicator(pat): return True
    return False
 
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
 
NOTE = """
> **已确认口径(2026-09-28,用户)——以下不计入问题**
> 1. ` 货运` 每市州 6 行 vs 手工 4 行:程序新增「规上+规下货运量(万吨)」「规下货运量(万吨)」各 17 行(共 34 行),属母版设计 → 归入「已知设计(母版新增行)」。
>    因行数不同,第 k 个市州在程序里比手工低 2×(k-1) 行(武汉都从第 11 行起;黄石 程序 17 / 手工 15;神农架 程序 107 / 手工 75)——**纯位置偏移,按"地区+指标"对齐后数值一致**。
> 2. ` 货运!U5:U112`(无表头辅助列)是人工草稿 → 归入「已知设计(无表头草稿列)」。
> 3. 各排名表「当月块」是当初做母版时就加上的设计 → 归入「已知设计(排名表当月/累计块)」。
> 4. 市州名短名/全名(武汉 vs 武汉市)不影响 → 已做归一化。
> 5. 取整尾差、文本型数字只在汇总里计数,不进「实质数值差异」清单。
>
> **仍需处理/核对**
> - `班线包车` 3 格:`EO36`/`FN36`/`FO36`(荆门 客运量 2020 累计/累计同比)——**程序生成时重算为 -2731.41 / -1.0008 / -1.0002**,母版与手工定稿均为 0.181777 / 2312.2199 / -0.586419;疑母版 2020 年块分母为 0,需核对公式与数据。
> - ` 货运` 10 条 2022 年各市州「累计同比」:业务已确认**软件口径正确**(手工那列错)。
> - 手工侧待改:304 格草稿列、286 格文本型数字、4 处脏数据(`班线包车!IL4/IL5` 断链、`IL50` 反引号、` 货运!EA93/EA111` 的 `#DIV/0!`)。
> - 母版/程序自身:11 个 `#REF!` + 356 个 `#DIV/0!`(历史遗留)、`城市客运` 第 2 行第 3 个月起表头为日期序列号、缺 2026-09 母版、导出前基数体检未做。
 
"""
 
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: continue
            if cl_g[c] == "(无表头列)":
                bump("已知设计(无表头草稿列)"); continue
            bump("仅程序有(列)"); rows_out.append([sh, "%s*" % get_column_letter(c+1), "(整列)", cl_g[c], "仅程序有(列)", "", "", "生成件多出此列"])
        for c in unmatched_cols_d:
            if c < 2: continue
            if cl_d[c] == "(无表头列)":
                bump("已知设计(无表头草稿列)"); continue
            bump("仅手工有(列)"); rows_out.append([sh, "(无)", "(整列)", cl_d[c], "仅手工有(列)", "", "", "手工定稿多出此列"])
        for r in unmatched_rows_g:
            if r < 4: continue
            rowb = "%s" % wg.cell(row=r+1, column=2).value
            if is_known_extra_row(sh, rowb, "程序"):
                bump("已知设计(母版新增行)"); continue
            bump("仅程序有(行)"); rows_out.append([sh, "第%d行" % (r+1), rl_g[r], "(整行)", "仅程序有(行)", "", "", "生成件多出此区段"])
        for r in unmatched_rows_d:
            if r < 4: continue
            if "当月块" in rl_d[r] or "累计块" in rl_d[r]:
                bump("已知设计(排名表当月/累计块)"); continue
            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])