Files
openclaw-config/workspace-resume/okr/parse_okr_to_mysql.py
T

269 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <xlsx> # 指定文件
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_monthYYYYMM
跨月周期(如“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()