两模块 #1

Merged
hnu202326010215 merged 3 commits from develop into main 5 months ago

@ -1,66 +1,48 @@
# 软件2302班_杨逸轩_个人项目 - 中小学数学卷子自动生成程序
## 一、项目说明(概述)
## 一、项目概览
本项目为命令行程序,用于为小学、初中、高中自动生成数学试题卷子(仅生成题目文本,不包含答案)。程序实现了登录(预设账户)、题目生成、查重、题目类型切换、按账号保存卷子等功能,符合课程个人项目要求。详细需求参见提交的需求文档。(开发参考:已阅读并参考课程需求文档与 Google Python 风格指南。)
本项目是一个命令行程序,旨在为小学、初中、高中不同年级的用户自动生成数学试卷。程序实现了用户登录、根据账户类型生成指定数量的题目、题目查重、以及按用户账户保存卷子等核心功能。所有账户信息都通过一个轻量级的 **SQLite 数据库**进行持久化管理,确保数据的稳定性和可扩展性。
## 二、目录结构
## 二、项目目录结构
X班_姓名_个人项目/
X班_姓名_个人项目/├── src/│ └── main.py # 程序主入口├── doc/│ └── README.md # 项目说明文档└── papers/ # 自动生成的试卷保存目录 (无需提交该目录)├── 张三1/│ └── 2025-09-25-16-30-01.txt└── ...
├── src/
## 三、运行环境与安装
│ └── main.py # 主程序Python
* **运行环境**: Python 3.8 或更高版本。
* **依赖**: 本项目仅使用 Python 标准库,无需安装任何额外依赖。
* **启动**: 在终端中,进入 `src` 目录的上层,执行以下命令即可启动程序:`python3 src/main.py` 或 `python src/main.py`
├── doc/
## 四、预设账户
│  └── README.md # 本文件
程序启动时会自动初始化一个名为 `accounts.db` 的数据库文件,并导入以下预设账户。所有账户的密码均为 `123`
| 账户类型 | 用户名 | 密码 |
| --- | --- | --- |
| 小学 | 张三1, 张三2, 张三3 | 123 |
| 初中 | 李四1, 李四2, 李四3 | 123 |
| 高中 | 王五1, 王五2, 王五3 | 123 |
## 三、运行环境与要求
**登录示例**:在命令行提示符下,输入 `张三1 123` 并回车即可登录。
- Python 版本:推荐 Python 3.8+(向后兼容 3.x
## 五、程序交互与功能说明
- 无需额外第三方库,仅使用标准库;
- 在终端中执行:`python3 src/main.py` 或 `python src/main.py`
## 四、预设账户(登录示例)
账户类型与账号(密码均为 `123`
- 小学:`张三1`、`张三2`、`张三3`
- 初中:`李四1`、`李四2`、`李四3`
- 高中:`王五1`、`王五2`、`王五3`
- 登录输入示例(命令行):`张三1 123`
## 五、使用说明(交互流程)
1.启动程序后,按提示输入 `用户名 密码`(中间以空格隔开)。
2.登录成功后将显示 `当前选择为 XX 出题`XX 为 小学/初中/高中)。
3.程序会提示:`准备生成 XX 数学题目,请输入生成题目数量(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):` - 输入整数 `n`10 ≤ n ≤ 30表示生成 n 道题并保存; - 输入 `-1` 退出当前用户,返回登录界面; - 输入 `切换为 初中`(或 小学/高中)可在登录状态下切换出题类型。
4卷子将保存在 `papers/<用户名>/年-月-日-时-分-秒.txt`,每题有题号,题目之间空一行。
5.程序在生成题目时会避免与 `papers/<用户名>` 下已有卷子中的题目重复(查重功能)。
## 六、题型规则(来自需求)
- 小学题:仅包含 `+`, `-`, `*`, `/` 和括号 `()`
- 初中题:题目中至少包含一个平方 `^2` 或一个开根号 `√()`
- 高中题:题目中至少包含一个三角函数 `sin`, `cos`, 或 `tan`。 题目中操作数个数为 1-5数值范围 1-100。详见课程需求附表。
1. **登录**: 程序启动后会提示输入用户名和密码。成功登录后,系统会显示当前选择的出题类型。
2. **题目数量**: 登录后,程序会提示输入生成题目的数量。有效范围为 **10-30** 题。
3. **题目生成**:
* **小学题**: 题目只包含加、减、乘、除和括号运算符。
* **初中题**: 题目中至少包含一个平方或一个开根号运算符。
* **高中题**: 题目中至少包含 `sin`, `cos`, 或 `tan` 三角函数运算符。
* **操作数**: 每道题目包含的操作数在 **1-5** 个之间,数值范围为 **1-100**
4. **查重**: 生成题目时,程序会自动检查并避免与该账户已生成的卷子中的题目重复。
5. **卷子保存**: 生成的卷子文件将自动保存至 `papers/<用户名>/` 目录下,文件名格式为 `年-月-日-时-分-秒.txt`。每道题目都有题号,且题目之间空一行,以确保格式清晰。
6. **命令操作**:
* 输入 `-1` : 退出当前用户,返回登录界面。
* 输入 `切换为 XX` : 在登录状态下切换出题类型,`XX` 选项为 `小学`、`初中` 或 `高中`
## 六、开发说明
* 本项目遵循 Google Python 风格指南。
* 主要代码逻辑位于 `src/main.py` 文件中,所有功能均在该文件中实现。
* 项目满足课程需求文档中的各项功能和规范。

@ -1,3 +1,5 @@
# src/main.py
"""
中小学数学卷子自动生成程序命令行版
功能要点
@ -9,134 +11,69 @@
"""
import os
import random
import sqlite3
from datetime import datetime
from typing import Callable, List, Set
# ------------------------------
# 常量:预设账户(来自需求文档附表)
# ------------------------------
ACCOUNTS = {
"小学": {"张三1": "123", "张三2": "123", "张三3": "123"},
"初中": {"李四1": "123", "李四2": "123", "李四3": "123"},
"高中": {"王五1": "123", "王五2": "123", "王五3": "123"},
}
# 从 questions 模块导入题目生成函数
from .questions import generate_primary_question, generate_middle_question, generate_high_question
# ------------------------------
# 工具:随机表达式生成(控制简单易懂)
# 每个生成函数尽量短小(符合风格指南对函数长度的建议)
# 数据库管理
# ------------------------------
def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]:
"""生成 n 个随机整数(闭区间),作为操作数使用。"""
return [random.randint(lo, hi) for _ in range(n)]
def _join_with_ops(numbers: List[int], ops: List[str]) -> str:
"""把数字列表用随机运算符连接成表达式字符串,可能加入括号。"""
expr = str(numbers[0])
for num in numbers[1:]:
op = random.choice(ops)
expr += f" {op} {num}"
# 20% 概率在表达式外层加一对括号
if random.random() < 0.2:
expr = "(" + expr + ")"
return expr
def generate_primary_question() -> str:
"""
生成小学题仅包含 + - * / 和可能的括号
操作数个数在 1-5 个之间且操作数取值 1-100
返回字符串形式的题目表达式不计算答案
"""
count = random.randint(2, 5)
nums = _rand_numbers(count)
ops = ["+", "-", "*", "/"]
return _join_with_ops(nums, ops)
def generate_middle_question() -> str:
"""
生成初中题在小学题的基础上题目中至少包含一个平方或一个开根号运算符
我们用简单的字符串形式表示x^2 (expr)
"""
# 确保题目中至少有一个平方或开根号运算符
ops = ["+", "-", "*", "/"]
special_ops = ["^2", ""]
# 随机生成操作数个数2-5个
num_count = random.randint(2, 5)
numbers = _rand_numbers(num_count)
# 将数字和运算符交替拼接成列表
parts = []
for i in range(num_count):
parts.append(str(numbers[i]))
if i < num_count - 1:
parts.append(random.choice(ops))
# 随机选择一个位置插入特殊运算符
# 尝试将特殊运算符插入到某个数字前或某个子表达式前
special_op_pos = random.randint(0, len(parts) // 2) * 2
# 获取要应用特殊运算的部分
part_to_modify = parts[special_op_pos]
special_op = random.choice(special_ops)
# 如果是开方,确保开方内部有括号,除非是单个数字
if special_op == "":
if num_count > 1:
# 找到一个子表达式进行开方
# 我们可以简单地选择一个操作数和它后面的运算符+操作数
# 为了简化,这里只对一个数字进行开方
parts[special_op_pos] = f"√({parts[special_op_pos]})"
else:
parts[special_op_pos] = f"{parts[special_op_pos]}"
# 如果是平方,且不是单个数字,则加上括号
elif special_op == "^2":
if num_count > 1:
parts[special_op_pos] = f"({parts[special_op_pos]})^2"
else:
parts[special_op_pos] = f"{parts[special_op_pos]}^2"
return "".join(parts)
def generate_high_question() -> str:
DB_NAME = "accounts.db"
VALID_LEVELS = ["小学", "初中", "高中"]
def init_db():
"""初始化数据库和账户表,并导入预设数据。"""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password TEXT NOT NULL,
level TEXT NOT NULL
)
""")
conn.commit()
predefined_accounts = {
"小学": {"张三1": "123", "张三2": "123", "张三3": "123"},
"初中": {"李四1": "123", "李四2": "123", "李四3": "123"},
"高中": {"王五1": "123", "王五2": "123", "王五3": "123"},
}
for level, users in predefined_accounts.items():
for username, password in users.items():
cursor.execute("SELECT 1 FROM users WHERE username = ?", (username,))
if cursor.fetchone() is None:
cursor.execute("INSERT INTO users (username, password, level) VALUES (?, ?, ?)",
(username, password, level))
conn.commit()
conn.close()
def authenticate_user(username, password):
"""
生成高中题题目中至少包含 sin/cos/tan
形式示例sin(expr) cos(expr)
通过查询数据库验证用户名和密码
返回 (level, username) (None, None)
"""
# 确保题目中至少有一个三角函数运算符
ops = ["+", "-", "*", "/"]
trig_ops = ["sin", "cos", "tan"]
# 随机生成操作数个数2-5个
num_count = random.randint(2, 5)
numbers = _rand_numbers(num_count)
# 将数字和运算符交替拼接成列表
parts = []
for i in range(num_count):
parts.append(str(numbers[i]))
if i < num_count - 1:
parts.append(random.choice(ops))
# 随机选择一个位置插入三角函数
# 尝试将三角函数插入到某个数字前
trig_op_pos = random.randint(0, len(parts) // 2) * 2
# 获取要应用三角函数的部分
part_to_modify = parts[trig_op_pos]
trig_op = random.choice(trig_ops)
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
# 将三角函数应用到该部分,并加上括号
parts[trig_op_pos] = f"{trig_op}({part_to_modify})"
cursor.execute("SELECT level FROM users WHERE username = ? AND password = ?",
(username, password))
# 50%的概率在表达式外层加上括号
if random.random() < 0.5:
return "(" + "".join(parts) + ")"
result = cursor.fetchone()
conn.close()
return "".join(parts)
if result:
return result[0], username
else:
return None, None
# ------------------------------
# 文件与查重相关函数
@ -148,8 +85,7 @@ def _ensure_dir(path: str) -> None:
def _read_existing_questions(folder: str) -> Set[str]:
"""
读取指定文件夹下所有文本文件返回其中已存在的题目集合去重并去空行
这样在生成新卷子时能避免题目重复
读取指定文件夹下所有文本文件返回其中已存在的题目集合
"""
questions = set()
if not os.path.isdir(folder):
@ -157,18 +93,14 @@ def _read_existing_questions(folder: str) -> Set[str]:
for fname in os.listdir(folder):
if not fname.lower().endswith(".txt"):
continue
# 跳过非题目文件
fpath = os.path.join(folder, fname)
try:
with open(fpath, "r", encoding="utf-8") as f:
for line in f:
s = line.strip()
# 跳过题号行前的序号(如 "1. "),提取实际内容
if not s:
continue
# 如果行以数字点空格开头,去掉序号(兼容已有文件格式)
if s.split(".", 1)[0].isdigit() and s.count(".") >= 1:
# 分割一次获取题目主体
parts = s.split(".", 1)
if len(parts) == 2:
content = parts[1].strip()
@ -179,7 +111,6 @@ def _read_existing_questions(folder: str) -> Set[str]:
if content:
questions.add(content)
except Exception:
# 忽略单个文件读取错误(但不抛出),继续处理其他文件
continue
return questions
@ -189,10 +120,8 @@ def _read_existing_questions(folder: str) -> Set[str]:
def generate_paper(level: str, count: int, folder: str) -> str:
"""
根据 level小学/初中/高中和题目数量 count 生成一个卷子并保存到 folder
返回生成的文件路径成功或空字符串失败
根据 level 和题目数量 count 生成一个卷子并保存
"""
# 目录与参数校验
_ensure_dir(folder)
if level == "小学":
@ -204,31 +133,25 @@ def generate_paper(level: str, count: int, folder: str) -> str:
else:
raise ValueError("level 必须是:小学、初中或高中")
# 读取已有题目以便查重
existing = _read_existing_questions(folder)
# 生成题目(去重)
new_questions: List[str] = []
attempt_limit = count * 20 # 防止死循环:尝试上限
attempt_limit = count * 20
attempts = 0
while len(new_questions) < count and attempts < attempt_limit:
attempts += 1
q = gen_func()
# 对题目主体做简单规范化(去掉多余空白)
q_norm = " ".join(q.split())
if q_norm not in existing and q_norm not in new_questions:
new_questions.append(q_norm)
if len(new_questions) < count:
# 如果没生成足够题目,提示并返回空
print("提示:未能生成足够的不重复题目,请稍后再试或清理旧卷子。")
return ""
# 文件名按 年-月-日-时-分-秒.txt
filename = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".txt"
filepath = os.path.join(folder, filename)
# 写入文件:每题前带题号,题目之间空一行
try:
with open(filepath, "w", encoding="utf-8") as f:
for idx, q in enumerate(new_questions, start=1):
@ -248,7 +171,6 @@ def login_prompt() -> (str, str):
"""
命令行登录提示
要求格式"用户名 密码"用空格分开
返回 (level, username)
"""
while True:
raw = input("请输入用户名和密码(空格隔开):").strip()
@ -256,65 +178,63 @@ def login_prompt() -> (str, str):
if len(parts) != 2:
print("输入格式错误,请输入:用户名 密码 (中间以空格隔开)")
continue
username, password = parts
for level, users in ACCOUNTS.items():
if username in users and users[username] == password:
print(f"当前选择为 {level} 出题")
return level, username
print("请输入正确的用户名、密码")
level, auth_username = authenticate_user(username, password)
if level:
print(f"当前选择为 {level} 出题")
return level, auth_username
else:
print("请输入正确的用户名、密码")
def main_loop() -> None:
"""主循环:登录->出题->可切换/退出登录"""
while True:
level, username = login_prompt()
user_folder = os.path.join("papers", username) # 每个账号一个文件夹
user_folder = os.path.join("papers", username)
while True:
prompt = f"准备生成 {level} 数学题目,请输入生成题目数量(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):"
inp = input(prompt).strip()
# 处理切换命令
if inp.startswith("切换为"):
new_level = inp.replace("切换为", "").strip()
if new_level in ACCOUNTS:
if new_level in VALID_LEVELS:
level = new_level
print(f"已切换,准备生成 {level} 数学题目")
else:
print("请输入小学、初中和高中三个选项中的一个")
continue
# 退出当前用户,返回登录界面
if inp == "-1":
print("退出当前用户,返回登录界面。")
break
# 解析数字
try:
count = int(inp)
except ValueError:
print("输入有误,请输入整数或指令(例如:切换为 初中,或 -1")
continue
# 数量范围校验10-30
if not (10 <= count <= 30):
print("题目数量必须在 10-30 之间(包含 10 和 30")
continue
# 生成题目
res = generate_paper(level, count, user_folder)
if not res:
# 生成失败(如查重过多),允许用户重试或退出
print("生成失败,请尝试更小的题目数量或清理已有卷子后重试。")
else:
# 生成成功后,继续允许该用户再次出题或切换/退出(满足评分细则:每次登录可出题多次)
pass
def main() -> None:
"""程序入口"""
init_db()
try:
main_loop()
except KeyboardInterrupt:
print("\n程序已被用户中断,退出。")
main()
if __name__ == "__main__":
main()

@ -0,0 +1,94 @@
import random
from typing import List
# ------------------------------
# 工具:随机表达式生成(控制简单易懂)
# ------------------------------
def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]:
"""生成 n 个随机整数(闭区间),作为操作数使用。"""
return [random.randint(lo, hi) for _ in range(n)]
def _join_with_ops(numbers: List[int], ops: List[str]) -> str:
"""把数字列表用随机运算符连接成表达式字符串,可能加入括号。"""
expr = str(numbers[0])
for num in numbers[1:]:
op = random.choice(ops)
expr += f" {op} {num}"
# 20% 概率在表达式外层加一对括号
if random.random() < 0.2:
expr = "(" + expr + ")"
return expr
def generate_primary_question() -> str:
"""
生成小学题仅包含 + - * / 和可能的括号
操作数个数在 1-5 个之间且操作数取值 1-100
"""
count = random.randint(2, 5)
nums = _rand_numbers(count)
ops = ["+", "-", "*", "/"]
return _join_with_ops(nums, ops)
def generate_middle_question() -> str:
"""
生成初中题题目中至少包含一个平方或一个开根号运算符
"""
ops = ["+", "-", "*", "/"]
special_ops = ["^2", ""]
num_count = random.randint(2, 5)
numbers = _rand_numbers(num_count)
parts = []
for i in range(num_count):
parts.append(str(numbers[i]))
if i < num_count - 1:
parts.append(random.choice(ops))
special_op_pos = random.randint(0, len(parts) // 2) * 2
part_to_modify = parts[special_op_pos]
special_op = random.choice(special_ops)
if special_op == "":
if num_count > 1:
parts[special_op_pos] = f"√({parts[special_op_pos]})"
else:
parts[special_op_pos] = f"{parts[special_op_pos]}"
elif special_op == "^2":
if num_count > 1:
parts[special_op_pos] = f"({parts[special_op_pos]})^2"
else:
parts[special_op_pos] = f"{parts[special_op_pos]}^2"
return "".join(parts)
def generate_high_question() -> str:
"""
生成高中题题目中至少包含 sin/cos/tan
"""
ops = ["+", "-", "*", "/"]
trig_ops = ["sin", "cos", "tan"]
num_count = random.randint(2, 5)
numbers = _rand_numbers(num_count)
parts = []
for i in range(num_count):
parts.append(str(numbers[i]))
if i < num_count - 1:
parts.append(random.choice(ops))
trig_op_pos = random.randint(0, len(parts) // 2) * 2
part_to_modify = parts[trig_op_pos]
trig_op = random.choice(trig_ops)
parts[trig_op_pos] = f"{trig_op}({part_to_modify})"
if random.random() < 0.5:
return "(" + "".join(parts) + ")"
return "".join(parts)
Loading…
Cancel
Save