Files
ClassFeedback/scripts/get_student_oj.py

239 lines
7.6 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
"""
简化版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'"[^>]*><b>([^<]+)</b>(?:&nbsp;|\s)*([^<]*)</a>'
)
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'<tr[^>]*>(.*?)</tr>', 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+"[^>]*>(.*?)</a>', 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()