|
|
|
|
@ -0,0 +1,506 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
中小学数学卷子自动生成程序
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
import re
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import List, Set, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class User:
|
|
|
|
|
"""用户类"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, username: str, password: str, user_type: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
初始化用户
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
username: 用户名
|
|
|
|
|
password: 密码
|
|
|
|
|
user_type: 用户类型(小学/初中/高中)
|
|
|
|
|
"""
|
|
|
|
|
self.username = username
|
|
|
|
|
self.password = password
|
|
|
|
|
self.user_type = user_type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QuestionGenerator:
|
|
|
|
|
"""题目生成器基类"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""初始化题目生成器"""
|
|
|
|
|
self.operators = ['+', '-', '*', '/']
|
|
|
|
|
|
|
|
|
|
def generate_question(self) -> str:
|
|
|
|
|
"""生成一道题目"""
|
|
|
|
|
raise NotImplementedError("子类必须实现此方法")
|
|
|
|
|
|
|
|
|
|
def generate_operands(self, count: int) -> List[int]:
|
|
|
|
|
"""
|
|
|
|
|
生成操作数
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
count: 操作数数量
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
操作数列表
|
|
|
|
|
"""
|
|
|
|
|
return [random.randint(1, 100) for _ in range(count)]
|
|
|
|
|
|
|
|
|
|
def generate_operator(self) -> str:
|
|
|
|
|
"""生成运算符"""
|
|
|
|
|
return random.choice(self.operators)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PrimarySchoolGenerator(QuestionGenerator):
|
|
|
|
|
"""小学题目生成器"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""初始化小学题目生成器"""
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.bracket_probability = 0.3 # 括号概率
|
|
|
|
|
self.min_operands = 2
|
|
|
|
|
self.max_operands = 5
|
|
|
|
|
|
|
|
|
|
def generate_question(self) -> str:
|
|
|
|
|
"""生成小学题目"""
|
|
|
|
|
num_operands = random.randint(self.min_operands, self.max_operands)
|
|
|
|
|
operands = self.generate_operands(num_operands)
|
|
|
|
|
|
|
|
|
|
# 小学使用+,-,*,/和()
|
|
|
|
|
operators = [random.choice(['+', '-', '*', '/'])
|
|
|
|
|
for _ in range(num_operands - 1)]
|
|
|
|
|
|
|
|
|
|
# 根据概率添加括号
|
|
|
|
|
if (random.random() < self.bracket_probability and
|
|
|
|
|
num_operands >= 3):
|
|
|
|
|
question = self._generate_with_brackets(operands, operators)
|
|
|
|
|
else:
|
|
|
|
|
question = str(operands[0])
|
|
|
|
|
for i in range(num_operands - 1):
|
|
|
|
|
question += f" {operators[i]} {operands[i + 1]}"
|
|
|
|
|
|
|
|
|
|
return question
|
|
|
|
|
|
|
|
|
|
def _generate_with_brackets(self, operands: List[int],
|
|
|
|
|
operators: List[str]) -> str:
|
|
|
|
|
"""
|
|
|
|
|
生成带括号的表达式
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
operands: 操作数列表
|
|
|
|
|
operators: 运算符列表
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
带括号的表达式字符串
|
|
|
|
|
"""
|
|
|
|
|
bracket_pos = random.randint(0, len(operands) - 2)
|
|
|
|
|
question = ""
|
|
|
|
|
|
|
|
|
|
for i in range(len(operands)):
|
|
|
|
|
if i == bracket_pos:
|
|
|
|
|
question += "("
|
|
|
|
|
|
|
|
|
|
question += str(operands[i])
|
|
|
|
|
|
|
|
|
|
if i == bracket_pos + 1:
|
|
|
|
|
question += ")"
|
|
|
|
|
|
|
|
|
|
if i < len(operators):
|
|
|
|
|
question += f" {operators[i]}"
|
|
|
|
|
|
|
|
|
|
return question
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MiddleSchoolGenerator(QuestionGenerator):
|
|
|
|
|
"""初中题目生成器"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""初始化初中题目生成器"""
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.min_operands = 2
|
|
|
|
|
self.max_operands = 5
|
|
|
|
|
self.special_operator_probability = 0.5
|
|
|
|
|
|
|
|
|
|
def generate_question(self) -> str:
|
|
|
|
|
"""生成初中题目"""
|
|
|
|
|
num_operands = random.randint(self.min_operands, self.max_operands)
|
|
|
|
|
operands = self.generate_operands(num_operands)
|
|
|
|
|
operators = [self.generate_operator() for _ in range(num_operands - 1)]
|
|
|
|
|
|
|
|
|
|
# 确保至少有一个平方或开根号运算符
|
|
|
|
|
question_parts = []
|
|
|
|
|
has_special_operator = False
|
|
|
|
|
|
|
|
|
|
for i in range(num_operands):
|
|
|
|
|
# 根据概率对操作数进行特殊运算
|
|
|
|
|
if (not has_special_operator and
|
|
|
|
|
random.random() < self.special_operator_probability):
|
|
|
|
|
if random.choice([True, False]):
|
|
|
|
|
# 平方
|
|
|
|
|
question_parts.append(f"{operands[i]}²")
|
|
|
|
|
else:
|
|
|
|
|
# 开根号
|
|
|
|
|
question_parts.append(f"√{operands[i]}")
|
|
|
|
|
has_special_operator = True
|
|
|
|
|
else:
|
|
|
|
|
question_parts.append(str(operands[i]))
|
|
|
|
|
|
|
|
|
|
if i < len(operators):
|
|
|
|
|
question_parts.append(operators[i])
|
|
|
|
|
|
|
|
|
|
# 如果没有特殊运算符,强制添加一个
|
|
|
|
|
if not has_special_operator:
|
|
|
|
|
pos = random.randint(0, num_operands - 1)
|
|
|
|
|
if random.choice([True, False]):
|
|
|
|
|
question_parts[pos * 2] = f"{operands[pos]}²"
|
|
|
|
|
else:
|
|
|
|
|
question_parts[pos * 2] = f"√{operands[pos]}"
|
|
|
|
|
|
|
|
|
|
return " ".join(question_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HighSchoolGenerator(QuestionGenerator):
|
|
|
|
|
"""高中题目生成器"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""初始化高中题目生成器"""
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.trig_functions = ['sin', 'cos', 'tan']
|
|
|
|
|
self.min_operands = 2
|
|
|
|
|
self.max_operands = 5
|
|
|
|
|
self.trig_probability = 0.5
|
|
|
|
|
self.bracket_probability = 0.3
|
|
|
|
|
self.max_angle = 90
|
|
|
|
|
|
|
|
|
|
def generate_question(self) -> str:
|
|
|
|
|
"""生成高中题目"""
|
|
|
|
|
num_operands = random.randint(self.min_operands, self.max_operands)
|
|
|
|
|
operands = self.generate_operands(num_operands)
|
|
|
|
|
operators = [self.generate_operator() for _ in range(num_operands - 1)]
|
|
|
|
|
|
|
|
|
|
# 确保至少有一个三角函数
|
|
|
|
|
question_parts = []
|
|
|
|
|
has_trig_function = False
|
|
|
|
|
|
|
|
|
|
for i in range(num_operands):
|
|
|
|
|
# 根据概率对操作数进行三角函数运算
|
|
|
|
|
if (not has_trig_function and
|
|
|
|
|
random.random() < self.trig_probability):
|
|
|
|
|
trig_func = random.choice(self.trig_functions)
|
|
|
|
|
angle = random.randint(1, self.max_angle) # 合理的角度范围
|
|
|
|
|
question_parts.append(f"{trig_func}({angle}°)")
|
|
|
|
|
has_trig_function = True
|
|
|
|
|
else:
|
|
|
|
|
question_parts.append(str(operands[i]))
|
|
|
|
|
|
|
|
|
|
if i < len(operators):
|
|
|
|
|
question_parts.append(operators[i])
|
|
|
|
|
|
|
|
|
|
# 如果没有三角函数,强制添加一个
|
|
|
|
|
if not has_trig_function:
|
|
|
|
|
pos = random.randint(0, num_operands - 1)
|
|
|
|
|
trig_func = random.choice(self.trig_functions)
|
|
|
|
|
angle = random.randint(1, self.max_angle)
|
|
|
|
|
question_parts[pos * 2] = f"{trig_func}({angle}°)"
|
|
|
|
|
|
|
|
|
|
# 高中题目可以包含括号
|
|
|
|
|
question = self._generate_with_brackets(question_parts)
|
|
|
|
|
return question
|
|
|
|
|
|
|
|
|
|
def _generate_with_brackets(self, question_parts: List[str]) -> str:
|
|
|
|
|
"""
|
|
|
|
|
生成带括号的表达式
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
question_parts: 题目部分列表
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
带括号的表达式字符串
|
|
|
|
|
"""
|
|
|
|
|
# 简单情况下直接返回
|
|
|
|
|
if len(question_parts) <= 5 or random.random() < self.bracket_probability:
|
|
|
|
|
return " ".join(question_parts)
|
|
|
|
|
|
|
|
|
|
# 添加括号
|
|
|
|
|
bracket_pos = random.randint(0, (len(question_parts) - 3) // 2) * 2
|
|
|
|
|
result_parts = []
|
|
|
|
|
|
|
|
|
|
for i, part in enumerate(question_parts):
|
|
|
|
|
if i == bracket_pos:
|
|
|
|
|
result_parts.append("(")
|
|
|
|
|
|
|
|
|
|
result_parts.append(part)
|
|
|
|
|
|
|
|
|
|
if i == bracket_pos + 2:
|
|
|
|
|
result_parts.append(")")
|
|
|
|
|
|
|
|
|
|
return " ".join(result_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExamSystem:
|
|
|
|
|
"""考试系统主类"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""初始化考试系统"""
|
|
|
|
|
# 预设用户账户(根据附表-1)
|
|
|
|
|
self.users = [
|
|
|
|
|
User("张三1", "123", "小学"),
|
|
|
|
|
User("张三2", "123", "小学"),
|
|
|
|
|
User("张三3", "123", "小学"),
|
|
|
|
|
User("李四1", "123", "初中"),
|
|
|
|
|
User("李四2", "123", "初中"),
|
|
|
|
|
User("李四3", "123", "初中"),
|
|
|
|
|
User("王五1", "123", "高中"),
|
|
|
|
|
User("王五2", "123", "高中"),
|
|
|
|
|
User("王五3", "123", "高中")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
self.current_user: Optional[User] = None
|
|
|
|
|
self.current_type: Optional[str] = None
|
|
|
|
|
self.generated_questions: Set[str] = set() # 存储已生成的题目用于去重
|
|
|
|
|
|
|
|
|
|
# 创建用户文件夹
|
|
|
|
|
self.create_user_folders()
|
|
|
|
|
|
|
|
|
|
def create_user_folders(self) -> None:
|
|
|
|
|
"""创建用户文件夹"""
|
|
|
|
|
# 获取项目根目录的绝对路径
|
|
|
|
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
for user in self.users:
|
|
|
|
|
folder_path = os.path.join(project_root, "papers", user.username)
|
|
|
|
|
os.makedirs(folder_path, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
def login(self) -> bool:
|
|
|
|
|
"""用户登录"""
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
input_str = input("请输入用户名和密码(用空格隔开):")
|
|
|
|
|
if input_str.strip().lower() == 'exit':
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
parts = input_str.split()
|
|
|
|
|
if len(parts) != 2:
|
|
|
|
|
print("请输入正确的用户名和密码格式!")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
username, password = parts
|
|
|
|
|
|
|
|
|
|
for user in self.users:
|
|
|
|
|
if user.username == username and user.password == password:
|
|
|
|
|
self.current_user = user
|
|
|
|
|
self.current_type = user.user_type
|
|
|
|
|
print(f"当前选择为 {self.current_type} 出题")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
print("请输入正确的用户名、密码")
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\n程序已退出")
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"输入错误:{e}")
|
|
|
|
|
|
|
|
|
|
def switch_type(self, new_type: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
切换题目类型
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
new_type: 新的题目类型
|
|
|
|
|
"""
|
|
|
|
|
if new_type in ["小学", "初中", "高中"]:
|
|
|
|
|
self.current_type = new_type
|
|
|
|
|
print(f"已切换为 {new_type} 出题")
|
|
|
|
|
else:
|
|
|
|
|
print("请输入小学、初中和高中三个选项中的一个")
|
|
|
|
|
|
|
|
|
|
def get_generator(self) -> QuestionGenerator:
|
|
|
|
|
"""
|
|
|
|
|
获取对应的题目生成器
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
题目生成器实例
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ValueError: 无效的题目类型
|
|
|
|
|
"""
|
|
|
|
|
if self.current_type == "小学":
|
|
|
|
|
return PrimarySchoolGenerator()
|
|
|
|
|
elif self.current_type == "初中":
|
|
|
|
|
return MiddleSchoolGenerator()
|
|
|
|
|
elif self.current_type == "高中":
|
|
|
|
|
return HighSchoolGenerator()
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("无效的题目类型")
|
|
|
|
|
|
|
|
|
|
def load_existing_questions(self, username: str) -> Set[str]:
|
|
|
|
|
"""
|
|
|
|
|
加载该用户已生成的所有题目
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
username: 用户名
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
已存在的题目集合
|
|
|
|
|
"""
|
|
|
|
|
existing_questions: Set[str] = set()
|
|
|
|
|
# 获取项目根目录的绝对路径
|
|
|
|
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
user_folder = os.path.join(project_root, "papers", username)
|
|
|
|
|
|
|
|
|
|
if os.path.exists(user_folder):
|
|
|
|
|
for filename in os.listdir(user_folder):
|
|
|
|
|
if filename.endswith('.txt'):
|
|
|
|
|
filepath = os.path.join(user_folder, filename)
|
|
|
|
|
try:
|
|
|
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
# 提取所有题目(忽略题号和空行)
|
|
|
|
|
questions = re.findall(r'\d+\.\s*(.+?)(?=\n\n|$)',
|
|
|
|
|
content, re.DOTALL)
|
|
|
|
|
existing_questions.update(questions)
|
|
|
|
|
except Exception:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
return existing_questions
|
|
|
|
|
|
|
|
|
|
def generate_unique_question(self, generator: QuestionGenerator,
|
|
|
|
|
existing_questions: Set[str]) -> str:
|
|
|
|
|
"""
|
|
|
|
|
生成唯一的题目
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
generator: 题目生成器
|
|
|
|
|
existing_questions: 已存在的题目集合
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
唯一的题目字符串
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
Exception: 无法生成唯一题目
|
|
|
|
|
"""
|
|
|
|
|
max_attempts = 100
|
|
|
|
|
for _ in range(max_attempts):
|
|
|
|
|
question = generator.generate_question()
|
|
|
|
|
if question not in existing_questions:
|
|
|
|
|
existing_questions.add(question)
|
|
|
|
|
return question
|
|
|
|
|
|
|
|
|
|
raise Exception("无法生成唯一题目,请尝试减少题目数量")
|
|
|
|
|
|
|
|
|
|
def generate_exam_paper(self, num_questions: int) -> None:
|
|
|
|
|
"""
|
|
|
|
|
生成试卷
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
num_questions: 题目数量
|
|
|
|
|
"""
|
|
|
|
|
min_questions = 10
|
|
|
|
|
max_questions = 30
|
|
|
|
|
|
|
|
|
|
if not min_questions <= num_questions <= max_questions:
|
|
|
|
|
print(f"题目数量必须在{min_questions}-{max_questions}之间")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 加载该用户已存在的题目
|
|
|
|
|
existing_questions = self.load_existing_questions(
|
|
|
|
|
self.current_user.username)
|
|
|
|
|
|
|
|
|
|
generator = self.get_generator()
|
|
|
|
|
questions: List[str] = []
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
for i in range(num_questions):
|
|
|
|
|
question = self.generate_unique_question(generator,
|
|
|
|
|
existing_questions)
|
|
|
|
|
questions.append(question)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"生成题目时出错:{e}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 生成文件名
|
|
|
|
|
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
|
|
|
filename = f"{timestamp}.txt"
|
|
|
|
|
# 获取项目根目录的绝对路径
|
|
|
|
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
user_folder = os.path.join(project_root, "papers",
|
|
|
|
|
self.current_user.username)
|
|
|
|
|
filepath = os.path.join(user_folder, filename)
|
|
|
|
|
|
|
|
|
|
# 写入文件
|
|
|
|
|
try:
|
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
|
|
|
for i, question in enumerate(questions, 1):
|
|
|
|
|
f.write(f"{i}. {question}\n\n")
|
|
|
|
|
print(f"试卷已生成:{filepath}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"保存文件时出错:{e}")
|
|
|
|
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
|
"""运行主程序"""
|
|
|
|
|
print("=== 中小学数学卷子自动生成程序 ===")
|
|
|
|
|
print("输入'exit'可以退出程序")
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
# 登录
|
|
|
|
|
if not self.login():
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# 登录后的主循环
|
|
|
|
|
while self.current_user:
|
|
|
|
|
try:
|
|
|
|
|
print(f"\n准备生成 {self.current_type} 数学题目,"
|
|
|
|
|
f"请输入生成题目数量(输入-1将退出当前用户,重新登录):")
|
|
|
|
|
|
|
|
|
|
user_input = input(">>> ").strip()
|
|
|
|
|
|
|
|
|
|
if user_input.lower() == 'exit':
|
|
|
|
|
print("程序已退出")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 检查切换命令
|
|
|
|
|
if user_input.startswith("切换为"):
|
|
|
|
|
parts = user_input.split("切换为")
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
new_type = parts[1].strip()
|
|
|
|
|
self.switch_type(new_type)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 处理题目数量输入
|
|
|
|
|
if user_input == "-1":
|
|
|
|
|
print("退出当前用户")
|
|
|
|
|
self.current_user = None
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
num_questions = int(user_input)
|
|
|
|
|
if num_questions == -1:
|
|
|
|
|
print("退出当前用户")
|
|
|
|
|
self.current_user = None
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
self.generate_exam_paper(num_questions)
|
|
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
print("请输入有效的数字(10-30)或-1退出")
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\n程序已退出")
|
|
|
|
|
return
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"发生错误:{e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
"""主函数"""
|
|
|
|
|
system = ExamSystem()
|
|
|
|
|
system.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|