final-vibevault-template/.autograde/objective_grade.py
2025-12-01 22:12:02 +08:00

445 lines
13 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
"""
选择题/判断题评分脚本
读取学生答案和标准答案,生成成绩 JSON 文件
"""
import json
import argparse
import sys
from datetime import datetime
from pathlib import Path
def load_answers(answer_file):
"""
加载学生答案文件(支持 JSON 和简单文本格式)
JSON 格式示例:
{
"MC1": "A",
"MC2": "B",
"TF1": true,
"TF2": false
}
文本格式示例(每行一个答案):
A
B
true
false
"""
try:
with open(answer_file, 'r', encoding='utf-8') as f:
content = f.read().strip()
# 尝试作为 JSON 加载
if content.startswith('{'):
return json.loads(content)
# 否则按行加载,忽略空行和注释
lines = [line.strip() for line in content.split('\n') if line.strip() and not line.strip().startswith('#')]
# 转换为字典格式:{"MC1": answer, "MC2": answer, ...}
answers = {}
for i, line in enumerate(lines, 1):
# 尝试识别题型
if line.lower() in ('true', 'false', 't', 'f'):
question_id = f"TF{len([k for k in answers if k.startswith('TF')])+1}"
answers[question_id] = line.lower() in ('true', 't')
else:
question_id = f"MC{len([k for k in answers if k.startswith('MC')])+1}"
answers[question_id] = line.upper()
return answers
except Exception as e:
print(f"Error loading answers: {e}", file=sys.stderr)
return {}
def load_standard_answers(std_file):
"""加载标准答案文件JSON 格式)"""
try:
with open(std_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading standard answers: {e}", file=sys.stderr)
return {}
def grade_multiple_choice(student_answers, standard_answers, question_texts=None):
"""
评选择题
Parameters
----------
student_answers : dict
学生答案,格式 {"MC1": "A", "MC2": "B", ...}
standard_answers : dict
标准答案,格式 {"MC1": "A", "MC2": "B", ...}
question_texts : dict, optional
题目文本,格式 {"MC1": "题目文本", ...}
Returns
-------
dict
成绩数据
"""
questions = []
correct_count = 0
for question_id, std_answer in standard_answers.items():
if not question_id.startswith('MC'):
continue
student_answer = student_answers.get(question_id, "")
is_correct = str(student_answer).upper() == str(std_answer).upper()
if is_correct:
correct_count += 1
score = 1
else:
score = 0
questions.append({
"question_id": question_id,
"question_text": question_texts.get(question_id, "") if question_texts else "",
"correct_answer": str(std_answer).upper(),
"student_answer": str(student_answer).upper(),
"correct": is_correct,
"score": score,
"max_score": 1
})
total_count = len(questions)
return {
"type": "multiple_choice",
"score": correct_count,
"max_score": total_count,
"details": {
"correct": correct_count,
"total": total_count,
"questions": questions
}
}
def grade_true_false(student_answers, standard_answers, question_texts=None):
"""
评判断题
Parameters
----------
student_answers : dict
学生答案,格式 {"TF1": true, "TF2": false, ...}
standard_answers : dict
标准答案,格式 {"TF1": true, "TF2": false, ...}
question_texts : dict, optional
题目文本
Returns
-------
dict
成绩数据
"""
questions = []
correct_count = 0
for question_id, std_answer in standard_answers.items():
if not question_id.startswith('TF'):
continue
student_answer = student_answers.get(question_id, None)
# 规范化布尔值
if isinstance(student_answer, str):
student_answer = student_answer.lower() in ('true', 't', '1', 'yes')
is_correct = bool(student_answer) == bool(std_answer)
if is_correct:
correct_count += 1
score = 1
else:
score = 0
questions.append({
"question_id": question_id,
"question_text": question_texts.get(question_id, "") if question_texts else "",
"correct_answer": bool(std_answer),
"student_answer": bool(student_answer) if student_answer is not None else None,
"correct": is_correct,
"score": score,
"max_score": 1
})
total_count = len(questions)
return {
"type": "true_false",
"score": correct_count,
"max_score": total_count,
"details": {
"correct": correct_count,
"total": total_count,
"questions": questions
}
}
def grade_multiple_select(student_answers, standard_answers, question_texts=None):
"""
评多选题
Parameters
----------
student_answers : dict
学生答案,格式 {"MS1": ["A", "B"], "MS2": ["C"], ...}
standard_answers : dict
标准答案,格式 {"MS1": ["A", "B"], "MS2": ["C"], ...}
question_texts : dict, optional
题目文本
Returns
-------
dict
成绩数据
"""
questions = []
correct_count = 0
for question_id, std_answer in standard_answers.items():
if not question_id.startswith('MS'):
continue
student_answer = student_answers.get(question_id, [])
# 规范化答案(转为大写并排序)
if isinstance(student_answer, str):
student_answer = [student_answer]
if not isinstance(student_answer, list):
student_answer = []
std_set = set([str(a).upper() for a in std_answer])
stu_set = set([str(a).upper() for a in student_answer])
is_correct = std_set == stu_set
if is_correct:
correct_count += 1
score = 1
else:
score = 0
questions.append({
"question_id": question_id,
"question_text": question_texts.get(question_id, "") if question_texts else "",
"correct_answer": sorted(list(std_set)),
"student_answer": sorted(list(stu_set)) if stu_set else [],
"correct": is_correct,
"score": score,
"max_score": 1
})
total_count = len(questions)
return {
"type": "multiple_select",
"score": correct_count,
"max_score": total_count,
"details": {
"correct": correct_count,
"total": total_count,
"questions": questions
}
}
def grade_fill_blank(student_answers, standard_answers, question_texts=None):
"""
评填空题
Parameters
----------
student_answers : dict
学生答案,格式 {"FB1": "答案", "FB2": ["答案1", "答案2"], ...}
standard_answers : dict
标准答案,格式同上
question_texts : dict, optional
题目文本
Returns
-------
dict
成绩数据
"""
questions = []
correct_count = 0
def normalize_answer(ans):
"""规范化答案:去除空格、转小写"""
if isinstance(ans, str):
return ans.strip().lower()
elif isinstance(ans, list):
return [a.strip().lower() for a in ans]
return ans
def compare_answers(student, standard):
"""比较答案是否相等"""
student_norm = normalize_answer(student)
standard_norm = normalize_answer(standard)
if isinstance(standard_norm, list) and isinstance(student_norm, list):
return student_norm == standard_norm
elif isinstance(standard_norm, str) and isinstance(student_norm, str):
return student_norm == standard_norm
return False
for question_id, std_answer in standard_answers.items():
if not question_id.startswith('FB'):
continue
student_answer = student_answers.get(question_id, "")
is_correct = compare_answers(student_answer, std_answer)
if is_correct:
correct_count += 1
score = 1
else:
score = 0
questions.append({
"question_id": question_id,
"question_text": question_texts.get(question_id, "") if question_texts else "",
"correct_answer": std_answer,
"student_answer": student_answer,
"correct": is_correct,
"score": score,
"max_score": 1
})
total_count = len(questions)
return {
"type": "fill_blank",
"score": correct_count,
"max_score": total_count,
"details": {
"correct": correct_count,
"total": total_count,
"questions": questions
}
}
def main():
parser = argparse.ArgumentParser(description="Grade objective questions")
parser.add_argument("--answers", required=True, help="Student answers file (JSON or text)")
parser.add_argument("--standard", required=True, help="Standard answers file (JSON)")
parser.add_argument("--questions", help="Question texts file (JSON, optional)")
parser.add_argument("--out", default="grade.json", help="Output grade JSON file")
parser.add_argument("--summary", default="summary.md", help="Output summary markdown file")
parser.add_argument("--type", choices=['mc', 'tf', 'ms', 'fb', 'all'], default='all',
help="Question type to grade")
args = parser.parse_args()
# 加载文件
student_answers = load_answers(args.answers)
standard_answers = load_standard_answers(args.standard)
question_texts = None
if args.questions:
try:
with open(args.questions, 'r', encoding='utf-8') as f:
question_texts = json.load(f)
except Exception as e:
print(f"Warning: Could not load question texts: {e}", file=sys.stderr)
if not student_answers or not standard_answers:
print("Error: Could not load answers", file=sys.stderr)
sys.exit(1)
# 评分
components = []
total_score = 0
total_max_score = 0
if args.type in ('mc', 'all'):
mc_grade = grade_multiple_choice(student_answers, standard_answers, question_texts)
if mc_grade['details']['total'] > 0:
components.append(mc_grade)
total_score += mc_grade['score']
total_max_score += mc_grade['max_score']
if args.type in ('tf', 'all'):
tf_grade = grade_true_false(student_answers, standard_answers, question_texts)
if tf_grade['details']['total'] > 0:
components.append(tf_grade)
total_score += tf_grade['score']
total_max_score += tf_grade['max_score']
if args.type in ('ms', 'all'):
ms_grade = grade_multiple_select(student_answers, standard_answers, question_texts)
if ms_grade['details']['total'] > 0:
components.append(ms_grade)
total_score += ms_grade['score']
total_max_score += ms_grade['max_score']
if args.type in ('fb', 'all'):
fb_grade = grade_fill_blank(student_answers, standard_answers, question_texts)
if fb_grade['details']['total'] > 0:
components.append(fb_grade)
total_score += fb_grade['score']
total_max_score += fb_grade['max_score']
# 生成 grade.json
grade_data = {
"score": total_score,
"max_score": total_max_score,
"components": components,
"timestamp": int(__import__('time').time())
}
with open(args.out, 'w', encoding='utf-8') as f:
json.dump(grade_data, f, ensure_ascii=False, indent=2)
# 生成 summary.md
summary_lines = [
"# 客观题评分\n",
f"- **总分**{total_score} / {total_max_score}\n",
f"- **组件数**{len(components)}\n",
""
]
for comp in components:
comp_type = comp['type']
correct = comp['details']['correct']
total = comp['details']['total']
type_names = {
'multiple_choice': '选择题',
'true_false': '判断题',
'multiple_select': '多选题',
'fill_blank': '填空题'
}
type_name = type_names.get(comp_type, comp_type)
summary_lines.append(f"## {type_name}\n")
summary_lines.append(f"- **正确**{correct} / {total}\n")
summary_lines.append("")
with open(args.summary, 'w', encoding='utf-8') as f:
f.write("\n".join(summary_lines))
print(f"Grading complete: {total_score}/{total_max_score}")
return 0
if __name__ == "__main__":
sys.exit(main())