#!/usr/bin/env python3 # -*- coding: utf-8 -*- """解析研究院-维云智造 OKR 绩效 Excel(杨轩),归档到 MySQL resume 库。 支持新旧两种模板: - 旧模板(202601 等,多 sheet):表头行4,目标行5起 - 新模板(8月起,单 sheet OKR考核):表头行5-6,目标行8起(跳过示例行) 用法: python3 parse_okr_to_mysql.py # 解析 inbound 最新 OKR xlsx python3 parse_okr_to_mysql.py --month 202601 # 指定考核月 python3 parse_okr_to_mysql.py --file # 指定文件 python3 parse_okr_to_mysql.py --dry-run # 预览不写入 """ import glob import os import re import sys import openpyxl MYSQL_DB = "resume" MYSQL_HOST = "127.0.0.1" MYSQL_PORT = 3306 MYSQL_USER = "root" def get_mysql_pwd(): env_path = "/home/yangxuan/.openclaw/.env" for line in open(env_path): line = line.strip() if line.startswith("MYSQL_PWD="): return line.split("=", 1)[1].strip() return "" def parse_info(text): """从人员信息行解析 被考核人/岗位/上级/周期/时间(新旧模板通用)""" info = {} m = re.search(r"(?:被考核人|姓名)[::]\s*(\S+)", text) info["full_name"] = m.group(1) if m else "" m = re.search(r"岗位[::]\s*(\S+)", text) info["position"] = m.group(1) if m else "" m = re.search(r"(?:直属上级|直接上级)[::]\s*(\S+)", text) info["supervisor"] = m.group(1) if m else "" m = re.search(r"考核周期[::]\s*([^\s]+)", text) info["period"] = m.group(1).strip() if m else "" m = re.search(r"考核时间[::]\s*(\S+)", text) info["assess_date"] = m.group(1).strip() if m else "" return info def detect_template(ws): """识别模板。'new'=8月新模板,'old'=202601旧模板。""" # 新模板:行5-6 含"员工本人填报"或"关键结果" for r in (5, 6): for c in range(3, 12): v = ws.cell(row=r, column=c).value if v and ("员工本人填报" in str(v) or "关键结果" in str(v)): return "new" # 旧模板:行4 含"目标O"或"关键绩效指标" for c in range(3, 12): v = ws.cell(row=4, column=c).value if v and ("目标O" in str(v) or "关键绩效指标" in str(v)): return "old" return "new" def parse_sheet_new(ws): """解析 8 月新模板(OKR考核 sheet)""" b3 = ws["B3"].value or "" info = parse_info(b3) # 月度评价:F15 是“月度评价”标签,分数在其右侧(G15 等),上级可能未填 monthly_rating = None for coord in ("G15", "H15", "I15"): v = ws[coord].value if isinstance(v, (int, float)): monthly_rating = float(v) break objectives = [] for row in range(8, 40): # 目标行8起 seq = ws.cell(row=row, column=2).value # B 序号 obj = ws.cell(row=row, column=3).value # C 任务工作 krs = ws.cell(row=row, column=4).value # D 关键结果 compl = ws.cell(row=row, column=5).value # E 完成情况 score = ws.cell(row=row, column=7).value # G 评分 weight = ws.cell(row=row, column=9).value # I 权重 wscore = ws.cell(row=row, column=10).value # J 得分 if isinstance(obj, str) and "月度工作补充" in obj: break if not isinstance(seq, (int, float)): continue objectives.append({ "seq": int(seq), "objective": (obj or "").strip(), "krs": (krs or "").strip(), "completion": (compl or "").strip(), "score": float(score) if isinstance(score, (int, float)) else None, "weight": float(weight) if isinstance(weight, (int, float)) else None, "weighted_score": float(wscore) if isinstance(wscore, (int, float)) else None, }) return {**info, "monthly_rating": monthly_rating, "objectives": objectives} def parse_sheet_old(ws): """解析 202601 旧模板""" b2 = ws["B2"].value or "" info = parse_info(b2) monthly_rating = None for coord in ("J12", "B12"): v = ws[coord].value if isinstance(v, (int, float)): monthly_rating = float(v) break objectives = [] for row in range(5, 11): # 行5-10 对应序号1-6 seq = ws.cell(row=row, column=2).value # B 序号 obj = ws.cell(row=row, column=3).value # C 目标O krs = ws.cell(row=row, column=5).value # E KR compl = ws.cell(row=row, column=6).value # F 完成情况 score = ws.cell(row=row, column=8).value # H 评分 weight = ws.cell(row=row, column=9).value # I 权重 wscore = ws.cell(row=row, column=10).value # J 加权 if seq is None and obj is None: continue objectives.append({ "seq": seq if isinstance(seq, (int, float)) else row - 4, "objective": (obj or "").strip(), "krs": (krs or "").strip(), "completion": (compl or "").strip(), "score": float(score) if isinstance(score, (int, float)) else None, "weight": float(weight) if isinstance(weight, (int, float)) else None, "weighted_score": float(wscore) if isinstance(wscore, (int, float)) else None, }) return {**info, "monthly_rating": monthly_rating, "objectives": objectives} def month_from_period(period, sheet_name, fallback): """从考核周期/sheet名 确定 assess_month(YYYYMM) 跨月周期(如“7月27日-8月27日”)取后段月份(考核期末) """ # 末段月份优先(跨月周期取考核期末,如 7月27日-8月27日 → 8月) m = re.search(r"-(\d{1,2})月(\d{1,2})日", period or "") if m: return f"2026{int(m.group(1)):02d}" m = re.search(r"(\d{4})年(\d{1,2})月", period or "") if m: return f"{m.group(1)}{int(m.group(2)):02d}" m = re.search(r"(\d{4})\s*(\d{2})", sheet_name or "") if m: return f"{m.group(1)}{int(m.group(2)):02d}" m = re.search(r"(\d{1,2})月", period or "") if m: return f"2026{int(m.group(1)):02d}" return fallback or "" def process_sheet(wb, sheet_name, xls, dry_run=False): """解析并归档单个 sheet,返回 (assess_month, 目标数) 或 None""" ws = wb[sheet_name] tpl = detect_template(ws) print(f"📑 Sheet:{sheet_name} | 识别模板:{'新模板(8月起)' if tpl == 'new' else '旧模板(202601)'}") data = parse_sheet_new(ws) if tpl == "new" else parse_sheet_old(ws) assess_month = month_from_period(data.get("period"), sheet_name, None) if not assess_month: print(f" ⚠️ 无法确定考核月,跳过") return None if not data["objectives"]: print(f" ⚠️ {sheet_name} 无目标数据,跳过") return None print(f" 被考核人:{data['full_name']} | 岗位:{data['position']} | 上级:{data['supervisor']}") print(f" 考核周期:{data.get('period')} | assess_month={assess_month} | 月度评价:{data.get('monthly_rating')}") print(f" 目标数:{len(data['objectives'])}") for o in data["objectives"]: print(f" {o['seq']}. {o['objective'][:28]} | 权重{o['weight']} 评分{o.get('score')}") if dry_run: print(" (dry-run 未写入)") return assess_month pwd = get_mysql_pwd() if not pwd: print("❌ 未找到 MYSQL_PWD") return None import pymysql conn = pymysql.connect(host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=pwd, database=MYSQL_DB, charset="utf8mb4") cur = conn.cursor() cur.execute("SELECT id FROM okr_monthly_records WHERE assess_month=%s AND full_name=%s", (assess_month, data["full_name"])) for (rid,) in cur.fetchall(): cur.execute("DELETE FROM okr_objectives WHERE record_id=%s", (rid,)) cur.execute("DELETE FROM okr_monthly_records WHERE id=%s", (rid,)) cur.execute( "INSERT INTO okr_monthly_records (assess_month, full_name, position, supervisor, assess_date, monthly_rating, source_file) " "VALUES (%s,%s,%s,%s,%s,%s,%s)", (assess_month, data["full_name"], data["position"], data["supervisor"], data.get("assess_date", ""), data.get("monthly_rating"), os.path.basename(xls))) rid = cur.lastrowid for o in data["objectives"]: cur.execute( "INSERT INTO okr_objectives (record_id, seq, objective, krs, completion, score, weight, weighted_score) " "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", (rid, o["seq"], o["objective"], o["krs"], o["completion"], o.get("score"), o["weight"], o["weighted_score"])) conn.commit() cur.close() conn.close() print(f" ✅ 已入库({len(data['objectives'])} 项目标)") return assess_month def main(): dry_run = "--dry-run" in sys.argv month_filter = None if "--month" in sys.argv: month_filter = sys.argv[sys.argv.index("--month") + 1] # 定位 Excel xls = None for i, a in enumerate(sys.argv[1:]): if a == "--file" and i + 2 < len(sys.argv): xls = sys.argv[i + 2] elif a.endswith(".xlsx"): xls = a if xls is None or not os.path.exists(xls): cand = sorted(glob.glob("/home/yangxuan/.openclaw/media/inbound/*OKR*.xlsx")) xls = cand[-1] if cand else None if not xls or not os.path.exists(xls): print("❌ 未找到 OKR Excel 文件") return print(f"📄 解析文件:{os.path.basename(xls)}") wb = openpyxl.load_workbook(xls, data_only=True) # 决定要处理的 sheet 列表 if month_filter: # 支持范围:如 202602-202607,或单个 202601,或逗号分隔 if "-" in month_filter and len(month_filter) == 13: start_m, end_m = month_filter.split("-") start_i, end_i = int(start_m), int(end_m) months = [str(m) for m in range(start_i, end_i + 1)] sheets = [n for n in wb.sheetnames if n in months] else: parts = [p.strip() for p in month_filter.replace(",", ",").split(",")] sheets = [n for n in wb.sheetnames if n in parts] if not sheets: print(f"❌ 文件里未找到指定月份 sheet:{month_filter}") return else: # 无 --month:优先 OKR考核(新模板单 sheet) sheets = [n for n in wb.sheetnames if "OKR考核" in n] if not sheets: sheets = wb.sheetnames[:1] print(f"\n📦 待处理 sheet:{sheets}") done = [] for sn in sheets: print() r = process_sheet(wb, sn, xls, dry_run) if r: done.append(r) if dry_run: print(f"\n🔍 dry-run 完成,共 {len(sheets)} 个 sheet(未写入)") else: print(f"\n🎉 已完成 {len(done)} 个月 OKR 归档:{done}") if __name__ == "__main__": main()