行者的成绩统计工作流
Dependencies
| 依赖 | 版本要求 | 用途 |
|---|---|---|
| Python | >=3.10 | 运行环境 |
| pandas | >=2.0 | 数据读取与处理 |
| numpy | >=1.24 | 数值计算 |
| xlsxwriter | >=3.0 | Excel输出(唯一写入引擎,严禁openpyxl写入) |
| openpyxl | >=3.0 | 仅用于读取期中xlsx |
| xlrd | >=2.0 | 读取期末.xls文件 |
pip install pandas numpy xlsxwriter openpyxl xlrd
输出路径智能路由
文件夹体系检测
脚本启动时自动检测工作空间是否存在标准化目录:
import os, shutil, tempfile
from datetime import datetime
WORKSPACE = os.getcwd() # 当前对话的工作空间
FINAL_DIR = os.path.join(WORKSPACE, '成绩分析', '最终成果')
DRAFT_DIR = os.path.join(WORKSPACE, '成绩分析', '过程稿')
# 输出路径决策:先检测后决策
if os.path.isdir(FINAL_DIR):
OUT_DIR = FINAL_DIR # 标准化用户 → 直出最终成果
else:
OUT_DIR = WORKSPACE # 普通用户 → 回退到根目录
版本管理(自动归档)
修改覆盖前,旧版必定加时间戳移到过程稿:
def save_with_version(out_path):
"""智能保存:已有同名的移到过程稿"""
if os.path.exists(out_path):
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
fname = os.path.basename(out_path)
draft_path = os.path.join(DRAFT_DIR, f'{fname.replace(".xlsx","")}_{ts}.xlsx')
os.makedirs(DRAFT_DIR, exist_ok=True)
shutil.move(out_path, draft_path)
print(f'📦 旧版已归档 → {draft_path}')
临时文件策略
- 解压zip →
tempfile.mkdtemp(),用完即删 - 中间脚本 → 同上
- 只保留最终xlsx在工作空间
核心流程(一句话)
用户丢数据 → 自动检测模式 → 推测考试信息 → 生成年级总表 → 解释怎么用 → 问"要班表吗?"
永远不问超过2个问题。 能推测的不问,有默认的不问。
自动推测链
1. 数据模式检测
检测到 .xlsx 文件 → 模式A(有上期汇总表)
全是 .zip 文件 → 模式B(两套原始zip)
2. 考试信息推测
从用户提供的数据中推断:
规则1 — 文件名推断
zip名含"期中" → 推测"期中考试"
zip名含"期末" → 推测"期末考试"
规则2 — 历史对话推断(优先级最高)
上次对话生成了"第二学期期中"的表
→ 推测本次是"第二学期期末"
上次对话生成了"第二学期期末"的表
→ 推测本次是"第一学期期中"(学年+1,学期翻转为第一学期)
规则3 — 考号推断年级
考号前两位 → 年级编号(如 25 → 25级)
中职/高中学制3年 → 年级编号取 入学年份后两位(24/25/26)
推测后立即告知用户,询问是否纠正:
"监测到本次数据应该是 2025~2026学年 第二学期 期末考试,对吗?不对我可以立即修改。"
姓名交叉验证(防学号复用)
退学/转学/学号复用会导致同号不同人。生成时自动对比期中期未姓名:
from collections import Counter
# 从期末四科中取姓名众数(综合提取,抗单科录入错误)
name_candidates = [get_name_from_final(d, kid) for d in final_dicts if get_name_from_final(d, kid)]
final_name = Counter(name_candidates).most_common(1)[0][0]
# 交叉验证
if mid_name and final_name and mid_name != final_name:
name_warning = '⚠姓名不一致(中:XX/末:YY)' # 追加到状态列
不一致时在状态列显示 ⚠姓名不一致(中:张三/末:李四),提醒人工核实。
桌面复制提示
生成完成后自动提示:
💡 需要将成果复制到桌面吗?
→ 桌面路径已检测
→ 确认后自动创建文件夹并复制所有 .xlsx
复制时自动创建文件夹名为当前考试信息(如 2025~2026学年第二学期期末成绩汇总)。
3. 特殊班级处理
生成开始前提示已排除班级列表:
📋 检测到以下班级匹配排除关键词: 25旅游(18人), 25数媒五年专(32人)
默认排除,不参与年级排名。如需纳入或保留,请回复修改。
支持三种模式:
- exclude: 排除(默认),不参与排名
- separate: 单独列Sheet,不参与主排名
- include: 正常纳入
排除关键词可自定义: 默认 ['五年', '旅游', '三二分段'],生成前询问一句:
排除关键词用默认的还是自定义?(默认: 五年/旅游/三二分段)
Step 0: 数据读取(穿透层层嵌套)
语数英:zip → {学科}/.../单科简明.xls → col0=考号, col2=班级, col3=成绩,从第4行开始
政治:zip → 每班独立.xls → 跳过含"缺/缺考"的文件 → 列名匹配 考号/班级/总分
排除班:通过期末数据的班级字段匹配关键词
# 通用读取器——不管文件夹嵌套多深,递归找到目标文件
def read_subject_data(zip_dir, subject_name):
"""从zip解压目录递归查找并读取单科成绩
Returns: dict {考号: (班级, 分数)}
"""
patterns = [
f'{zip_dir}/{subject_name}/**/*单科简明*.xls',
f'{zip_dir}/{subject_name}/**/*年级*.xls',
f'{zip_dir}/*{subject_name}*/**/*单科简明*.xls',
]
files = []
for pat in patterns: files.extend(glob.glob(pat, recursive=True))
if not files:
raise FileNotFoundError(f'❌ 未找到{subject_name}成绩文件,请确认zip解压完整')
df = pd.read_excel(files[0], header=None)
result = {}
for i in range(3, len(df)):
row = df.iloc[i]
kid = str(row[0]).strip()
cls = str(row[2]).strip() if pd.notna(row[2]) else ''
if kid.isdigit() and len(kid) >= 9:
try: result[kid] = (cls, float(row[3]))
except: result[kid] = (cls, np.nan)
return result
Step 1: 生成年级总表(默认,不问)
合并逻辑
- 按考号匹配上期↔本期
- 排除关键词匹配的班级(可事后调整)
- 计算:各科总分 → 年级排名 → 班内排名 → 各科变化
- 0分 ≠ 缺考(只有NaN才是缺考)
年级总表结构(9~10 Sheet)
| 序号 | Sheet | 结构 |
|---|---|---|
| 1 | 年级总排名 | 全年级按总分降序,年级排名/班级/姓名/考号/各科/总分/备注,🥇🔵极值 |
| 2 | 期中vs期末对比 | 班级/姓名/考号 + 各科(期中/期末/变化) + 总分(期中/期末/变化) + 排名变化 + 状态 |
| 3 | 各班前5名 | 每班一个标题行"XX班 — 班级前5名" + 表格(班级/姓名/考号/各科/总分) |
| 4 | 各班汇总 | 各班人数/均分/进步率/退步率 降序排列,全年级垫底行 |
| 5~8 | 语文/数学/英语/政治排名 | 独立按单科成绩降序,年级排名/班级/姓名/考号/成绩/备注 |
| 9 | 缺考退学 | 异常明细 |
| +1 | 特殊班级(可选) | 如有separate模式的班级,单独列此Sheet |
格式铁律
| 项目 | 规范 |
|---|---|
| 列序 | 班级 → 姓名 → 考号 |
| 冻结 | 前2列(班级+姓名)——手机WPS不挤占可视区 |
| 筛选 | 仅班级列——筛选后即得本班视图 |
| 标题行 | 合并行,微软雅黑13pt加粗居中 |
| 表头 | 蓝底(#4472C4)白字,微软雅黑9pt加粗,换行居中 |
| 数据 | 微软雅黑9pt,边框全,居中 |
| 极值 | 最高分🥇#FFD700加粗 / 最低分🔵#BDD7EE |
| 变化列 | >0绿色(#C6EFCE) / <0红色(#FFC7CE) |
| 页面 | A4横向 1页宽 ws.set_landscape() |
# 冻结和筛选(关键:只冻前2列)
ws.freeze_panes(1, 2) # 冻结表头行 + 班级+姓名 2列
ws.autofilter('A1:A{N}') # 仅班级列筛选(列A=班级)
Step 2: 解释+补问
生成完年级总表后,回复格式:
📊 年级总表已生成 → [文件名].xlsx({N}人 {M}班)
💡 使用提示:
→ 筛选【班级列】选你想要的班
→ 左滑查看各科成绩和排名
→ 班级/姓名列固定不动(冻结前2列)
🔍 需要为某个班单独出班级报表吗?
→ 单个班:"25电子"
→ 多个班合并:"25旅游, 25数媒五年专"(逗号分隔)
Step 3: 班级报表(按需生成)
用户指定班级后:
3.0 期中数据预检(生成前必做)
生成班表前,先在年级总表 DataFrame 中检查目标班级的期中数据:
⚠ 注意:"{target_class}"期中无数据记录(可能被排除或未参加期中)
→ 选项1: 提供该班的期中原始数据文件 → 我会重新匹配
→ 选项2: 仅生成期末成绩表,跳过对比和分析Sheet
→ 选项3: 保留对比Sheet框架(期中列留空待补)
如果选2或3,班表从4Sheet降为2Sheet(期末成绩+分析汇总),对比Sheet期中列全部留空或删除。
3.1 先问专业课
该班有专业课成绩吗?(如电工技术、电子技术等)
→ 有 → 请上传Excel文件
→ 没有 → 纯四科版
3.2 班级报表结构(4 Sheet)
| Sheet | 列结构 |
|---|---|
| 期末成绩表 | 班级/姓名/考号 + 各科(动态列,给几科出几科) + N科总分/班排 + 年级排名 + 备注 |
| 期中vs期末对比 | 班级/姓名/考号 + 各科(期中/期末/变化) + N科总分(期中/期末/变化) + 班排(期中/期末/变化) + 年级排名 + 状态 |
| 分析汇总 | 按学科分段:四科均分/变化 → 专业课各科→ 进步退步统计 |
| 进退步明细 | 进步Top5 + 退步Top5(排除退学者)+ 缺考/退学列表 |
科数动态原则:
- 不写死"四科"或"六科"
- 基础四科来自期末zip → 自动生成"四科总分"/"四科班排"
- 专业课来自用户上传 → 追加列 + 自动生成"N科总分"/"N科班排"(N = 实际科数)
- 分析汇总按学科分段,每个专业课独立一行
专业课匹配:
- 按考号匹配(优先),失败后按姓名匹配
- 专业课名称取用户上传的文件名中提取(如"电工技术基础"→"电工")
3.3 多班合并
target_class = "25旅游, 25数媒五年专"
→ 取两班数据合为一个虚拟班 → 标题自动:"25旅游+25数媒五年专"
→ 班级列显示原始班名
→ 分析汇总首行标注"多班合并:XX + XX"
各班前5名 Sheet 格式铁律(v1.4.1 修复):
- Row1: 总标题(无背景色)→ 微软雅黑13pt加粗居中
- Row2: "XX班 — 班级前5名" → 无背景色,微软雅黑9pt加粗
- Row3: 列标题(班级/姓名/考号/语/数/英/政)→ 蓝底白字 #4472C4
- 数据行 → 微软雅黑9pt边框居中
- ❌ 禁止:Row2(班级名行)使用蓝底——班级名行应无底色
微信兼容检查(xlsxwriter铁律)
严禁openpyxl写入。 微信不兼容原因:
- t="inlineStr" → 不显示文字 → xlsxwriter默认t="s"共享字符串
- 缺applyFont/applyFill → 不显示颜色 → xlsxwriter自动设置
- 条件格式FormulaRule → 不识别 → 逐格手动填色
- 合并标题行存在但不影响数据行渲染
Auto Upgrade Logic
每次加载本 Skill 完成对话后:
1. 对比实际操作与 SKILL.md 记录的步骤/样式
2. 用户确认满意后,提示询问是否更新 Skill
3. 确认后合并差异,version +0.0.1(如 1.4.0 → 1.4.1)
Optional Modules
以下模块按需执行,用户不提时不运行。
[可选] 质量分析报告
触发:用户说"质量分析" | 生成多Sheet报告含KPI/各班对比/单科诊断/建议文字
[可选] 特殊班处理调整
触发:用户对排除列表不满 | 支持三种模式:exclude(排除)/separate(独立Sheet)/include(纳入)
[可选] 模式B建议
触发:检测到>10%学生期中缺考 | 提示改用两套原始zip(匹配更完整)
[可选] 各班汇总排序
触发:排序不对 | 选班级行→数据→排序→进步率降序,全年级垫底