完成需求 #1

Merged
hnu202329010109 merged 3 commits from develop into main 4 months ago

@ -0,0 +1,267 @@
# 中小学数学卷子自动生成程序 - 详细说明文档
## 目录
1. [项目概述](#项目概述)
2. [系统架构](#系统架构)
3. [功能详解](#功能详解)
4. [使用指南](#使用指南)
5. [技术实现](#技术实现)
6. [测试用例](#测试用例)
7. [部署说明](#部署说明)
## 项目概述
本程序是一个面向中小学数学教师的智能试卷生成系统,能够根据不同的学段要求自动生成符合教学大纲的数学题目。
### 设计目标
- 为小学、初中、高中教师提供个性化的试卷生成服务
- 确保题目难度与学段匹配
- 避免题目重复,保证每次生成的试卷都是唯一的
- 提供友好的命令行交互界面
## 系统架构
### 类图结构
```
ExamSystem
├── User
├── QuestionGenerator
│ ├── PrimarySchoolGenerator
│ ├── MiddleSchoolGenerator
│ └── HighSchoolGenerator
└── 文件管理模块
```
### 主要类说明
#### 1. User类
- 属性username, password, user_type
- 功能:存储用户信息和权限
#### 2. QuestionGenerator基类
- 抽象方法generate_question()
- 通用方法:生成操作数、运算符
#### 3. 各学段生成器
- PrimarySchoolGenerator小学题目生成
- MiddleSchoolGenerator初中题目生成
- HighSchoolGenerator高中题目生成
#### 4. ExamSystem主类
- 用户管理:登录验证、会话管理
- 题目生成:协调各生成器工作
- 文件操作:试卷保存、题目去重
## 功能详解
### 1. 用户登录验证
```python
def login(self) -> bool:
# 实现用户名密码验证
# 支持9个预设账号
# 提供友好的错误提示
```
### 2. 题目生成算法
#### 小学题目
- 操作数2-3个随机数1-100
- 运算符:仅使用 + 和 -
- 示例:"23 + 45 - 12"
#### 初中题目
- 操作数3-4个随机数1-100
- 运算符:+、-、*、/
- 示例:"15 * 3 + 28 / 4"
#### 高中题目
- 操作数4-5个随机数1-100
- 运算符:+、-、*、/
- 括号30%概率包含括号运算
- 示例:"(15 + 3) * 2 - 8 / 4"
### 3. 题目去重机制
```python
def load_existing_questions(self, username: str) -> Set[str]:
# 加载用户所有历史题目
# 使用集合实现快速查找
def generate_unique_question(self, generator, existing_questions):
# 生成题目并检查是否重复
# 最大尝试次数100次
```
### 4. 文件存储系统
- 目录结构papers/用户名/
- 文件名:年-月-日-时-分-秒.txt
- 格式:题号 + 题目 + 空行
## 使用指南
### 快速开始
1. 确保安装Python 3.6+
2. 下载项目代码
3. 运行命令:`python src/main.py`
### 交互命令说明
#### 登录阶段
```
请输入用户名和密码用空格隔开primary_teacher1 primary123
当前选择为 小学 出题
```
#### 生成试卷
```
准备生成 小学 数学题目,请输入生成题目数量(输入-1将退出当前用户重新登录
>>> 15
试卷已生成papers/primary_teacher1/2025-01-15-14-30-25.txt
```
#### 切换学段
```
>>> 切换为 初中
已切换为 初中 出题
准备生成 初中 数学题目,请输入生成题目数量:
>>> 20
```
#### 退出操作
```
>>> -1
退出当前用户
```
## 技术实现
### 核心算法
#### 1. 表达式生成算法
```python
def generate_question(self) -> str:
num_operands = random.randint(2, 3)
operands = self.generate_operands(num_operands)
operators = [random.choice(['+', '-']) for _ in range(num_operands - 1)]
# 构建表达式
expression = str(operands[0])
for i in range(num_operands - 1):
expression += f" {operators[i]} {operands[i + 1]}"
return expression
```
#### 2. 括号插入算法(高中)
```python
def _generate_with_brackets(self, operands, operators):
if len(operands) <= 3 or random.random() < 0.3:
# 简单表达式或不加括号
return simple_expression
# 随机选择插入括号的位置
bracket_pos = random.randint(0, len(operands) - 3)
# 构建带括号的表达式
```
### 错误处理
- 输入验证:检查用户名密码格式
- 数量验证题目数量必须在10-30之间
- 类型验证:切换学段时检查有效性
- 文件操作:异常处理和重试机制
### 性能优化
- 使用集合进行快速去重查找
- 限制最大尝试次数防止无限循环
- 惰性加载历史题目
## 测试用例
### 功能测试
#### 1. 登录测试
```python
# 测试正确登录
assert login("primary_teacher1", "primary123") == True
# 测试错误密码
assert login("primary_teacher1", "wrong") == False
# 测试不存在的用户
assert login("nonexistent", "123") == False
```
#### 2. 题目生成测试
```python
# 测试小学题目
question = PrimarySchoolGenerator().generate_question()
assert question.count('+') + question.count('-') >= 1
assert '*' not in question and '/' not in question
# 测试题目去重
existing = {"1 + 2"}
new_question = generate_unique_question(generator, existing)
assert new_question != "1 + 2"
```
#### 3. 文件操作测试
```python
# 测试文件保存
filename = save_exam_paper(["1 + 1", "2 * 2"])
assert os.path.exists(filename)
# 测试文件格式
content = read_file(filename)
assert "1. 1 + 1" in content
assert "\n\n2. 2 * 2" in content
```
## 部署说明
### 环境要求
- Python 3.6或更高版本
- 磁盘空间至少10MB用于存储试卷文件
- 内存最少128MB RAM
### 安装步骤
1. 下载项目代码
2. 无需安装依赖(使用标准库)
3. 直接运行:`python src/main.py`
### 文件结构部署
```
项目根目录/
├── src/ # 源代码
│ └── main.py # 主程序
├── doc/ # 文档
│ └── README.md # 详细说明
├── papers/ # 试卷存储(自动创建)
└── README.md # 项目简介
```
### 维护说明
- 定期清理papers目录下的旧试卷文件
- 如需修改预设账号编辑main.py中的users列表
- 支持自定义题目生成规则
## 扩展性
### 可扩展功能
1. **题目难度调节**:增加难度级别参数
2. **题目类型扩展**:支持几何、代数等不同类型
3. **批量生成**:支持一次生成多份试卷
4. **Web界面**:开发图形化操作界面
5. **题目导出**支持PDF、Word格式导出
### 接口设计
```python
# 可扩展的题目生成器接口
class QuestionGenerator:
def set_difficulty(self, level: int):
"""设置题目难度"""
def generate_with_topics(self, topics: List[str]):
"""按主题生成题目"""
```
---

@ -0,0 +1,2 @@
# 项目依赖
# 这个项目使用纯Python标准库无需额外依赖

@ -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()
Loading…
Cancel
Save