feat: 第16课课评完成(31人)+ 补全AICODE03周五1700班级总结 + 补课学生课评 + OJ数据

This commit is contained in:
chengzi
2026-06-23 13:10:02 +08:00
parent 7dd9c1dced
commit d093831f74
102 changed files with 3211 additions and 551 deletions

238
scripts/get_student_oj.py Normal file
View File

@@ -0,0 +1,238 @@
#!/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()