feat: 第16课课评完成(31人)+ 补全AICODE03周五1700班级总结 + 补课学生课评 + OJ数据
This commit is contained in:
238
scripts/get_student_oj.py
Normal file
238
scripts/get_student_oj.py
Normal 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>(?: |\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()
|
||||
Reference in New Issue
Block a user