From 8bcd59732179e920733dee79fc917aa513f6a37c Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期四, 17 九月 2026 16:42:07 +0800
Subject: [PATCH] chore(git): 根目录临时文件归档至 _tmp/ 并加固 .gitignore(防真实数据误提交),工作日结补记

---
 docs/投资/工具/匹配与校验.py |  609 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 609 insertions(+), 0 deletions(-)

diff --git "a/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py" "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py"
new file mode 100644
index 0000000..80f980c
--- /dev/null
+++ "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py"
@@ -0,0 +1,609 @@
+# -*- coding: utf-8 -*-
+r"""鍖归厤涓庢牎楠�.py 鈥斺�� Step1 鐨勫啓鍏ュ墠涓�姝ワ細璇汇�岃В鏋愮粨鏋� + 鐩爣鏄庣粏琛� + 鎶曡祫绯荤粺鏌ヨ缁撴灉銆嶏紝浜у嚭鍙樻洿娓呭崟涓庢牎楠屾姤鍛婏紙鍙锛�
+
+鐢ㄦ硶锛�
+    python docs\鎶曡祫\宸ュ叿\鍖归厤涓庢牎楠�.py --month 2026-07
+
+杈撳叆锛�
+    1) docs\鎶曡祫\杈撳嚭\瑙f瀽缁撴灉_<YYYY-MM>.tsv        鐢� 甯傚窞鏈堟姤瑙f瀽.py 鐢熸垚锛堝競宸炴姤涓婃潵鐨勯」鐩锛�
+    2) docs\鎶曡祫\杈撳嚭\X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx       鐩爣琛紙瀹㈣繍锛宻heet銆屾槑缁嗐�嶏級
+       docs\鎶曡祫\杈撳嚭\X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls        鐩爣琛紙鐗╂祦锛宻heet銆� 鍒嗛」鐩姇璧勫畬鎴愭儏鍐点�嶏級
+    3) docs\鎶曡祫\杈撳叆\妯℃澘_鏌ヨ缁撴灉(鎶曡祫绯荤粺-瀹㈣繍+鐗╂祦 YYYY.M).xls
+       鈥斺�� 鏈湡閭d唤锛堟潈濞佸鐓э級锛涘彟鎸夌敤鎴峰彛寰� (a) 鎵�**涓婃湀**閭d唤鍋氭牎楠屽熀鏁�
+
+杈撳嚭锛�
+    docs\鎶曡祫\杈撳嚭\鍙樻洿娓呭崟_<YYYY-MM>.tsv     缁欍�屽啓鍏ヤ晶銆嶇敤鐨勯�愯鍙樻洿锛堝姩浣�/鐩爣琛屽彿/6 鍒楁柊鍊硷級
+    docs\鎶曡祫\杈撳嚭\鍖归厤鏍¢獙鎶ュ憡_<YYYY-MM>.md  浜鸿锛氬尮閰嶇粺璁°�佸悕绉板涓嶄笂娓呭崟銆佷笁鏂规暟鍊煎樊寮傘�佹湀搴︽牎楠�
+"""
+import sys, os, io, re, argparse, importlib.util, difflib
+
+_here = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.abspath(os.path.join(_here, '..', '..', '..'))
+for _b in (_here, _root, os.getcwd()):
+    _lib = os.path.abspath(os.path.join(_b, '_pylibs'))
+    if os.path.isdir(_lib) and _lib not in sys.path:
+        sys.path.insert(0, _lib)
+_spec = importlib.util.spec_from_file_location('_fmt', os.path.join(_here, '鏍煎紡鎺㈡祴.py'))
+F = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(F)
+
+TARGETS = {
+    '瀹㈣繍': dict(path=os.path.join('docs', '鎶曡祫', '杈撳嚭', 'X鏈堝杩愮珯鎶曡祫鏄庣粏琛�.xlsx'),
+               sheet='鏄庣粏', kind='xlsx', name_col='B', city_col='A',
+               cols=dict(椤圭洰鍚嶇О='B', 寤鸿鍗曚綅='C', 寤鸿鎬ц川='D', 寮�宸ユ椂闂�='E', 绔e伐鏃堕棿='F',
+                         鎬绘姇璧�='G', 鑷紑濮嬪缓璁剧疮璁�='H', 鏈勾璁″垝鎶曡祫='I', 鑷勾鍒濈疮璁�='J',
+                         鏈湀瀹屾垚='K', 寤鸿闃舵='L', 褰㈣薄杩涘害='M')),
+    '鐗╂祦': dict(path=os.path.join('docs', '鎶曡祫', '杈撳嚭', 'X鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls'),
+               sheet=' 鍒嗛」鐩姇璧勫畬鎴愭儏鍐�', kind='xls', name_col='B', city_col='A',
+               cols=dict(椤圭洰鍚嶇О='B', 寤鸿鍗曚綅='C', 寤鸿鎬ц川='D', 寮�宸ユ椂闂�='E', 绔e伐鏃堕棿='F',
+                         鎬绘姇璧�='G', 鑷紑濮嬪缓璁剧疮璁�='H', 鏈勾璁″垝鎶曡祫='I', 鑷勾鍒濈疮璁�='J',
+                         鏈湀瀹屾垚='K', 寤鸿闃舵='L', 褰㈣薄杩涘害='M')),
+}
+WANT = ['鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚', '寤鸿闃舵', '褰㈣薄杩涘害']
+NUMF = ['鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚']
+CITY17 = ('姝︽眽', '榛勭煶', '鍗佸牥', '瀹滄槍', '瑗勯槼', '閯傚窞', '鑽嗛棬', '瀛濇劅', '鑽嗗窞', '榛勫唸',
+          '鍜稿畞', '闅忓窞', '鎭╂柦', '浠欐', '娼滄睙', '澶╅棬', '绁炲啘鏋�')
+GROUP_PAT = re.compile(r'^(涓�|浜寍涓墊鍥泑浜攟鍏瓅涓億鍏�)銆�')
+SKIP_PAT = re.compile(r'^(灏廫s*璁鍚圽s*璁鎬籠s*璁�)$')
+
+
+def normname(s, loose=False):
+    """瑙勮寖鍖栭」鐩悕绉扮敤浜庡尮閰嶃��
+
+    loose=False锛氬彧缁熶竴鍏ㄨ/鍗婅銆佸幓绌虹櫧锛堜繚鐣欐嫭鍙峰唴淇℃伅锛夆�斺�旂簿纭尮閰嶇敤锛�
+    loose=True 锛氬啀鍘绘帀鎵�鏈夋嫭鍙峰瓧绗︿笌缁撳熬鐨勩�岄」鐩�嶄簩瀛椻�斺�斿垽鏂�岀枒浼煎悓涓�椤圭洰銆嶇敤
+                锛堜緥锛氶剛婀樿担鍟嗚锤鐗╂祦涓績涓�鏈� 鈬� 閯傛箻璧e晢璐哥墿娴佷腑蹇冿紙涓�鏈燂級銆�
+                      鑽嗛棬鏅烘収鍐烽摼鐗╂祦鍥� 鈬� 鑽嗛棬鏅烘収鍐烽摼鐗╂祦鍥」鐩級
+    """
+    s = str(s or '')
+    for a, b in (('锛�', '('), ('锛�', ')'), ('銆�', '['), ('銆�', ']'), ('锛�', ','), ('銆�', ','),
+                 ('锛�', ':'), ('銆�', ''), (' ', ''), ('\u3000', '')):
+        s = s.replace(a, b)
+    s = s.strip().upper()
+    if loose:
+        for ch in '()[]':
+            s = s.replace(ch, '')
+        while s.endswith('椤圭洰'):
+            s = s[:-2]
+        s = s.rstrip('.銆�-')
+    return s
+
+
+def normname2(s):
+    """鎶曡祫绯荤粺姣斿涓撶敤锛氬湪 normname 鍩虹涓婂啀鍘绘帀闂撮殧鍙风瓑绾楗板瓧绗︺��"""
+    for ch in ('路', '锛�', '鈥�', '鈻�', '鈼�', '.', '-'):
+        s = str(s or '').replace(ch, '')
+    return normname(s)
+
+
+def to_num(v):
+    if isinstance(v, (int, float)) and not isinstance(v, bool):
+        return float(v)
+    s = str(v or '').replace(',', '').strip()
+    try:
+        return float(s)
+    except ValueError:
+        return None
+
+
+def pretty(v):
+    if v is None:
+        return ''
+    if isinstance(v, float) and v.is_integer():
+        return str(int(v))
+    return ('%.4f' % v).rstrip('0').rstrip('.') if isinstance(v, float) else str(v)
+
+
+def read_target(spec):
+    """璇荤洰鏍囨槑缁嗚〃 鈫� (琛屽垪琛�, 鍒椾綅鏄犲皠, 鍒椾綅鍛婅)銆�
+
+    鍒椾綅**鎸夎〃澶村畾浣�**锛堟瘝鐗堥粯璁ゅ垪浣嶅彧浣滃厹搴曪級锛涗笌姣嶇増榛樿涓嶄竴鑷存椂杩斿洖鍛婅锛岀敱璋冪敤鏂瑰脊绐椼��
+    """
+    from openpyxl import load_workbook
+    import xlrd
+    rows, notes, infos, merges = [], [], [], []
+    KEYF = ('椤圭洰鍚嶇О', '鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚')
+    if spec['kind'] == 'xlsx':
+        wb = load_workbook(spec['path'], data_only=True)
+        ws = wb[spec['sheet']]
+        def cells(r, c):
+            return ws.cell(r, c).value
+        nrows, ncols = ws.max_row, ws.max_column
+        merges = [(r.min_row - 1, r.max_row, r.min_col - 1, r.max_col)
+                  for r in ws.merged_cells.ranges]
+    else:
+        try:
+            bk = xlrd.open_workbook(spec['path'], formatting_info=True)
+        except Exception:
+            bk = xlrd.open_workbook(spec['path'])
+        sh = bk.sheet_by_name(spec['sheet'])
+        def cells(r, c):
+            return sh.cell_value(r - 1, c - 1) if (r - 1 < sh.nrows and c - 1 < sh.ncols) else ''
+        nrows, ncols = sh.nrows, sh.ncols
+        merges = list(getattr(sh, 'merged_cells', []) or [])
+
+    # 1) 鎸夎〃澶村畾浣嶅垪锛堜袱绾ц〃澶� / 鍚屽悕鍒楅兘鍦� pick_columns 閲屾秷姝э級
+    cols = {}
+    try:
+        _c, _h = F.pick_columns(lambda r, c: cells(r + 1, c + 1), nrows, ncols, merges)
+        for k in spec['cols']:
+            if k in _c:
+                cols[k] = F.colname(_c[k])
+    except Exception as e:
+        notes.append('琛ㄥご瀹氫綅寮傚父(%s: %s)锛屽叏閮ㄩ��鍥炴瘝鐗堥粯璁ゅ垪浣�' % (type(e).__name__, str(e)[:40]))
+    # 2) 鏈壘鍒扮殑閫�鍥炴瘝鐗堥粯璁ゅ垪浣嶏紱涓庨粯璁や笉涓�鑷寸殑璁板綍鍛婅锛堝叧閿垪锛�/鎻愮ず锛堝叾浣欏垪锛�
+    for k, exp in spec['cols'].items():
+        got = cols.get(k)
+        if got is None:
+            msg = '%s锛氳〃澶存湭鎵惧埌锛屾寜姣嶇増榛樿 %s 鍒�' % (k, exp)
+            (notes if k in KEYF else infos).append(msg)
+            cols[k] = exp
+        elif got != exp:
+            msg = '%s锛氳〃澶村畾浣� %s 鍒楋紝姣嶇増榛樿 %s 鍒�' % (k, got, exp)
+            (notes if k in KEYF else infos).append(msg)
+
+    namec = F.colindex(cols['椤圭洰鍚嶇О']) + 1
+    anchors = [F.colindex(cols[k]) + 1 for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鎬绘姇璧�') if k in cols]
+    for r in range(1, nrows + 1):
+        name = str(cells(r, namec) or '').strip()
+        a = str(cells(r, 1) or '').strip()
+        if not name or len(name) < 3:
+            continue
+        if GROUP_PAT.match(F.norm(a) + F.norm(name)) or SKIP_PAT.match(F.norm(name)):
+            continue
+        if any(name.startswith(x) for x in F.BAD_NAME):
+            continue
+        if not any(to_num(cells(r, c)) is not None for c in anchors):
+            continue
+        rec = {'鐩爣琛屽彿': r, '甯傚窞鎴栬棣�': a}
+        for k in spec['cols']:
+            v = cells(r, F.colindex(cols[k]) + 1)
+            rec[k] = pretty(to_num(v)) if k in NUMF else str(v or '').strip()
+        rows.append(rec)
+    return rows, cols, notes, infos
+
+
+def read_system(path):
+    """璇绘姇璧勭郴缁熸煡璇㈢粨鏋� 鈫� {normname: {椤圭洰鍚嶇О, 璁″垝鎬绘姇璧�, 鑷紑濮嬪缓璁剧疮璁�, 鏈勾璁″垝鎶曡祫, 鑷勾鍒濈疮璁�, 鏈湀瀹屾垚, 鏃舵湡}}"""
+    import xlrd
+    bk = xlrd.open_workbook(path)
+    sh = bk.sheet_by_index(0)
+    out, period = {}, ''
+    for r in range(1, sh.nrows):
+        name = str(sh.cell_value(r, 1)).strip()
+        if not name or name in ('鍗曚綅',):
+            continue
+        period = period or str(sh.cell_value(r, 2)).strip()
+        out[normname(name)] = {
+            '椤圭洰鍚嶇О': name, '鏃舵湡': str(sh.cell_value(r, 2)).strip(),
+            '鎬绘姇璧�': to_num(sh.cell_value(r, 3)),
+            '鑷紑濮嬪缓璁剧疮璁�': to_num(sh.cell_value(r, 4)),
+            '鏈勾璁″垝鎶曡祫': to_num(sh.cell_value(r, 5)),
+            '鑷勾鍒濈疮璁�': to_num(sh.cell_value(r, 6)),
+            '鏈湀瀹屾垚': to_num(sh.cell_value(r, 7)),
+        }
+    return out, period
+
+
+def normname3(s):
+    """鍐嶅幓鎺夋嫭鍙峰強鍏跺唴瀹癸紙鐢ㄤ簬璇嗗埆銆屽悕绉板浜嗘嫭娉ㄣ�嶇殑鍚屼竴椤圭洰锛夈��"""
+    t = normname(s)
+    t = re.sub(r'\([^)]*\)', '', t)
+    for ch in ('路', '锛�', '鈥�', '鈼�', '-', '.', '路'):
+        t = t.replace(ch, '')
+    return t
+
+
+def best_name_hint(key, tkeys, cutoff=0.80):
+    """鍦ㄧ洰鏍囪〃椤圭洰鍚嶉噷鎵句笌 key 鏈�鍍忕殑涓�涓紙瀹芥澗/鍘绘嫭娉ㄤ袱绉嶅彛寰勫彇鏈�浼橈級銆�
+
+    杩斿洖 (鐩爣琛ㄩ」鐩悕, 鐩镐技搴�)锛涙壘涓嶅埌杩斿洖 (None, 0)銆�
+    """
+    best = (None, 0.0)
+    for normf in (lambda s: normname(s, loose=True), normname3):
+        base = normf(key)
+        table = [normf(x) for x in tkeys]
+        hit = difflib.get_close_matches(base, table, n=1, cutoff=cutoff)
+        if hit:
+            ratio = difflib.SequenceMatcher(None, base, hit[0]).ratio()
+            if ratio > best[1]:
+                best = (tkeys[table.index(hit[0])], ratio)
+    return best
+
+
+def sys_lookup(key, table):
+    """鍦ㄦ姇璧勭郴缁熷瓧鍏搁噷鎵鹃」鐩細鍏堢簿纭紙鍚幓闂撮殧鍙凤級锛屽啀瀹芥澗+鐩镐技搴︼紙>=0.90锛夈��
+
+    杩斿洖 (璁板綍, 璇存槑)锛涙壘涓嶅埌杩斿洖 (None, '')銆�
+    """
+    if not table:
+        return None, ''
+    s = table.get(key)
+    if s:
+        return s, ''
+    for kk, vv in table.items():
+        if normname2(kk) == normname2(key):
+            return vv, '鍚嶇О鍐欐硶涓嶅悓锛堟姇璧勭郴缁燂細%s锛�' % vv['椤圭洰鍚嶇О']
+    idx = {}
+    for kk in table:
+        idx.setdefault(normname2(kk), []).append(kk)
+    cands = difflib.get_close_matches(normname2(key), list(idx.keys()), n=1, cutoff=0.90)
+    if cands:
+        kk = idx[cands[0]][0]
+        return table[kk], '鍚嶇О鍐欐硶涓嶅悓锛堟姇璧勭郴缁燂細%s锛�' % table[kk]['椤圭洰鍚嶇О']
+    return None, ''
+
+
+def find_system_files(indir, ym):
+    """鍦ㄨ緭鍏ョ洰褰曢噷鎵俱�屾湰鏈�/涓婃湀銆嶆姇璧勭郴缁熸煡璇㈢粨鏋溿�傛枃浠跺悕鎴栥�庢椂鏈熴�忓垪閲岀殑骞存湀閮借銆�"""
+    found = {}
+    if not os.path.isdir(indir):
+        return found
+    for f in os.listdir(indir):
+        if not f.lower().endswith(('.xls', '.xlsx')) or '鏌ヨ缁撴灉' not in f:
+            continue
+        m = re.search(r'(20\d\d)\s*[-.骞碷\s*(\d{1,2})', f)
+        key = '%s%02d' % (m.group(1), int(m.group(2))) if m else None
+        if key is None:
+            try:
+                _d, period = read_system(os.path.join(indir, f))
+                m2 = re.search(r'(20\d\d)\D+(\d{1,2})', period or '')
+                key = '%s%02d' % (m2.group(1), int(m2.group(2))) if m2 else None
+            except Exception:
+                key = None
+        if key:
+            found[key] = os.path.join(indir, f)
+    return found
+
+
+def prev_ym(ym):
+    y, m = int(ym[:4]), int(ym[4:])
+    return '%d%02d' % (y - 1, 12) if m == 1 else '%d%02d' % (y, m - 1)
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--month', required=True)
+    ap.add_argument('--root', default=os.path.join('docs', '鎶曡祫'))
+    a = ap.parse_args()
+    ym = a.month.replace('-', '')
+    out = os.path.join(a.root, '杈撳嚭')
+    tsv_in = os.path.join(out, '瑙f瀽缁撴灉_%s.tsv' % a.month)
+    if not os.path.exists(tsv_in):
+        print('缂哄皯瑙f瀽缁撴灉锛�%s锛堝厛璺� 甯傚窞鏈堟姤瑙f瀽.py锛�' % tsv_in); return
+
+    parsed = []
+    for line in io.open(tsv_in, encoding='utf-8-sig').read().splitlines()[1:]:
+        p = line.split('\t')
+        if len(p) < 8:
+            continue
+        parsed.append(dict(zip(['鍝佺被', '甯傚窞', '鏂囦欢', '宸ヤ綔琛�', '琛屽彿', '搴忓彿', '鍒嗙被'] +
+                               ['椤圭洰鍚嶇О', '寤鸿鍗曚綅', '寤鸿鎬ц川', '寮�宸ユ椂闂�', '绔e伐鏃堕棿', '鎬绘姇璧�',
+                                '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚',
+                                '寤鸿闃舵', '褰㈣薄杩涘害'], p)))
+    rep = ['# 鍖归厤鏍¢獙鎶ュ憡锛�%s锛�' % a.month, '']
+    rep.append('> 鍙锛氭湰姝ラ涓嶆敼浠讳綍鏂囦欢锛涜緭鍑虹殑銆婂彉鏇存竻鍗曘�嬩緵銆屽啓鍏ヤ晶銆嶆墽琛屻��')
+    rep.append('')
+    changes = []
+    alerts = []
+
+    def add_alert(level, kind, cat, city, name, src, detail, action):
+        alerts.append(dict(绾у埆=level, 绫诲瀷=kind, 鍝佺被=cat, 甯傚窞=city, 椤圭洰鍚嶇О=name,
+                           鏉ユ簮=src, 璇︽儏=detail, 寤鸿鍔ㄤ綔=action))
+
+    # 婧愭枃浠惰В鏋愭牎楠岋紙鐢� 甯傚窞鏈堟姤瑙f瀽.py 浜у嚭锛�
+    checks = []
+    p_ck = os.path.join(out, '瑙f瀽鏍¢獙_%s.tsv' % a.month)
+    if os.path.exists(p_ck):
+        _lines = io.open(p_ck, encoding='utf-8-sig').read().splitlines()
+        if _lines:
+            _ckc = _lines[0].split('\t')
+            for _ln in _lines[1:]:
+                _v = _ln.split('\t')
+                _v += [''] * (len(_ckc) - len(_v))
+                checks.append(dict(zip(_ckc, _v)))
+
+    # 鎶曡祫绯荤粺鏂囦欢锛堟湰鏈� / 涓婃湀锛�
+    indir = os.path.join(a.root, '杈撳叆')
+    sysfiles = find_system_files(indir, ym)
+    cur_path = sysfiles.get(ym)
+    pre_path = sysfiles.get(prev_ym(ym))
+    cur_sys = cur_pre = {}
+    if cur_path:
+        cur_sys, _ = read_system(cur_path)
+    if pre_path:
+        cur_pre, _ = read_system(pre_path)
+    rep.append('## 涓�銆佹姇璧勭郴缁熸暟鎹紙鏈湡 / 涓婃湀锛�')
+    rep.append('')
+    rep.append('| 鐢ㄩ�� | 鏂囦欢 | 椤圭洰鏁� |')
+    rep.append('| --- | --- | --- |')
+    rep.append('| 鏈湡锛堟潈濞佸鐓э級 | %s | %d |' % (os.path.basename(cur_path) if cur_path else '**鏈壘鍒�**', len(cur_sys)))
+    rep.append('| 涓婃湀锛堟牎楠屽熀鏁帮級 | %s | %d |' % (os.path.basename(pre_path) if pre_path else '**鏈壘鍒帮紙鐢ㄦ埛鍙e緞 a锛氶渶姣忔湀鏀句竴浠斤級**', len(cur_pre)))
+    rep.append('')
+
+    # 鈥斺�� 寮圭獥瑙勫垯锛氭姇璧勭郴缁熸枃浠剁己澶� / 婧愭枃浠惰В鏋愬紓甯� / 婧愭枃浠跺唴閮ㄥ涓嶄笂 鈥斺��
+    if not cur_path:
+        add_alert('闃绘柇', '缂烘湰鏈熸姇璧勭郴缁熻〃', '', '', '', os.path.basename(indir),
+                  '鏈湪銆岃緭鍏ャ�嶇洰褰曟壘鍒� %s 鐨勬姇璧勭郴缁熸煡璇㈢粨鏋�' % a.month,
+                  '鎶婃湰鏈熸姇璧勭郴缁熷鍑虹殑銆婃ā鏉縚鏌ヨ缁撴灉銆嬫斁杩� docs\\鎶曡祫\\杈撳叆\\ 鍚庨噸璺�')
+    if not pre_path:
+        add_alert('璀﹀憡', '缂轰笂鏈堟姇璧勭郴缁熻〃', '', '', '', os.path.basename(indir),
+                  '鏈壘鍒� %s 鐨勬姇璧勭郴缁熸煡璇㈢粨鏋� 鈫� 鏈堝害鏍¢獙锛堟湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚锛夋棤娉曟墽琛�' % prev_ym(a.month),
+                  '姣忔湀鎶婁笂鏈堛�佹湰鏈堜袱浠芥姇璧勭郴缁熸煡璇㈢粨鏋滈兘鏀捐繘 docs\\鎶曡祫\\杈撳叆\\')
+    if not checks:
+        add_alert('闃绘柇', '缂烘簮鏂囦欢瑙f瀽鏍¢獙', '', '', '', os.path.basename(p_ck),
+                  '鏈壘鍒般�婅В鏋愭牎楠屻�嬫竻鍗曪紝鏃犳硶鍒ゆ柇婧愭枃浠舵槸鍚﹁嚜娲姐�佹槸鍚﹁В鏋愬け璐�',
+                  '鍏堣窇 甯傚窞鏈堟姤瑙f瀽.py --month %s' % a.month)
+    for ck in checks:
+        st = ck.get('鐘舵��', 'OK')
+        if st and st != 'OK':
+            add_alert('闃绘柇' if st.startswith('瑙f瀽澶辫触') else '璀﹀憡', '婧愭枃浠惰В鏋愬紓甯�',
+                      ck.get('鍝佺被', ''), ck.get('甯傚窞', ''), '', ck.get('鏂囦欢', ''),
+                      st, '浜哄伐鎵撳紑婧愭枃浠剁‘璁よ〃澶�/鏁版嵁锛涘繀瑕佹椂鐧昏鐗逛緥鍚庡啀瀵煎叆')
+        elif ck.get('鍒椾綅') and not all(k in ck['鍒椾綅'] for k in ('鑷紑濮嬪缓璁剧疮璁�=', '鑷勾鍒濈疮璁�=', '鏈湀瀹屾垚=')):
+            _miss = [k for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚') if k + '=' not in ck['鍒椾綅']]
+            add_alert('璀﹀憡', '婧愭枃浠剁己鍏抽敭鍒�', ck.get('鍝佺被', ''), ck.get('甯傚窞', ''), '', ck.get('鏂囦欢', ''),
+                      '鎸夎〃澶存病鎵惧埌鍒楋細%s锛堝疄闄呰瘑鍒埌鐨勫垪浣嶏細%s锛�' % ('銆�'.join(_miss), ck['鍒椾綅']),
+                      '浜哄伐纭婧愭枃浠惰〃澶存槸鍚﹁鏀硅繃锛涘繀瑕佹椂鐧昏涓虹壒渚�')
+        elif ck.get('涓�鑷存��', '') == '鈿狅笍 涓嶄竴鑷�':
+            add_alert('璀﹀憡', '婧愭枃浠跺唴閮ㄥ涓嶄笂', ck.get('鍝佺被', ''), ck.get('甯傚窞', ''), '', ck.get('鏂囦欢', ''),
+                      '椤圭洰琛屾眰鍜� %s/%s/%s 鈮� %s 琛屽悎璁� %s/%s/%s' % (
+                          ck.get('姹傚拰_鑷紑濮嬪缓璁剧疮璁�', ''), ck.get('姹傚拰_鑷勾鍒濈疮璁�', ''), ck.get('姹傚拰_鏈湀瀹屾垚', ''),
+                          ck.get('鍚堣琛�', ''), ck.get('鍚堣_鑷紑濮嬪缓璁剧疮璁�', ''),
+                          ck.get('鍚堣_鑷勾鍒濈疮璁�', ''), ck.get('鍚堣_鏈湀瀹屾垚', '')),
+                      '璁╁競宸炴牳瀵规簮鏂囦欢锛堝悎璁¤鎴栭」鐩鏈夐敊锛夛紝纭鍚庡啀瀵煎叆')
+
+    for cat, spec in TARGETS.items():
+        rep.append('## %s 鈥斺�� 鐩爣鏄庣粏琛�' % cat)
+        rep.append('')
+        if not os.path.exists(spec['path']):
+            rep.append('鐩爣琛ㄤ笉瀛樺湪锛�%s' % spec['path']); rep.append('')
+            add_alert('闃绘柇', '缂虹洰鏍囨槑缁嗚〃', cat, '', '', spec['path'],
+                      '鐩爣鏄庣粏琛ㄤ笉瀛樺湪锛屾湰瀛e害璇ュ搧绫绘棤娉曞啓鍏�', '鎶婃ā鏉胯〃鏀惧埌 docs\\鎶曡祫\\杈撳嚭\\')
+            continue
+        tgt, tcols, tnotes, tinfos = read_target(spec)
+        if tnotes:
+            add_alert('璀﹀憡', '鐩爣琛ㄥ垪浣嶇疆涓庨鏈熶笉绗�', cat, '', '', os.path.basename(spec['path']),
+                      '锛�'.join(tnotes[:8]),
+                      '纭姣嶇増琛ㄥご鏄惁琚敼鍔紱绋嬪簭宸叉寜琛ㄥご瀹氫綅锛屼絾璇蜂汉宸ユ牳瀵瑰悗鍐嶅啓鍏�')
+        if tinfos:
+            add_alert('鎻愮ず', '鐩爣琛ㄤ釜鍒垪鎸夋瘝鐗堥粯璁�', cat, '', '', os.path.basename(spec['path']),
+                      '锛�'.join(tinfos[:8]),
+                      '鐭ユ倝鍗冲彲锛堣繖浜涘垪鍦ㄦ瘝鐗堥噷鏈潵灏辨病鏈夊瓙琛ㄥご锛屾寜榛樿鍒椾綅鍐欏叆锛�')
+        prow = [r for r in parsed if r['鍝佺被'] == cat]
+        tmap = {}
+        for t in tgt:
+            tmap.setdefault(normname(t['椤圭洰鍚嶇О']), []).append(t)
+        pmap = {}
+        for p in prow:
+            pmap.setdefault(normname(p['椤圭洰鍚嶇О']), []).append(p)
+
+        matched = [k for k in tmap if k in pmap]
+        raw_new = [k for k in pmap if k not in tmap]             # 婧愭枃浠舵湁銆佺洰鏍囪〃娌℃湁
+        only_tgt = [k for k in tmap if k not in pmap]            # 鐩爣琛ㄦ湁銆佹簮鏂囦欢娌℃湁 鈫� 淇濇寔鍘熷��
+        # 鏂板鍊欓�夛細鍏堢湅鏄惁涓庣洰鏍囪〃鏌愪釜椤圭洰銆屽鏉剧瓑浠枫�嶆垨楂樺害鐩镐技 鈫� 鐤戜技鍚屼竴椤圭洰锛屼笉鏂板锛屾敼涓哄脊绐楁彁绀�
+        loose_tgt = {}
+        for k in tmap:
+            loose_tgt.setdefault(normname(k, loose=True), []).append(k)
+        plain_tgt = {}
+        for k in tmap:
+            plain_tgt.setdefault(normname3(k), []).append(k)
+        tkeys = list(tmap.keys())
+        new_in_src, suspicious = [], []
+        for k in raw_new:
+            nl = normname(k, loose=True)
+            if nl in loose_tgt:
+                suspicious.append((k, loose_tgt[nl][0], '瀹芥澗绛変环锛堝幓鎷彿/鍘烩�滈」鐩�濅簩瀛楀悗鐩稿悓锛�'))
+                continue
+            n3 = normname3(k)
+            if n3 and n3 in plain_tgt:
+                suspicious.append((k, plain_tgt[n3][0], '鍘绘帀鎷敞涓庨棿闅斿彿鍚庣浉鍚�'))
+                continue
+            close = difflib.get_close_matches(nl, [normname(x, loose=True) for x in tkeys], n=1, cutoff=0.90)
+            if close:
+                hit = tkeys[[normname(x, loose=True) for x in tkeys].index(close[0])]
+                suspicious.append((k, hit, '楂樺害鐩镐技 %.2f' % difflib.SequenceMatcher(None, nl, close[0]).ratio()))
+                continue
+            new_in_src.append(k)
+        rep.append('- 鐩爣琛ㄩ」鐩 **%d**锛涘競宸炴湀鎶ヨВ鏋愬嚭 **%d** 琛�' % (len(tgt), len(prow)))
+        rep.append('- 鎸夐」鐩悕绉�**绮剧‘鍖归厤**涓� **%d**锛堚啋 鏇存柊 6 鍒楋級' % len(matched))
+        rep.append('- **鐪熸闇�瑕佹柊澧�** **%d**' % len(new_in_src))
+        rep.append('- **鐤戜技鍚屼竴椤圭洰銆侀渶浜哄伐纭** **%d**锛堟寜鐢ㄦ埛鍙e緞锛�**涓嶈嚜鍔ㄦ柊澧炪�佷笉鍔ㄥ悕绉帮紝寮圭獥鎻愮ず**锛�' % len(suspicious))
+        rep.append('- 鐩爣琛ㄦ湁浣嗘湰鏈熸簮鏂囦欢娌℃湁锛堜繚鎸佸師鍊硷級**%d**' % len(only_tgt))
+        rep.append('')
+        for k1, hit1, why1 in suspicious:
+            _p1 = pmap[k1][0]
+            add_alert('璀﹀憡', '鐤戜技鍚屼竴椤圭洰锛堝悕绉板啓娉曚笉鍚岋級', cat, _p1.get('甯傚窞', ''), _p1['椤圭洰鍚嶇О'],
+                      os.path.basename(_p1['鏂囦欢']),
+                      '涓庣洰鏍囪〃宸叉湁椤圭洰銆�%s銆�%s' % (tmap[hit1][0]['椤圭洰鍚嶇О'], why1),
+                      '浜哄伐纭鏄惁鍚屼竴椤圭洰锛氭槸 鈫� 涓嶆敼鍚嶇О銆佹寜鎶曡祫绯荤粺鍚嶇О鏇存柊锛涘惁 鈫� 鏁磋鏂板')
+        if suspicious:
+            rep.append('### 鈿狅笍 鐤戜技鍚屼竴椤圭洰浣嗗悕绉板啓娉曚笉鍚� 鈫� 闇�浜哄伐纭锛堢▼搴忎笉鑷姩鍚堝苟銆佷篃涓嶉噸澶嶆柊澧烇級')
+            rep.append('')
+            rep.append('| 婧愭枃浠堕」鐩悕绉� | 鐩爣琛ㄥ凡鏈夐」鐩悕绉� | 鍒ゅ畾渚濇嵁 |')
+            rep.append('| --- | --- | --- |')
+            for a1, b1, why in suspicious:
+                rep.append('| %s | %s | %s |' % (a1, b1, why))
+            rep.append('')
+        if new_in_src:
+            rep.append('### 鉃� 灏嗘柊澧炵殑椤圭洰锛�%d 涓級' % len(new_in_src))
+            rep.append('')
+            rep.append('| 椤圭洰鍚嶇О | 鏉ユ簮鏂囦欢 | 鑷紑濮嬪缓璁剧疮璁� | 鑷勾鍒濈疮璁� | 鏈湀瀹屾垚 | 鍚嶇О鐩镐技搴︽彁绀� |')
+            rep.append('| --- | --- | --- | --- | --- | --- |')
+            for k in new_in_src:
+                p0 = pmap[k][0]
+                _hit, _r = best_name_hint(k, tkeys)
+                hint = ('鈿狅笍 涓庛��%s銆嶇浉浼� %.2f锛岃纭鏄惁鍚屼竴椤圭洰' % (_hit, _r)) if _hit else ''
+                _sim = hint
+                add_alert('鎻愮ず', '灏嗘柊澧為」鐩�', cat, p0.get('甯傚窞', ''), p0['椤圭洰鍚嶇О'],
+                          os.path.basename(p0['鏂囦欢']),
+                          '鐩爣琛ㄦ病鏈夎繖涓」鐩紱鑷紑濮嬪缓璁剧疮璁�/鑷勾鍒濈疮璁�/鏈湀瀹屾垚 = %s/%s/%s%s' % (
+                              p0.get('鑷紑濮嬪缓璁剧疮璁�', ''), p0.get('鑷勾鍒濈疮璁�', ''), p0.get('鏈湀瀹屾垚', ''),
+                              ('锛�' + hint) if hint else ''),
+                          '纭鏄湡瀹炴柊椤圭洰鍚庡啓鍏ワ紙鏁磋鎻掑叆锛屽鍒剁浉閭昏鏍煎紡锛�')
+                rep.append('| %s | %s | %s | %s | %s | %s |' % (
+                    p0['椤圭洰鍚嶇О'], os.path.basename(p0['鏂囦欢'])[:30], p0.get('鑷紑濮嬪缓璁剧疮璁�', ''),
+                    p0.get('鑷勾鍒濈疮璁�', ''), p0.get('鏈湀瀹屾垚', ''), hint))
+            rep.append('')
+
+        # 鍙樻洿娓呭崟
+        for k in matched:
+            for p in pmap[k]:
+                t = tmap[k][0]
+                changes.append({'鍝佺被': cat, '鍔ㄤ綔': '鏇存柊', '鐩爣琛屽彿': t['鐩爣琛屽彿'],
+                                '椤圭洰鍚嶇О': t['椤圭洰鍚嶇О'], '鍐欏叆鏉ユ簮': os.path.basename(p['鏂囦欢']),
+                                **{c: p.get(c, '') for c in WANT}})
+        for k in new_in_src:
+            for p in pmap[k]:
+                changes.append({'鍝佺被': cat, '鍔ㄤ綔': '鏂板', '鐩爣琛屽彿': '',
+                                '椤圭洰鍚嶇О': p['椤圭洰鍚嶇О'], '鍐欏叆鏉ユ簮': os.path.basename(p['鏂囦欢']),
+                                **{c: p.get(c, '') for c in WANT}})
+
+        # 鐩爣琛ㄦ湁銆佹湰鏈熸簮鏂囦欢娌℃湁 鈫� 淇濇寔鍘熷�硷紙鎻愮ず锛屼笉闃绘柇锛�
+        if only_tgt:
+            _names = [tmap[k][0]['椤圭洰鍚嶇О'] for k in only_tgt]
+            add_alert('鎻愮ず', '鏈湡婧愭枃浠堕噷娌℃湁鐨勯」鐩�', cat, '', '', '鈥�',
+                      '鐩爣琛ㄩ噷鏈� %d 涓」鐩湰鏈熷競宸炴湀鎶ユ病鎶ワ細%s%s' % (
+                          len(_names), '銆�'.join(_names[:8]), ' 绛�' if len(_names) > 8 else ''),
+                      '鎸夊彛寰勪繚鎸佷笂鏈堝師鍊硷紱鑻ュ競宸炵‘瀹炲凡鍋滃缓锛岃浜哄伐纭鏄惁淇濈暀')
+
+        # 涓夋柟鏁板�兼瘮瀵癸細婧愭枃浠� vs 鎶曡祫绯荤粺锛堟湰鏈燂級
+        if cur_sys:
+            diff, missing, shifted = [], [], []
+            for k in matched:
+                p, t = pmap[k][0], tmap[k][0]
+                s, note = sys_lookup(k, cur_sys)
+                if not s:
+                    missing.append(t['椤圭洰鍚嶇О'])
+                    continue
+                if note:
+                    add_alert('鎻愮ず', '鎶曡祫绯荤粺鍚嶇О鍐欐硶涓嶅悓', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                              os.path.basename(p['鏂囦欢']), note + '锛涘凡鎸夋姇璧勭郴缁熻繖鏉¤褰曞仛鏁板�煎鐓э紙椤圭洰鍚嶇О涓嶅姩锛�',
+                              '鐭ユ倝鍗冲彲')
+                for c in ('鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+                    pv, sv = to_num(p.get(c)), s.get(c)
+                    if pv is None or sv is None:
+                        continue
+                    if abs(pv - sv) <= 0.05:
+                        continue
+                    diff.append((t['椤圭洰鍚嶇О'], c, pretty(pv), pretty(sv), pretty(sv - pv)))
+                    if abs(pv - sv) > 1.0:
+                        add_alert('璀﹀憡', '婧愭枃浠朵笌鎶曡祫绯荤粺涓嶄竴鑷�', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                                  os.path.basename(p['鏂囦欢']),
+                                  '%s锛氬競宸炴湀鎶� %s 锛� 鎶曡祫绯荤粺 %s锛堝樊 %s锛�' % (c, pretty(pv), pretty(sv), pretty(sv - pv)),
+                                  '浜哄伐纭浠ュ摢涓负鍑嗭紱鑻ユ槸鍙栧垪閿欎綅鍒欎慨瑙f瀽瑙勫垯鍚庨噸璺�')
+                    for c2 in ('鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+                        if c2 == c:
+                            continue
+                        sv2 = s.get(c2)
+                        if sv2 is not None and abs(pv - sv2) <= 0.05:
+                            shifted.append((t['椤圭洰鍚嶇О'], c, c2))
+                            add_alert('璀﹀憡', '鐤戜技鍙栧垪閿欎綅', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                                      os.path.basename(p['鏂囦欢']),
+                                      '甯傚窞鏈堟姤銆�%s銆�= %s锛屾伆濂界瓑浜庢姇璧勭郴缁熴��%s銆�' % (c, pretty(pv), c2),
+                                      '妫�鏌ユ簮鏂囦欢琛ㄥご锛堢粍琛ㄥご鍚堝苟鍗曞厓鏍兼渶瀹规槗閿欎綅锛夛紝淇彇鍒楄鍒欏悗閲嶈窇瑙f瀽')
+            rep.append('')
+            rep.append('### 婧愭枃浠� vs 鎶曡祫绯荤粺锛堟湰鏈燂級锛�**鏁板�间笉涓�鑷� %d 澶�**锛堥渶寮圭獥鎻愮ず锛夛紝鍏朵腑鐤戜技鍙栧垪閿欎綅 **%d** 澶�' % (len(diff), len(shifted)))
+            rep.append('')
+            if diff:
+                rep.append('| 椤圭洰鍚嶇О | 瀛楁 | 甯傚窞鏈堟姤 | 鎶曡祫绯荤粺 | 宸 |')
+                rep.append('| --- | --- | --- | --- | --- |')
+                for nm1, c1, x1, y1, d1 in diff[:60]:
+                    rep.append('| %s | %s | %s | %s | %s |' % (nm1, c1, x1, y1, d1))
+                rep.append('')
+            else:
+                rep.append('锛堟棤锛�'); rep.append('')
+            rep.append('### 鎶曡祫绯荤粺涓煡涓嶅埌鐨勯」鐩細**%d** 涓紙涓嶈涓哄樊閿欙紱鎶曡祫绯荤粺鍙兘鍙敹褰曠渷閲嶇偣椤圭洰锛�' % len(missing))
+            rep.append('')
+            for nm1 in missing[:60]:
+                rep.append('- %s' % nm1)
+            rep.append('')
+            for nm1 in missing:
+                add_alert('鎻愮ず', '鎶曡祫绯荤粺鏌ユ棤姝ら」鐩�', cat, '', nm1, '鈥�',
+                          '鎶曡祫绯荤粺鏈湡鏌ヨ缁撴灉閲屾病鏈夎繖涓」鐩悕绉�', '鐭ユ倝鍗冲彲锛涘闇�浠ユ姇璧勭郴缁熶负鍑嗭紝璇蜂汉宸ユ牳瀵瑰悕绉�')
+
+        # 鏈堝害鏍¢獙锛氭湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚
+        if cur_pre:
+            bad = []
+            for k in matched:
+                p, t = pmap[k][0], tmap[k][0]
+                nowj = to_num(p.get('鑷勾鍒濈疮璁�'))
+                nowk = to_num(p.get('鏈湀瀹屾垚'))
+                pre, _note = sys_lookup(k, cur_pre)
+                if nowj is None or nowk is None or not pre:
+                    continue
+                prej = pre.get('鑷勾鍒濈疮璁�')
+                if prej is None:
+                    continue
+                if abs(nowj - (prej + nowk)) > 0.05:
+                    bad.append((t['椤圭洰鍚嶇О'], pretty(prej), pretty(nowk), pretty(nowj), pretty(prej + nowk)))
+                    add_alert('璀﹀憡', '鏈堝害绛夊紡涓嶇', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                              '%s + %s = %s锛岃�屾湰鏈堣嚜骞村垵绱鏄� %s锛堝樊 %s锛�' % (
+                                  pretty(prej), pretty(nowk), pretty(prej + nowk), pretty(nowj),
+                                  pretty(nowj - prej - nowk)),
+                              '鍐欏叆鏃舵寜绛夊紡绾犳涓� %s锛屽苟璁╁競宸炵‘璁�' % pretty(prej + nowk))
+            rep.append('### 鏈堝害鏍¢獙銆屾湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚銆嶏細涓嶇 **%d** 椤�' % len(bad))
+            rep.append('')
+            if bad:
+                rep.append('| 椤圭洰鍚嶇О | 涓婃湀鑷勾鍒濈疮璁� | 鏈湀瀹屾垚 | 鏈湀鑷勾鍒濈疮璁� | 绛夊紡缁撴灉锛堝皢绾犳涓鸿鍊硷級 |')
+                rep.append('| --- | --- | --- | --- | --- |')
+                for r1, r2, r3, r4, r5 in bad[:60]:
+                    rep.append('| %s | %s | %s | %s | %s |' % (r1, r2, r3, r4, r5))
+                rep.append('')
+        else:
+            rep.append('### 鏈堝害鏍¢獙锛�**璺宠繃**锛堟湭鎵惧埌涓婃湀鎶曡祫绯荤粺鏁版嵁琛紝鎸夌敤鎴峰彛寰� (a) 闇�姣忔湀鏀句竴浠斤級')
+            rep.append('')
+
+    # 鏈湡娌℃湁浠讳綍椤圭洰琛岀殑甯傚窞 鈫� 鎻愮ず锛堝彲鑳芥紡鎶ワ級
+    for cat in TARGETS:
+        _cities = {r['甯傚窞'] for r in parsed if r['鍝佺被'] == cat and r['甯傚窞']}
+        _miss = [c for c in CITY17 if c not in _cities]
+        if _miss:
+            add_alert('鎻愮ず', '鏈湡鏃犺甯傚窞鏁版嵁', cat, '', '', '鈥�',
+                      '鍏� %d 涓競宸炴湰鏈熸病鏈変换浣曢」鐩锛�%s' % (len(_miss), '銆�'.join(_miss)),
+                      '纭鏄�岃甯傚窞纭疄娌℃湁椤圭洰銆嶈繕鏄�屾紡鎶ャ�嶏紱婕忔姤鍒欒甯傚窞琛ユ姤')
+
+    # ---- 姹囨�汇�婂脊绐楁彁绀恒�嬶紙鏈潵鎼繘 Java 鍚庣鏃舵寜姝ら�愭潯寮圭獥锛� ----
+    _lv = {'闃绘柇': 0, '璀﹀憡': 1, '鎻愮ず': 2}
+    alerts.sort(key=lambda x: (_lv.get(x['绾у埆'], 9), x['绫诲瀷'], x['鍝佺被'], x['甯傚窞'], x['椤圭洰鍚嶇О']))
+    _n = {k: sum(1 for x in alerts if x['绾у埆'] == k) for k in ('闃绘柇', '璀﹀憡', '鎻愮ず')}
+    a_cols = ['搴忓彿', '绾у埆', '绫诲瀷', '鍝佺被', '甯傚窞', '椤圭洰鍚嶇О', '鏉ユ簮', '璇︽儏', '寤鸿鍔ㄤ綔']
+    p_al = os.path.join(out, '寮圭獥鎻愮ず_%s.tsv' % a.month)
+    with io.open(p_al, 'w', encoding='utf-8-sig') as fh:
+        fh.write('\t'.join(a_cols) + '\n')
+        for _i, _x in enumerate(alerts, 1):
+            fh.write('\t'.join(str(_i) if _k == '搴忓彿' else str(_x.get(_k, '')).replace('\t', ' ')
+                                for _k in a_cols) + '\n')
+    al_md = ['## 鈿狅笍 寮圭獥鎻愮ず鎬昏锛堝叡 %d 椤癸細闃绘柇 %d 锛� 璀﹀憡 %d 锛� 鎻愮ず %d锛�' % (
+                 len(alerts), _n['闃绘柇'], _n['璀﹀憡'], _n['鎻愮ず']), '']
+    al_md.append('> 绾у埆锛�**闃绘柇**=涓嶈兘鍐欒〃锛屽厛澶勭悊锛�**璀﹀憡**=鏁版嵁鏈夌枒鐐癸紝浜哄伐纭鍚庡啀鍐欙紱**鎻愮ず**=浠呭憡鐭ャ��')
+    al_md.append('> 鏈哄櫒鍙娓呭崟锛歚%s`锛堟湭鏉� Java/缃戦〉鎸夋閫愭潯寮圭獥锛夈�傝鍒欒 `docs\\鎶曡祫\\寮圭獥鎻愮ず瑙勫垯_2026-09-17.md`銆�'
+                 % os.path.basename(p_al))
+    al_md.append('')
+    if alerts:
+        al_md.append('| 搴忓彿 | 绾у埆 | 绫诲瀷 | 鍝佺被 | 甯傚窞 | 椤圭洰鍚嶇О | 璇︽儏 | 寤鸿鍔ㄤ綔 |')
+        al_md.append('| --- | --- | --- | --- | --- | --- | --- | --- |')
+        for _i, _x in enumerate(alerts, 1):
+            al_md.append('| %d | %s | %s | %s | %s | %s | %s | %s |' % (
+                _i, _x['绾у埆'], _x['绫诲瀷'], _x['鍝佺被'], _x['甯傚窞'], _x['椤圭洰鍚嶇О'], _x['璇︽儏'], _x['寤鸿鍔ㄤ綔']))
+        al_md.append('')
+    else:
+        al_md.append('锛堟棤锛氭墍鏈夋鏌ラ」鍧囬�氳繃锛�'); al_md.append('')
+    rep = rep[:4] + al_md + rep[4:]
+
+    cols = ['鍝佺被', '鍔ㄤ綔', '鐩爣琛屽彿', '椤圭洰鍚嶇О', '鍐欏叆鏉ユ簮'] + WANT
+    p_tsv = os.path.join(out, '鍙樻洿娓呭崟_%s.tsv' % a.month)
+    with io.open(p_tsv, 'w', encoding='utf-8-sig') as fh:
+        fh.write('\t'.join(cols) + '\n')
+        for c in changes:
+            fh.write('\t'.join(str(c.get(k, '')) for k in cols) + '\n')
+    p_md = os.path.join(out, '鍖归厤鏍¢獙鎶ュ憡_%s.md' % a.month)
+    io.open(p_md, 'w', encoding='utf-8').write('\n'.join(rep) + '\n')
+    print('鍙樻洿鏉℃暟 = %d' % len(changes))
+    print('寮圭獥鎻愮ず = %d锛堥樆鏂� %d / 璀﹀憡 %d / 鎻愮ず %d锛�' % (len(alerts), _n['闃绘柇'], _n['璀﹀憡'], _n['鎻愮ず']))
+    print('wrote', p_md)
+    print('wrote', p_tsv)
+    print('wrote', p_al)
+
+
+if __name__ == '__main__':
+    main()

--
Gitblit v1.9.1