Files
gaoandQwen-Coder 1701472642 feat: Enhanced PDF parsing and question extraction system
- Implemented enhanced PDF parsing with V2 parser for better question extraction
- Added improved question formatting and display components
- Integrated question navigation and progress tracking features
- Added JSON export functionality for paper questions
- Enhanced image extraction and association with questions
- Improved database configuration with environment variables
- Updated UI with responsive question cards and navigation sidebar
- Added mathematical symbol rendering and question type detection

Co-authored-by: Qwen-Coder <[email protected]>
2025-11-02 15:54:43 +08:00

648 lines
22 KiB
Python
Raw Permalink 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.
"""
题目数据格式化工具
用于标准化题目数据的JSON输出格式
"""
from typing import Dict, List, Any, Optional
from datetime import datetime
import json
import re
class QuestionFormatter:
"""题目数据格式化器"""
def __init__(self):
# 题目类型映射
self.type_mapping = {
'choice': 'single_choice',
'single_choice': 'single_choice',
'multiple_choice': 'multiple_choice',
'fill_blank': 'fill_blank',
'calculation': 'calculation',
'essay': 'essay',
'solve': 'solve',
'prove': 'prove',
'true_false': 'true_false',
'general': 'essay'
}
# 学科映射
self.subject_mapping = {
'math': '数学',
'chinese': '语文',
'english': '英语',
'physics': '物理',
'chemistry': '化学',
'biology': '生物',
'history': '历史',
'geography': '地理',
'politics': '政治'
}
# 难度等级映射
self.difficulty_mapping = {
1: '简单',
2: '较易',
3: '中等',
4: '较难',
5: '困难'
}
def format_question(self, raw_question: Dict, paper_info: Optional[Dict] = None) -> Dict:
"""
格式化单个题目数据
Args:
raw_question: 原始题目数据
paper_info: 试卷信息(可选)
Returns:
标准化的题目数据
"""
try:
# 基础信息
formatted = {
"id": raw_question.get("id"),
"number": self._extract_question_number(raw_question),
"type": self._normalize_question_type(raw_question),
"subject": self._normalize_subject(raw_question, paper_info),
"grade": raw_question.get("grade", ""),
"chapter": raw_question.get("chapter", ""),
"difficulty": self._normalize_difficulty(raw_question),
"score": self._extract_score(raw_question),
"estimated_time": self._estimate_time(raw_question)
}
# 题目内容
formatted.update({
"stem": self._clean_question_text(raw_question.get("question_content") or raw_question.get("text", "")),
"options": self._format_options(raw_question.get("options", {})),
"correct_answer": self._clean_answer(raw_question.get("correct_answer", "")),
"explanation": self._clean_text(raw_question.get("explanation", "")),
"analysis": self._clean_text(raw_question.get("analysis", "")),
"hints": self._extract_hints(raw_question)
})
# 知识点
formatted["knowledge_points"] = self._format_knowledge_points(raw_question.get("knowledge_points", []))
# 媒体资源
formatted.update({
"images": self._format_images(raw_question.get("images", [])),
"formulas": self._format_formulas(raw_question.get("formula", [])),
"audio": raw_question.get("audio", ""),
"video": raw_question.get("video", "")
})
# 来源信息
formatted.update({
"source": self._format_source(raw_question, paper_info),
"metadata": self._extract_metadata(raw_question)
})
# 统计信息
formatted["statistics"] = {
"created_at": self._format_datetime(raw_question.get("created_at")),
"updated_at": self._format_datetime(raw_question.get("updated_at")),
"view_count": raw_question.get("view_count", 0),
"attempt_count": raw_question.get("attempt_count", 0),
"correct_count": raw_question.get("correct_count", 0),
"accuracy_rate": self._calculate_accuracy(raw_question)
}
# 变体信息(子题)
formatted["variants"] = self._extract_variants(raw_question)
# 验证数据完整性
formatted["validation"] = self._validate_question(formatted)
return formatted
except Exception as e:
print(f"格式化题目失败: {e}")
return self._create_error_question(raw_question, str(e))
def format_paper_questions(self, paper_data: Dict) -> Dict:
"""
格式化试卷的所有题目
Args:
paper_data: 包含试卷信息和题目列表的数据
Returns:
标准化的试卷题目数据
"""
questions = paper_data.get("questions", [])
paper_info = {
"id": paper_data.get("paper_id"),
"title": paper_data.get("title", ""),
"subject": paper_data.get("subject", ""),
"grade": paper_data.get("grade", "")
}
formatted_questions = []
processing_errors = []
for i, raw_question in enumerate(questions):
try:
formatted = self.format_question(raw_question, paper_info)
formatted_questions.append(formatted)
except Exception as e:
processing_errors.append({
"index": i,
"question_id": raw_question.get("id"),
"error": str(e),
"raw_data": raw_question
})
return {
"paper_info": paper_info,
"questions": formatted_questions,
"statistics": self._calculate_paper_stats(formatted_questions),
"processing_errors": processing_errors,
"export_info": {
"exported_at": datetime.now().isoformat(),
"total_questions": len(formatted_questions),
"error_count": len(processing_errors),
"format_version": "2.0"
}
}
def _extract_question_number(self, question: Dict) -> str:
"""提取题号"""
number = question.get("question_number") or question.get("number") or question.get("index")
if number:
return str(number)
# 尝试从题干中提取题号
text = question.get("question_content") or question.get("text", "")
import re
match = re.search(r'^(\d+)[.).、]', text)
if match:
return match.group(1)
return ""
def _normalize_question_type(self, question: Dict) -> str:
"""标准化题目类型"""
raw_type = question.get("question_type") or question.get("type") or ""
# 如果有选项,默认为选择题
if question.get("options") and len(question.get("options", {})) > 0:
return "single_choice"
return self.type_mapping.get(raw_type, "essay")
def _normalize_subject(self, question: Dict, paper_info: Optional[Dict] = None) -> str:
"""标准化学科"""
subject = question.get("subject")
if subject:
return self.subject_mapping.get(subject.lower(), subject)
if paper_info and paper_info.get("subject"):
return self.subject_mapping.get(paper_info["subject"].lower(), paper_info["subject"])
return "未分类"
def _normalize_difficulty(self, question: Dict) -> Dict:
"""标准化难度信息"""
raw_difficulty = question.get("difficulty", 3)
try:
difficulty_value = int(raw_difficulty)
except (ValueError, TypeError):
difficulty_value = 3
return {
"level": max(1, min(5, difficulty_value)),
"label": self.difficulty_mapping.get(difficulty_value, "中等"),
"stars": "★" * max(1, min(5, difficulty_value))
}
def _extract_score(self, question: Dict) -> Dict:
"""提取分值信息"""
text = question.get("question_content") or question.get("text", "")
import re
# 查找分值模式
match = re.search(r'(\d+\.?\d*)\s*分', text)
if match:
score_value = float(match.group(1))
return {
"total": score_value,
"formatted": f"{score_value}分",
"extracted_from_text": True
}
# 查找其他分值模式
score_field = question.get("score")
if score_field:
try:
score_value = float(score_field)
return {
"total": score_value,
"formatted": f"{score_value}分",
"extracted_from_text": False
}
except (ValueError, TypeError):
pass
return {
"total": 0,
"formatted": "未标分",
"extracted_from_text": False
}
def _estimate_time(self, question: Dict) -> Dict:
"""估算答题时间"""
question_type = self._normalize_question_type(question)
text_length = len(question.get("question_content") or question.get("text", ""))
options_count = len(question.get("options", {}))
difficulty = self._normalize_difficulty(question)["level"]
# 基础时间(分钟)
base_times = {
"single_choice": 1,
"multiple_choice": 2,
"fill_blank": 2,
"calculation": 5,
"essay": 10,
"solve": 8,
"prove": 12,
"true_false": 0.5
}
base_time = base_times.get(question_type, 3)
# 根据难度调整
difficulty_multiplier = 0.5 + (difficulty * 0.3)
# 根据文本长度调整
text_multiplier = 1 + (text_length / 500)
estimated_time = base_time * difficulty_multiplier * text_multiplier
return {
"minutes": round(estimated_time, 1),
"seconds": round(estimated_time * 60),
"formatted": f"{round(estimated_time, 1)}分钟"
}
def _clean_question_text(self, text: str) -> str:
"""清理题目文本"""
if not text:
return ""
# 移除题号前缀
import re
text = re.sub(r'^\d+\s*[.).、]\s*', '', text)
# 移除分值信息
text = re.sub(r'^\d+\.?\d*\s*分[::]\s*', '', text)
# 清理多余空格
text = re.sub(r'\s+', ' ', text).strip()
return text
def _format_options(self, options: Dict) -> List[Dict]:
"""格式化选项"""
if not options:
return []
formatted_options = []
for key, value in options.items():
if not value or not value.strip():
continue
formatted_options.append({
"label": key.upper(),
"text": self._clean_text(value),
"is_correct": False, # 需要根据答案判断
"explanation": ""
})
# 按标签排序
formatted_options.sort(key=lambda x: x["label"])
return formatted_options
def _clean_answer(self, answer: str) -> str:
"""清理答案"""
if not answer:
return ""
# 移除多余空格
answer = " ".join(answer.split())
# 标准化多答案格式
if "," in answer:
answers = [a.strip() for a in answer.split(",")]
return ", ".join(sorted(answers))
return answer
def _clean_text(self, text: str) -> str:
"""清理普通文本"""
if not text:
return ""
# 清理多余空格和换行
text = re.sub(r'\s+', ' ', text).strip()
return text
def _extract_hints(self, question: Dict) -> List[str]:
"""提取提示信息"""
hints = []
# 从解析中提取提示
explanation = question.get("explanation", "")
if "提示" in explanation or "注意" in explanation:
import re
hint_matches = re.findall(r'[提示注意][::]\s*([^。!?\n]*?)[。!?\n]', explanation)
hints.extend(hint_matches)
return hints
def _format_knowledge_points(self, knowledge_points: List) -> List[Dict]:
"""格式化知识点"""
if not knowledge_points:
return []
formatted = []
for point in knowledge_points:
if isinstance(point, str):
formatted.append({
"name": point,
"category": "",
"weight": 1.0
})
elif isinstance(point, dict):
formatted.append({
"name": point.get("name", ""),
"category": point.get("category", ""),
"weight": point.get("weight", 1.0)
})
return formatted
def _format_images(self, images: List) -> List[Dict]:
"""格式化图片信息"""
if not images:
return []
formatted = []
for i, image in enumerate(images):
if isinstance(image, str):
formatted.append({
"id": f"img_{i+1}",
"url": image,
"alt": f"题目图片{i+1}",
"type": "image"
})
elif isinstance(image, dict):
formatted.append({
"id": image.get("id", f"img_{i+1}"),
"url": image.get("url") or image.get("uri") or image.get("key", ""),
"alt": image.get("alt", f"题目图片{i+1}"),
"type": "image",
"width": image.get("width"),
"height": image.get("height"),
"format": image.get("format", "unknown")
})
return formatted
def _format_formulas(self, formulas: List) -> List[Dict]:
"""格式化公式信息"""
if not formulas:
return []
formatted = []
for i, formula in enumerate(formulas):
if isinstance(formula, dict):
formatted.append({
"id": formula.get("id", f"formula_{i+1}"),
"latex": formula.get("latex", ""),
"position": formula.get("bbox", []),
"type": "formula"
})
return formatted
def _format_source(self, question: Dict, paper_info: Optional[Dict] = None) -> Dict:
"""格式化来源信息"""
source = {
"type": "auto_extracted",
"method": question.get("source", "unknown"),
"paper": {
"id": paper_info.get("id") if paper_info else None,
"title": paper_info.get("title", "") if paper_info else "",
"page": question.get("page", 1)
},
"extraction_confidence": 0.8, # 提取置信度
"verified": False # 是否已人工验证
}
return source
def _extract_metadata(self, question: Dict) -> Dict:
"""提取元数据"""
return {
"extraction_method": question.get("auto_extracted", False) and "automatic" or "manual",
"original_format": "pdf",
"language": "zh-CN",
"tags": question.get("tags", []),
"notes": question.get("notes", ""),
"page_spans": question.get("page_spans", []),
"source_text": question.get("source_text", "")
}
def _format_datetime(self, dt) -> str:
"""格式化日期时间"""
if not dt:
return ""
if isinstance(dt, str):
return dt
if hasattr(dt, 'isoformat'):
return dt.isoformat()
return str(dt)
def _calculate_accuracy(self, question: Dict) -> float:
"""计算正确率"""
attempt_count = question.get("attempt_count", 0)
correct_count = question.get("correct_count", 0)
if attempt_count == 0:
return 0.0
return round(correct_count / attempt_count, 2)
def _extract_variants(self, question: Dict) -> List[Dict]:
"""提取子题/变体"""
variants = []
text = question.get("question_content") or question.get("text", "")
# 查找子题模式 (1), (2), (3) 等
import re
sub_matches = re.findall(r'\((\d+)\)\s*([^()]*?)(?=\(\d+\)|$)', text)
for num, content in sub_matches:
if content.strip():
variants.append({
"number": num,
"type": "subquestion",
"content": self._clean_text(content),
"score": None # 可以尝试提取子题分值
})
return variants
def _validate_question(self, question: Dict) -> Dict:
"""验证题目数据完整性"""
validation = {
"is_valid": True,
"errors": [],
"warnings": [],
"completeness_score": 1.0
}
# 必需字段检查
required_fields = ["stem", "type"]
for field in required_fields:
if not question.get(field):
validation["errors"].append(f"缺少必需字段: {field}")
validation["is_valid"] = False
# 题目类型特定检查
if question["type"] in ["single_choice", "multiple_choice"]:
if not question.get("options"):
validation["errors"].append("选择题缺少选项")
validation["is_valid"] = False
elif not question.get("correct_answer"):
validation["warnings"].append("选择题缺少正确答案")
# 内容质量检查
stem_length = len(question.get("stem", ""))
if stem_length < 5:
validation["warnings"].append("题干内容过短")
elif stem_length > 2000:
validation["warnings"].append("题干内容过长")
# 计算完整性分数
total_fields = 8 # 总共评估的字段数
filled_fields = sum(1 for field in ["stem", "options", "correct_answer", "explanation", "knowledge_points", "difficulty", "score", "images"]
if question.get(field))
validation["completeness_score"] = filled_fields / total_fields
return validation
def _calculate_paper_stats(self, questions: List[Dict]) -> Dict:
"""计算试卷统计信息"""
if not questions:
return {}
stats = {
"total_questions": len(questions),
"question_types": {},
"difficulty_distribution": {"简单": 0, "较易": 0, "中等": 0, "较难": 0, "困难": 0},
"total_score": 0,
"estimated_time": 0,
"knowledge_points": [],
"has_images": 0,
"average_accuracy": 0.0
}
all_accuracy = []
all_knowledge_points = set()
for question in questions:
# 题目类型统计
q_type = question.get("type", "unknown")
stats["question_types"][q_type] = stats["question_types"].get(q_type, 0) + 1
# 难度分布
difficulty = question.get("difficulty", {}).get("label", "中等")
stats["difficulty_distribution"][difficulty] = stats["difficulty_distribution"].get(difficulty, 0) + 1
# 分值统计
score = question.get("score", {}).get("total", 0)
stats["total_score"] += score
# 时间估算
time_minutes = question.get("estimated_time", {}).get("minutes", 0)
stats["estimated_time"] += time_minutes
# 知识点统计
for kp in question.get("knowledge_points", []):
kp_name = kp.get("name", kp) if isinstance(kp, dict) else kp
all_knowledge_points.add(kp_name)
# 图片统计
if question.get("images"):
stats["has_images"] += 1
# 正确率统计
accuracy = question.get("statistics", {}).get("accuracy_rate", 0)
if accuracy > 0:
all_accuracy.append(accuracy)
stats["knowledge_points"] = list(all_knowledge_points)
if all_accuracy:
stats["average_accuracy"] = round(sum(all_accuracy) / len(all_accuracy), 2)
# 格式化时间
total_minutes = stats["estimated_time"]
if total_minutes >= 60:
hours = int(total_minutes // 60)
minutes = int(total_minutes % 60)
stats["estimated_time_formatted"] = f"{hours}小时{minutes}分钟"
else:
stats["estimated_time_formatted"] = f"{int(total_minutes)}分钟"
return stats
def _create_error_question(self, raw_question: Dict, error_msg: str) -> Dict:
"""创建错误题目对象"""
return {
"id": raw_question.get("id"),
"number": self._extract_question_number(raw_question),
"type": "error",
"stem": f"[数据解析错误] {error_msg}",
"options": [],
"correct_answer": "",
"explanation": "",
"validation": {
"is_valid": False,
"errors": [error_msg],
"warnings": [],
"completeness_score": 0.0
},
"source": {
"type": "error",
"method": "error",
"error_message": error_msg
}
}
def export_to_json_file(self, data: Dict, file_path: str, indent: int = 2) -> bool:
"""导出到JSON文件"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=indent, default=str)
return True
except Exception as e:
print(f"导出JSON文件失败: {e}")
return False
def export_to_json_string(self, data: Dict, indent: int = 2) -> str:
"""导出为JSON字符串"""
try:
return json.dumps(data, ensure_ascii=False, indent=indent, default=str)
except Exception as e:
print(f"导出JSON字符串失败: {e}")
return json.dumps({"error": str(e)}, ensure_ascii=False, indent=indent)