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()
|
||||
@@ -1,117 +0,0 @@
|
||||
const axios = require('axios');
|
||||
require('dotenv').config();
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL;
|
||||
const AUTHORIZATION = process.env.AUTHORIZATION;
|
||||
|
||||
// 查询橙子老师上周整体出勤率
|
||||
async function getTeacherAttendanceRate() {
|
||||
try {
|
||||
console.log('🔍 正在查询橙子老师上周整体出勤率...');
|
||||
const response = await axios.get(`${API_BASE_URL}/reports/teacher-attendance-rates`, {
|
||||
headers: {
|
||||
'Authorization': AUTHORIZATION
|
||||
},
|
||||
params: {
|
||||
teacher_name: '橙子(程城)',
|
||||
begin_date: '2026-04-21',
|
||||
end_date: '2026-04-27'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 整体出勤率数据获取成功:');
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ 查询失败:', error.response ? error.response.data : error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询上周每天的详细课程和学生出勤明细
|
||||
async function getDailyTeachingSchedule() {
|
||||
const dates = [
|
||||
'2026-04-21', '2026-04-22', '2026-04-23', '2026-04-24',
|
||||
'2026-04-25', '2026-04-26', '2026-04-27'
|
||||
];
|
||||
|
||||
const allData = [];
|
||||
|
||||
for (const date of dates) {
|
||||
try {
|
||||
console.log(`\n🔍 正在查询 ${date} 的课程明细...`);
|
||||
const response = await axios.get(`${API_BASE_URL}/reports/teaching-schedule`, {
|
||||
headers: {
|
||||
'Authorization': AUTHORIZATION
|
||||
},
|
||||
params: {
|
||||
teacher_name: '橙子(程城)',
|
||||
teaching_date: date
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.code === 0 && response.data.data.items.length > 0) {
|
||||
allData.push({
|
||||
date: date,
|
||||
courses: response.data.data.items
|
||||
});
|
||||
console.log(`✅ ${date} 找到 ${response.data.data.items.length} 节课`);
|
||||
} else {
|
||||
console.log(`ℹ️ ${date} 没有课程安排`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 查询 ${date} 失败:`, error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
// 1. 获取整体出勤率
|
||||
const overallRate = await getTeacherAttendanceRate();
|
||||
|
||||
// 2. 获取每日明细
|
||||
const dailyDetails = await getDailyTeachingSchedule();
|
||||
|
||||
// 3. 汇总数据并保存
|
||||
const result = {
|
||||
query_range: '2026-04-21 至 2026-04-27',
|
||||
teacher_name: '橙子(程城)',
|
||||
overall_attendance_rate: overallRate,
|
||||
daily_course_details: dailyDetails
|
||||
};
|
||||
|
||||
// 保存到文件
|
||||
const fs = require('fs');
|
||||
const outputPath = '上周学生出勤原始数据.json';
|
||||
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
||||
|
||||
console.log(`\n🎉 所有数据查询完成,已保存到:${outputPath}`);
|
||||
console.log('\n📊 数据摘要:');
|
||||
|
||||
// 统计总出勤情况
|
||||
let totalStudents = 0;
|
||||
let attendedStudents = 0;
|
||||
let leaveStudents = 0;
|
||||
let absentStudents = 0;
|
||||
|
||||
dailyDetails.forEach(day => {
|
||||
day.courses.forEach(course => {
|
||||
course.students.forEach(student => {
|
||||
totalStudents++;
|
||||
if (student.attendance_status === '✅ 出勤') attendedStudents++;
|
||||
else if (student.attendance_status === '⏸ 请假') leaveStudents++;
|
||||
else if (student.attendance_status === '❌ 缺勤') absentStudents++;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`- 总学生人次:${totalStudents}`);
|
||||
console.log(`- 正常出勤:${attendedStudents} 人次(${totalStudents > 0 ? ((attendedStudents / totalStudents) * 100).toFixed(2) : 0}%)`);
|
||||
console.log(`- 请假:${leaveStudents} 人次(${totalStudents > 0 ? ((leaveStudents / totalStudents) * 100).toFixed(2) : 0}%)`);
|
||||
console.log(`- 缺勤:${absentStudents} 人次(${totalStudents > 0 ? ((absentStudents / totalStudents) * 100).toFixed(2) : 0}%)`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user