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 | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 305 insertions(+), 0 deletions(-)
diff --git "a/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\270\202\345\267\236\346\234\210\346\212\245\350\247\243\346\236\220.py" "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\270\202\345\267\236\346\234\210\346\212\245\350\247\243\346\236\220.py"
new file mode 100644
index 0000000..77e32f1
--- /dev/null
+++ "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\270\202\345\267\236\346\234\210\346\212\245\350\247\243\346\236\220.py"
@@ -0,0 +1,305 @@
+# -*- coding: utf-8 -*-
+"""甯傚窞鏈堟姤瑙f瀽.py 鈥斺�� 鎶� 17 甯傚窞鐨勬湀鎶ユ簮鏂囦欢瑙f瀽鎴愮粺涓�鐨勩�岄」鐩銆嶈褰曪紙鍙锛�
+
+鐢ㄦ硶锛�
+ python docs\鎶曡祫\宸ュ叿\甯傚窞鏈堟姤瑙f瀽.py --month 2026-07
+ python docs\鎶曡祫\宸ュ叿\甯傚窞鏈堟姤瑙f瀽.py --month 2026-07 --body 瀹㈣繍锛堝杩愮珯锛�
+杈撳嚭锛�
+ docs\鎶曡祫\杈撳嚭\瑙f瀽缁撴灉_<YYYY-MM>.tsv 涓�琛屼竴涓」鐩紙鏈哄櫒鍙锛屼緵鍐欏簱/鍐欒〃鐢級
+ docs\鎶曡祫\杈撳嚭\瑙f瀽鎶ュ憡_<YYYY-MM>.md 浜鸿锛氭瘡涓枃浠剁殑甯傚窞/椤圭洰鏁�/瀛楁姹傚拰/涓�鑷存�ф牎楠�
+
+璁捐瑕佺偣锛堜笌銆婂競宸炴湀鎶ユ牸寮忓樊寮傛姤鍛娿�嬩竴鑷达級锛�
+ 1) 鍒椾綅涓嶅啓姝伙細鎸夎〃澶村叧閿瓧瀹氫綅鍒楋紙澶嶇敤 鏍煎紡鎺㈡祴.py 鐨� FIELD_PAT 涓� scan_fields锛夛紱
+ 2) 閫夎〃锛氬厛鎸夆�滆〃澶村尯骞存湀鏍囪鈥濆懡涓湰鏈燂紝鍐嶆寜瀛楁鍛戒腑鏁版墦鍒� 鈥斺�� 涓嶇湅闅愯棌涓庡惁锛�
+ 3) 閫夎锛氬厛鎵俱�屽競宸炶銆嶅垏娈碉紝鍙彇鏈枃浠跺搴斿競宸為偅涓�娈碉紱
+ 4) 椤圭洰琛岋細鍚嶇О鍒楅潪绌� + 闈炲皬璁�/鍚堣/鍒嗙被琛� + 鏈夋暟鍊奸敋鐐癸紱鍏煎鏃犲簭鍙峰垪/缂哄簭鍙凤紱
+ 5) 鍒嗙被琛岋紙涓�銆佷簩銆佷笁銆佸洓銆佷簲銆侊級涓庛�屽皬璁�/鍚堣銆嶈鍗曠嫭鏀堕泦锛氬垎绫昏浣滀负椤圭洰鐨勫綊灞烇紝
+ 灏忚/鍚堣琛屽彧鐢ㄤ簬**涓�鑷存�ф牎楠�**锛堥」鐩姹傚拰 vs 灏忚琛岋級銆�
+ 6) 闅愯棌琛�/闅愯棌琛ㄤ竴寰嬩笉浣滀负杩囨护鏉′欢锛堥粍鐭� R10 鏄湡瀹為」鐩锛夈��
+"""
+import sys, os, io, re, argparse, json, importlib.util
+
+_here = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.abspath(os.path.join(_here, '..', '..', '..'))
+for _b in (_here, _root, os.getcwd()):
+ for _sub in ('_pylibs',):
+ _lib = os.path.abspath(os.path.join(_b, _sub))
+ 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)
+
+# 闇�瑕佷粠婧愭枃浠跺彇鍑虹殑瀛楁锛堜笌鐩爣鏄庣粏琛ㄥ垪涓�涓�瀵瑰簲锛�
+FIELDS = ['椤圭洰鍚嶇О', '寤鸿鍗曚綅', '寤鸿鎬ц川', '寮�宸ユ椂闂�', '绔e伐鏃堕棿', '鎬绘姇璧�',
+ '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚', '寤鸿闃舵', '褰㈣薄杩涘害']
+# 鏁板�煎瓧娈碉紙鐢ㄤ簬涓�鑷存�ф牎楠岋級
+NUM_FIELDS = ['鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚']
+
+GROUP_PAT = re.compile(r'^(涓�|浜寍涓墊鍥泑浜攟鍏瓅涓億鍏�)銆�')
+SKIP_PAT = re.compile(r'^(灏廫s*璁鍚圽s*璁鎬籠s*璁�)')
+TOTAL_PAT = re.compile(r'^(鍚圽s*璁鎬籠s*璁�)')
+
+# 灏戞暟鏂囦欢鏃㈡棤鏂囦欢鍚嶅煄甯傘�佷篃鏃犮�屽競宸炶銆嶏紝鍙兘鎸変笟鍔′簨瀹炰汉宸ョ櫥璁帮紙鏈夋嵁鍙煡锛氭眽鍙e杩愪腑蹇�=姝︽眽锛�
+CITY_OVERRIDE = [
+ ('绔欏満鍥烘姇锛堝崡灞便�佹眽鍙g珯锛�', '姝︽眽'),
+]
+
+
+def city_by_override(fname):
+ for key, city in CITY_OVERRIDE:
+ if key in fname:
+ return city
+ return None
+
+
+def num(v):
+ """灏介噺杞垚鏁板�硷紱杞笉浜嗚繑鍥� None銆�"""
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
+ return v
+ s = F.norm(v)
+ if not s:
+ return None
+ s = s.replace(',', '').replace('锛�', '').replace('%', '')
+ 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 str(v)
+
+
+def pick_columns(cell, nrows, ncols, merges=None):
+ """鎸夎〃澶村畾浣嶅垪锛堝惈涓ょ骇琛ㄥご娑堟锛夆�斺�斿疄鐜拌 鏍煎紡鎺㈡祴.py 鐨� pick_columns锛屼繚璇佷袱澶勫彛寰勪竴鑷淬��"""
+ return F.pick_columns(cell, nrows, ncols, merges, fields=FIELDS)
+
+
+def parse_sheet(cell, nrows, ncols, city=None, merges=None):
+ """瑙f瀽涓�寮犲伐浣滆〃 鈫� (椤圭洰琛屽垪琛�, 娈典俊鎭�, 鏍¢獙淇℃伅)"""
+ cols, hits = pick_columns(cell, nrows, ncols, merges)
+ namec = cols.get('椤圭洰鍚嶇О')
+ if namec is None:
+ return [], {}, {}
+ segs = F.city_segments(cell, nrows, ncols)
+ if city is None and len({s[1] for s in segs}) == 1:
+ city = segs[0][1]
+ lo, hi, city_row = 1, 10 ** 9, None
+ if city:
+ for i, (rr, cc) in enumerate(segs):
+ if cc == city:
+ city_row, lo = rr, rr
+ hi = segs[i + 1][0] if i + 1 < len(segs) else 10 ** 9
+ break
+ project_name_pat = None
+ # 鏁板�奸敋鐐瑰垪锛氳嚜寮�濮嬪缓璁剧疮璁� / 鑷勾鍒濈疮璁� / 鎬绘姇璧�
+ anchors = [cols[k] for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鎬绘姇璧�') if k in cols]
+
+ rows, group, subtotals, city_vals = [], '', {}, None
+ for r in range(min(nrows, 800)):
+ if not (lo <= r + 1 < hi):
+ continue
+ if city_row and r + 1 == city_row:
+ # 甯傚窞琛屾湰韬�氬父灏卞甫鐫�鏈競宸炲悎璁★紙榛勭煶 R5 / 瀛濇劅 R7 / 鍜稿畞 R6 鈥︼級锛岀暀浣滄牎楠�
+ city_vals = {f: num(cell(r, cols[f])) for f in NUM_FIELDS if f in cols}
+ continue
+ a = F.norm(cell(r, 0))
+ name = F.norm(cell(r, namec))
+ joined = a + name
+ if GROUP_PAT.match(joined):
+ group = joined[:40]
+ continue
+ if SKIP_PAT.match(a) or SKIP_PAT.match(name):
+ # 灏忚/鍚堣琛岋細鍏ㄩ儴鏀堕泦锛岀敤浜庝竴鑷存�ф牎楠屻��
+ # 瑙勫垯锛氫紭鍏堛�屽悎璁�/鎬昏銆嶈锛涙病鏈夊悎璁¤鏃讹紝鍙栥�岃嚜寮�濮嬪缓璁剧疮璁°�嶆渶澶х殑閭h灏忚
+ #锛堟簮鏂囦欢甯告湁澶氫釜绌哄皬璁¤锛屽鏋楀尯瀹㈣繍 R11 绌恒�丷14 鏈夊�笺�丷17 鍙湁鏂囧瓧锛夈��
+ vals = {}
+ for f in NUM_FIELDS:
+ if f in cols:
+ vals[f] = num(cell(r, cols[f]))
+ vals['_kind'] = 'total' if TOTAL_PAT.match(a) or TOTAL_PAT.match(name) else 'sub'
+ subtotals['R%d' % (r + 1)] = vals
+ continue
+ if not name or len(name) < 4:
+ continue
+ if any(name.startswith(x) for x in F.BAD_NAME):
+ continue
+ if namec == 0:
+ numbered = True # 鏃犲簭鍙峰垪锛堥殢宸烇級锛氬悕绉板垪灏辨槸 A 鍒�
+ else:
+ numbered = bool(re.match(r'^\d{1,3}(\.0)?$', a)) or a == ''
+ has_value = any(num(cell(r, c)) is not None for c in anchors) if anchors else True
+ if not (numbered and has_value):
+ continue
+ rec = {'琛屽彿': r + 1, '搴忓彿': pretty(cell(r, 0)), '鍒嗙被': group}
+ for f in FIELDS:
+ c = cols.get(f)
+ v = cell(r, c) if c is not None else ''
+ rec[f] = pretty(num(v)) if f in NUM_FIELDS else F.norm(v)
+ rows.append(rec)
+ seg_info = {'甯傚窞': city or '', '甯傚窞琛�': city_row or '', '娈佃捣': lo,
+ '娈垫': '' if hi >= 10 ** 9 else hi,
+ '鍏ㄩ儴甯傚窞琛�': ['R%s:%s' % s for s in segs]}
+ return rows, seg_info, {'鍒椾綅': {f: F.colname(c) for f, c in cols.items()},
+ '灏忚鍚堣琛�': subtotals, '甯傚窞琛屽��': city_vals}
+
+
+def pick_total(subtotals):
+ """浠庢敹闆嗗埌鐨勫皬璁�/鍚堣琛岄噷鎸戝嚭鐢ㄤ簬鏍¢獙鐨勯偅涓�琛岋細浼樺厛鍚堣琛岋紝鍏舵鑷紑濮嬪缓璁剧疮璁℃渶澶х殑灏忚琛屻��"""
+ if not subtotals:
+ return None, None
+ totals = [(k, v) for k, v in subtotals.items() if v.get('_kind') == 'total']
+ if totals:
+ return totals[-1]
+ subs = sorted(subtotals.items(), key=lambda kv: (kv[1].get('鑷紑濮嬪缓璁剧疮璁�') or 0), reverse=True)
+ return subs[0]
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--month', required=True)
+ ap.add_argument('--root', default=os.path.join('docs', '鎶曡祫'))
+ ap.add_argument('--body', default=None)
+ ap.add_argument('--out', default=None)
+ a = ap.parse_args()
+ out = a.out or os.path.join(a.root, '杈撳嚭')
+ os.makedirs(out, exist_ok=True)
+ ym = a.month.replace('-', '')
+ bodies = [a.body] if a.body else ['瀹㈣繍锛堝杩愮珯锛�', '鐗╂祦锛堢墿娴佸洯鍖猴級']
+ md = '%d鏈�' % int(a.month.split('-')[1])
+
+ records, report, checks = [], [], []
+ report.append('# 甯傚窞鏈堟姤瑙f瀽鎶ュ憡锛�%s锛�' % a.month)
+ report.append('')
+ report.append('锛堝彧璇昏В鏋愶紱涓�琛屼竴涓」鐩锛屾槑缁嗚鍚屽悕 `.tsv`锛�')
+ report.append('')
+ for body in bodies:
+ d = os.path.join(a.root, '杈撳叆', body, md)
+ body_key = body.split('锛�')[0]
+ report.append('## %s' % body)
+ report.append('')
+ report.append('| 鏂囦欢 | 宸ヤ綔琛� | 甯傚窞 | 椤圭洰琛� | 鎶曡祫绯荤粺鍚堣(鑷紑濮嬪缓璁剧疮璁�/鑷勾鍒濈疮璁�/鏈湀瀹屾垚) | 婧愭枃浠跺皬璁¤ | 涓�鑷存�� |')
+ report.append('| --- | --- | --- | --- | --- | --- | --- |')
+ if not os.path.isdir(d):
+ report.append('| 锛堢洰褰曚笉瀛樺湪锛�%s锛� | | | | | | |' % d)
+ report.append('')
+ continue
+ files = sorted(f for f in os.listdir(d)
+ if os.path.isfile(os.path.join(d, f)) and not f.startswith('~$'))
+ for f in files:
+ path = os.path.join(d, f)
+ city_file = F.city_of_text(f)
+ try:
+ sheets = F.open_sheets(path)
+ sel = F.pick_sheet(sheets, ym=ym)
+ if not sel:
+ report.append('| %s | 锛堟湭閫変腑宸ヤ綔琛級 | | | | | |' % f[:44])
+ checks.append({'鍝佺被': body_key, '甯傚窞': city_file or '', '鏂囦欢': f, '宸ヤ綔琛�': '',
+ '鐘舵��': '鏈�変腑宸ヤ綔琛�', '椤圭洰琛屾暟': 0, '涓�鑷存��': '鈥�'})
+ continue
+ nm, nr, nc, cell, _hits = sel
+ merges = F.sheet_merges(path).get(nm, [])
+ rows, seg, extra = parse_sheet(cell, nr, nc, city=city_file, merges=merges)
+ city = seg.get('甯傚窞') or city_file or city_by_override(f) or ''
+ sums = {k: sum(num(r.get(k)) or 0 for r in rows) for k in NUM_FIELDS}
+ sub = extra.get('灏忚鍚堣琛�') or {}
+ sub_txt, ok = [], '鈥�'
+ if not sub and extra.get('甯傚窞琛屽��'):
+ sub = {'R%s(甯傚窞琛�)' % (seg.get('甯傚窞琛�') or '?'): extra['甯傚窞琛屽��']}
+ tok = tval = None
+ if sub:
+ key, val = pick_total(sub)
+ tok, tval = key, val
+ sub_txt.append('%s=%s' % (key, '/'.join(pretty(val.get(k)) for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'))))
+ if len(sub) > 1:
+ sub_txt.append('锛堝彟鏈� %d 琛屽皬璁�/鍚堣锛�' % (len(sub) - 1))
+ same = all(abs((val.get(k) or 0) - sums[k]) < 0.05
+ for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'))
+ ok = '鉁� 涓�鑷�' if same else '鈿狅笍 涓嶄竴鑷�'
+ ck = {'鍝佺被': body_key, '甯傚窞': city, '鏂囦欢': f, '宸ヤ綔琛�': nm, '鐘舵��': 'OK',
+ '椤圭洰琛屾暟': len(rows), '鍚堣琛�': '', '涓�鑷存��': ok,
+ '鍒椾綅': ','.join('%s=%s' % (k, v) for k, v in (extra.get('鍒椾綅') or {}).items())}
+ for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+ ck['姹傚拰_' + k] = sums[k]
+ if sub:
+ ck['鍚堣琛�'] = tok
+ for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+ ck['鍚堣_' + k] = tval.get(k)
+ checks.append(ck)
+ report.append('| %s | %s | %s | %d | %s | %s | %s |' % (
+ f[:44], nm[:14], city, len(rows),
+ '/'.join(pretty(sums[k]) for k in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚')),
+ ' '.join(sub_txt), ok))
+ for r in rows:
+ rec = {'鍝佺被': body_key, '甯傚窞': city, '鏂囦欢': f, '宸ヤ綔琛�': nm}
+ rec.update(r)
+ records.append(rec)
+ except Exception as e:
+ report.append('| %s | 锛堣В鏋愬け璐ワ細%s: %s锛� | | | | | |' % (f[:44], type(e).__name__, str(e)[:60]))
+ checks.append({'鍝佺被': body_key, '甯傚窞': city_file or '', '鏂囦欢': f, '宸ヤ綔琛�': '',
+ '鐘舵��': '瑙f瀽澶辫触锛�%s: %s' % (type(e).__name__, str(e)[:80]),
+ '椤圭洰琛屾暟': 0, '涓�鑷存��': '鈥�'})
+ report.append('')
+
+ # 甯傚窞姹囨�伙紙璺ㄦ枃浠跺悎骞跺悓甯傚窞锛�
+ by_city = {}
+ for r in records:
+ k = (r['鍝佺被'], r['甯傚窞'])
+ s = by_city.setdefault(k, {f: 0.0 for f in NUM_FIELDS})
+ for f in NUM_FIELDS:
+ s[f] += num(r.get(f)) or 0
+ report.append('## 鎸夊競宸炴眹鎬伙紙椤圭洰琛屾眰鍜岋紝璺ㄦ枃浠跺悎骞讹級')
+ report.append('')
+ report.append('| 鍝佺被 | 甯傚窞 | 椤圭洰鏁� | 璁″垝鎬绘姇璧� | 鑷紑濮嬪缓璁剧疮璁� | 鏈勾璁″垝 | 鑷勾鍒濈疮璁� | 鏈湀瀹屾垚 |')
+ report.append('| --- | --- | --- | --- | --- | --- | --- | --- |')
+ for (bk, city), s in sorted(by_city.items()):
+ n = sum(1 for r in records if r['鍝佺被'] == bk and r['甯傚窞'] == city)
+ report.append('| %s | %s | %d | %s | %s | %s | %s | %s |' % (
+ bk, city, n,
+ *(pretty(s[f]) for f in ('鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'))))
+ report.append('')
+
+ report.append('## 鍚勬簮鏂囦欢璇嗗埆鍒扮殑鍒椾綅锛�**鎸夎〃澶村叧閿瓧瀹氫綅锛屼笉鏄浐瀹氬垪鍙�**锛�')
+ report.append('')
+ report.append('| 鍝佺被 | 鏂囦欢 | 宸ヤ綔琛� | 椤圭洰鍚嶇О | 鎬绘姇璧� | 鑷紑濮嬪缓璁剧疮璁� | 鏈勾璁″垝鎶曡祫 | 鑷勾鍒濈疮璁� | 鏈湀瀹屾垚 |')
+ report.append('| --- | --- | --- | --- | --- | --- | --- | --- | --- |')
+ _key5 = ('椤圭洰鍚嶇О', '鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚')
+ for ck in checks:
+ _cm = dict(kv.split('=', 1) for kv in (ck.get('鍒椾綅') or '').split(',') if '=' in kv)
+ report.append('| %s | %s | %s | %s |' % (ck.get('鍝佺被', ''), ck.get('鏂囦欢', '')[:34], ck.get('宸ヤ綔琛�', '')[:12],
+ ' | '.join(_cm.get(k, '鈥�') for k in _key5)))
+ report.append('')
+
+ ck_cols = ['鍝佺被', '甯傚窞', '鏂囦欢', '宸ヤ綔琛�', '鐘舵��', '椤圭洰琛屾暟', '涓�鑷存��', '鍚堣琛�', '鍒椾綅',
+ '姹傚拰_鑷紑濮嬪缓璁剧疮璁�', '姹傚拰_鑷勾鍒濈疮璁�', '姹傚拰_鏈湀瀹屾垚',
+ '鍚堣_鑷紑濮嬪缓璁剧疮璁�', '鍚堣_鑷勾鍒濈疮璁�', '鍚堣_鏈湀瀹屾垚']
+ p_ck = os.path.join(out, '瑙f瀽鏍¢獙_%s.tsv' % a.month)
+ with io.open(p_ck, 'w', encoding='utf-8-sig') as fh:
+ fh.write('\t'.join(ck_cols) + '\n')
+ for c in checks:
+ fh.write('\t'.join(pretty(c.get(k, '')) if isinstance(c.get(k), float) else str(c.get(k, ''))
+ for k in ck_cols) + '\n')
+ print('鏍¢獙璁板綍鏁� = %d' % len(checks))
+
+ cols = ['鍝佺被', '甯傚窞', '鏂囦欢', '宸ヤ綔琛�', '琛屽彿', '搴忓彿', '鍒嗙被'] + FIELDS
+ tsv = os.path.join(out, '瑙f瀽缁撴灉_%s.tsv' % a.month)
+ with io.open(tsv, 'w', encoding='utf-8-sig') as fh:
+ fh.write('\t'.join(cols) + '\n')
+ for r in records:
+ fh.write('\t'.join(str(r.get(c, '')) for c in cols) + '\n')
+ p_md = os.path.join(out, '瑙f瀽鎶ュ憡_%s.md' % a.month)
+ io.open(p_md, 'w', encoding='utf-8').write('\n'.join(report) + '\n')
+ print('椤圭洰琛屽悎璁� = %d' % len(records))
+ print('wrote', p_md)
+ print('wrote', tsv)
+ print('wrote', p_ck)
+
+
+if __name__ == '__main__':
+ main()
--
Gitblit v1.9.1