#!/usr/bin/env python3 """ 简化版OJ学生做题数据查询脚本 使用方式: python scripts/get_student_oj.py --homework-id <作业ID> --student <学生姓名> 功能: 1. 登录OJ系统 2. 通过homework ID获取题目列表 3. 获取提交记录并解析学生做题情况 4. 输出Markdown格式的OJ数据 """ import io import re import sys from html import unescape from pathlib import Path # 修复Windows控制台编码 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') import httpx def load_env(): """从.env文件加载配置""" env_path = Path(__file__).parent.parent / '.env' env = {} if env_path.exists(): for line in env_path.read_text(encoding='utf-8').split('\n'): line = line.strip() if line and not line.startswith('#') and '=' in line: key, value = line.split('=', 1) env[key] = value return env def strip_tags(html: str) -> str: """移除HTML标签""" text = re.sub(r"<[^>]+>", "", html) return unescape(text).replace("\xa0", " ").strip() def detect_status(row_html: str) -> str: """识别判题状态""" status_map = { "Accepted": "AC", "Wrong Answer": "WA", "Compile Error": "CE", "Time Limit Exceeded": "TLE", "Runtime Error": "RE", "Presentation Error": "PE", } for keyword, status in status_map.items(): if keyword in row_html: return status return "UNKNOWN" def get_student_oj_data(homework_id: str, student_name: str) -> str: """ 获取指定学生在指定作业中的OJ做题数据 Args: homework_id: 作业ID(如 6a2bbd65aeff4da880b8f01d) student_name: 学生姓名 Returns: Markdown格式的OJ数据 """ env = load_env() base_url = env.get('OJ_BASE_URL', 'https://oj.qonnwolf.com') username = env.get('OJ_USERNAME', '') password = env.get('OJ_PASSWORD', '') if not username or not password: return "❌ 错误:未配置OJ用户名或密码,请检查.env文件" # 1. 登录OJ client = httpx.Client(base_url=base_url, follow_redirects=True) try: login_resp = client.post('/login', json={'uname': username, 'password': password}) login_resp.raise_for_status() # 检查登录是否成功 has_sid = any(c.name == "sid" for c in client.cookies.jar) if not has_sid: return "❌ 错误:OJ登录失败,请检查用户名密码" except Exception as e: return f"❌ 错误:OJ登录失败 - {e}" # 2. 获取作业题目列表 try: hw_resp = client.get(f'/homework/{homework_id}') if hw_resp.status_code != 200: return f"❌ 错误:获取作业详情失败 (HTTP {hw_resp.status_code})" # 解析题目列表 pattern = ( r'href="/p/([^"?]+)\?tid=' + re.escape(homework_id) + r'"[^>]*>([^<]+)(?: |\s)*([^<]*)' ) problems = re.findall(pattern, hw_resp.text) if not problems: return f"❌ 错误:作业 {homework_id} 中未找到题目" except Exception as e: return f"❌ 错误:获取题目列表失败 - {e}" # 3. 获取提交记录 try: record_resp = client.get(f'/record?tid={homework_id}&page=1') if record_resp.status_code != 200: return f"❌ 错误:获取提交记录失败 (HTTP {record_resp.status_code})" # 解析提交记录 rows = re.findall(r']*>(.*?)', record_resp.text, re.DOTALL) student_records = [] for row in rows: # 检查是否包含用户和题目链接 if "/user/" not in row or "/p/" not in row: continue # 提取学生姓名 user_match = re.search(r'href="/user/\d+"[^>]*>(.*?)', row, re.DOTALL) if not user_match: continue name = strip_tags(user_match.group(1)) # 只处理目标学生 if name != student_name: continue # 提取题目ID problem_match = re.search(r'href="/p/([^"?]+)', row) if not problem_match: continue problem_id = problem_match.group(1) # 提取状态 status = detect_status(row) # 提取提交时间 time_match = re.search(r'(\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{2}:\d{2})', row) submit_time = time_match.group(1) if time_match else "" student_records.append({ 'problem_id': problem_id, 'status': status, 'submit_time': submit_time }) except Exception as e: return f"❌ 错误:解析提交记录失败 - {e}" # 4. 生成Markdown报告 if not student_records: return f"📝 {student_name} 在本次作业中暂无提交记录" # 按题目分组统计 problem_stats = {} for record in student_records: pid = record['problem_id'] if pid not in problem_stats: problem_stats[pid] = { 'attempts': 0, 'statuses': [], 'final_status': None } problem_stats[pid]['attempts'] += 1 problem_stats[pid]['statuses'].append(record['status']) problem_stats[pid]['final_status'] = record['status'] # 构建题目名称映射 problem_names = {} for pid, code, name in problems: problem_names[pid] = name.strip() if name.strip() else code # 生成Markdown total_problems = len(problems) solved_problems = sum(1 for stats in problem_stats.values() if stats['final_status'] == 'AC') total_submits = len(student_records) md_lines = [ f"## 【OJ做题数据】", f"", f"**完成情况**: {solved_problems}/{total_problems} (共{total_submits}次提交)", f"", "| 题目 | 状态 | 提交次数 | 错误类型 |", "|------|------|----------|----------|", ] for pid, code, name in problems: if pid in problem_stats: stats = problem_stats[pid] status = "✅ 通过" if stats['final_status'] == 'AC' else "❌ 未通过" attempts = stats['attempts'] # 统计错误类型 error_types = [] for s in stats['statuses']: if s not in ('AC', 'UNKNOWN'): error_types.append(s) error_str = ", ".join(f"{e}×{error_types.count(e)}" for e in set(error_types)) if error_types else "—" problem_display = f"{code} {problem_names.get(pid, '')}".strip() md_lines.append(f"| {problem_display} | {status} | {attempts} | {error_str} |") else: problem_display = f"{code} {problem_names.get(pid, '')}".strip() md_lines.append(f"| {problem_display} | ⬜ 未提交 | 0 | — |") # 分析总结 md_lines.extend([ f"", f"**📊 分析**: {student_name}完成{solved_problems}/{total_problems}题" ]) return "\n".join(md_lines) def main(): import argparse parser = argparse.ArgumentParser(description="获取学生OJ做题数据") parser.add_argument("--homework-id", required=True, help="作业ID") parser.add_argument("--student", required=True, help="学生姓名") args = parser.parse_args() result = get_student_oj_data(args.homework_id, args.student) print(result) if __name__ == "__main__": main()