From 58fb38ebbe6d90fe8a882121edf1e9a726b13220 Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Sat, 27 Sep 2025 13:09:34 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E6=9F=A5=E9=87=8D=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 106 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/src/main.py b/src/main.py index 725de5c..01a3b30 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,3 @@ -# src/main.py - """ 中小学数学卷子自动生成程序(命令行版) 功能要点: @@ -10,12 +8,13 @@ - 生成题目时避免与该账号已有文件中的题目重复(查重)。 """ +# 标准库导入 import os import sqlite3 from datetime import datetime from typing import Callable, List, Set -# 从 questions 模块导入题目生成函数 +# 本地应用模块导入 from .questions import generate_primary_question, generate_middle_question, generate_high_question # ------------------------------ @@ -30,6 +29,7 @@ def init_db(): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() + # 创建用户表 cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, @@ -37,6 +37,16 @@ def init_db(): level TEXT NOT NULL ) """) + + # 新增:创建题目表,用于查重 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS questions ( + question TEXT PRIMARY KEY, + username TEXT NOT NULL, + level TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) conn.commit() predefined_accounts = { @@ -49,14 +59,16 @@ def init_db(): 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)) + cursor.execute( + "INSERT INTO users (username, password, level) VALUES (?, ?, ?)", + (username, password, level) + ) conn.commit() conn.close() -def authenticate_user(username, password): +def authenticate_user(username: str, password: str) -> (str, str): """ 通过查询数据库验证用户名和密码。 返回: (level, username) 或 (None, None) @@ -64,16 +76,17 @@ def authenticate_user(username, password): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() - cursor.execute("SELECT level FROM users WHERE username = ? AND password = ?", - (username, password)) + cursor.execute( + "SELECT level FROM users WHERE username = ? AND password = ?", + (username, password) + ) result = cursor.fetchone() conn.close() if result: return result[0], username - else: - return None, None + return None, None # ------------------------------ # 文件与查重相关函数 @@ -83,42 +96,42 @@ def _ensure_dir(path: str) -> None: """确保目录存在,不存在则创建。""" os.makedirs(path, exist_ok=True) -def _read_existing_questions(folder: str) -> Set[str]: +def _read_existing_questions_from_db(username: str) -> Set[str]: """ - 读取指定文件夹下所有文本文件,返回其中已存在的题目集合。 + 从数据库中读取指定用户已存在的题目集合。 """ - questions = set() - if not os.path.isdir(folder): - return questions - for fname in os.listdir(folder): - if not fname.lower().endswith(".txt"): - continue - fpath = os.path.join(folder, fname) + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("SELECT question FROM questions WHERE username = ?", (username,)) + existing_questions = {row[0] for row in cursor.fetchall()} + conn.close() + return existing_questions + +def _save_new_questions_to_db(new_questions: List[str], username: str, level: str): + """ + 将新生成的题目保存到数据库中。 + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + for question in new_questions: try: - with open(fpath, "r", encoding="utf-8") as f: - for line in f: - s = line.strip() - 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() - else: - content = s - else: - content = s - if content: - questions.add(content) - except Exception: - continue - return questions + cursor.execute( + "INSERT OR IGNORE INTO questions (question, username, level) VALUES (?, ?, ?)", + (question, username, level) + ) + except sqlite3.Error as exc: + print(f"Error saving question to database: {exc}") + conn.rollback() + conn.commit() + conn.close() # ------------------------------ # 生成卷子并保存 # ------------------------------ -def generate_paper(level: str, count: int, folder: str) -> str: +def generate_paper( + level: str, count: int, folder: str, username: str +) -> str: """ 根据 level 和题目数量 count 生成一个卷子并保存。 """ @@ -133,7 +146,7 @@ def generate_paper(level: str, count: int, folder: str) -> str: else: raise ValueError("level 必须是:小学、初中或高中") - existing = _read_existing_questions(folder) + existing = _read_existing_questions_from_db(username) new_questions: List[str] = [] attempt_limit = count * 20 @@ -146,7 +159,7 @@ def generate_paper(level: str, count: int, folder: str) -> str: new_questions.append(q_norm) if len(new_questions) < count: - print("提示:未能生成足够的不重复题目,请稍后再试或清理旧卷子。") + print("提示:未能生成足够的不重复题目,请稍后再试或清理已有卷子后重试。") return "" filename = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".txt" @@ -156,6 +169,9 @@ def generate_paper(level: str, count: int, folder: str) -> str: with open(filepath, "w", encoding="utf-8") as f: for idx, q in enumerate(new_questions, start=1): f.write(f"{idx}. {q}\n\n") + + _save_new_questions_to_db(new_questions, username, level) + except Exception as exc: print("保存文件失败:", exc) return "" @@ -186,8 +202,7 @@ def login_prompt() -> (str, str): if level: print(f"当前选择为 {level} 出题") return level, auth_username - else: - print("请输入正确的用户名、密码") + print("请输入正确的用户名、密码") def main_loop() -> None: """主循环:登录->出题->可切换/退出登录""" @@ -196,7 +211,10 @@ def main_loop() -> None: user_folder = os.path.join("papers", username) while True: - prompt = f"准备生成 {level} 数学题目,请输入生成题目数量(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):" + prompt = ( + f"准备生成 {level} 数学题目,请输入生成题目数量" + "(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):" + ) inp = input(prompt).strip() if inp.startswith("切换为"): @@ -222,7 +240,7 @@ def main_loop() -> None: print("题目数量必须在 10-30 之间(包含 10 和 30)") continue - res = generate_paper(level, count, user_folder) + res = generate_paper(level, count, user_folder, username) if not res: print("生成失败,请尝试更小的题目数量或清理已有卷子后重试。") else: -- 2.34.1 From 5a16ad6d52d4a8adaff3510dd13a50ec2cbc76fd Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Sat, 27 Sep 2025 13:39:31 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=88=A9=E7=94=A8abc=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=AF=E6=89=A9=E5=B1=95=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 33 +++++---- src/questions.py | 172 +++++++++++++++++++++++++++++------------------ 2 files changed, 127 insertions(+), 78 deletions(-) diff --git a/src/main.py b/src/main.py index 01a3b30..fe0093c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,6 @@ """ 中小学数学卷子自动生成程序(命令行版) -功能要点: +要点: - 预设小学/初中/高中各 3 个账户,登录后按账号类型出题; - 题目数量有效范围 10-30,输入 -1 可退出当前用户重新登录; - 小学题只含 + - * / 和 ( ),初中题至少含平方或开根号,高中题至少含 sin/cos/tan; @@ -12,10 +12,14 @@ import os import sqlite3 from datetime import datetime -from typing import Callable, List, Set +from typing import List, Set # 本地应用模块导入 -from .questions import generate_primary_question, generate_middle_question, generate_high_question +from .questions import ( + HighQuestionGenerator, + MiddleQuestionGenerator, + PrimaryQuestionGenerator, +) # ------------------------------ # 数据库管理 @@ -24,6 +28,14 @@ from .questions import generate_primary_question, generate_middle_question, gene DB_NAME = "accounts.db" VALID_LEVELS = ["小学", "初中", "高中"] +# 将难度级别与对应的生成器类进行映射 +QUESTION_GENERATORS = { + "小学": PrimaryQuestionGenerator(), + "初中": MiddleQuestionGenerator(), + "高中": HighQuestionGenerator(), +} + + def init_db(): """初始化数据库和账户表,并导入预设数据。""" conn = sqlite3.connect(DB_NAME) @@ -137,14 +149,10 @@ def generate_paper( """ _ensure_dir(folder) - if level == "小学": - gen_func: Callable[[], str] = generate_primary_question - elif level == "初中": - gen_func = generate_middle_question - elif level == "高中": - gen_func = generate_high_question - else: - raise ValueError("level 必须是:小学、初中或高中") + # 使用多态性,动态获取生成器对象 + generator = QUESTION_GENERATORS.get(level) + if not generator: + raise ValueError("无效的题目难度级别") existing = _read_existing_questions_from_db(username) @@ -153,7 +161,8 @@ def generate_paper( attempts = 0 while len(new_questions) < count and attempts < attempt_limit: attempts += 1 - q = gen_func() + # 调用统一的 generate_question 方法 + q = generator.generate_question() q_norm = " ".join(q.split()) if q_norm not in existing and q_norm not in new_questions: new_questions.append(q_norm) diff --git a/src/questions.py b/src/questions.py index 9a36699..77575a3 100644 --- a/src/questions.py +++ b/src/questions.py @@ -1,94 +1,134 @@ +import abc 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: +class QuestionGenerator(abc.ABC): """ - 生成小学题(仅包含 + - * / 和可能的括号)。 - 操作数个数在 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: + @abc.abstractmethod + def generate_question(self) -> str: + """ + 抽象方法:生成一道数学题目。 + 所有继承此类的子类都必须实现此方法。 + """ + raise NotImplementedError + + @staticmethod + def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]: + """生成 n 个随机整数(闭区间),作为操作数使用。""" + return [random.randint(lo, hi) for _ in range(n)] + + @staticmethod + 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 + +# ------------------------------ +# 具体实现类 +# ------------------------------ + +class PrimaryQuestionGenerator(QuestionGenerator): """ - 生成初中题:题目中至少包含一个平方或一个开根号运算符。 + 生成小学题目。 + 题目只包含 + - * / 和括号运算符。 """ - ops = ["+", "-", "*", "/"] - special_ops = ["^2", "√"] - num_count = random.randint(2, 5) - numbers = _rand_numbers(num_count) + def generate_question(self) -> str: + """ + 生成一道小学数学题目。 + 操作数个数在 1-5 个之间,且操作数取值 1-100。 + """ + count = random.randint(2, 5) + nums = self._rand_numbers(count) + ops = ["+", "-", "*", "/"] + return self._join_with_ops(nums, ops) - 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 +class MiddleQuestionGenerator(QuestionGenerator): + """ + 生成初中题目。 + 题目中至少包含一个平方或一个开根号运算符。 + """ - part_to_modify = parts[special_op_pos] - special_op = random.choice(special_ops) + def generate_question(self) -> str: + """ + 生成一道初中数学题目。 + """ + ops = ["+", "-", "*", "/"] + special_ops = ["^2", "√"] - 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" + num_count = random.randint(2, 5) + numbers = self._rand_numbers(num_count) - return "".join(parts) + 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 -def generate_high_question() -> str: + 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) + + +class HighQuestionGenerator(QuestionGenerator): """ - 生成高中题:题目中至少包含 sin/cos/tan。 + 生成高中题目。 + 题目中至少包含 sin/cos/tan。 """ - ops = ["+", "-", "*", "/"] - trig_ops = ["sin", "cos", "tan"] - num_count = random.randint(2, 5) - numbers = _rand_numbers(num_count) + def generate_question(self) -> str: + """ + 生成一道高中数学题目。 + """ + ops = ["+", "-", "*", "/"] + trig_ops = ["sin", "cos", "tan"] + + num_count = random.randint(2, 5) + numbers = self._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)) + 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 + trig_op_pos = random.randint(0, len(parts) // 2) * 2 - part_to_modify = parts[trig_op_pos] - trig_op = random.choice(trig_ops) + part_to_modify = parts[trig_op_pos] + trig_op = random.choice(trig_ops) - parts[trig_op_pos] = f"{trig_op}({part_to_modify})" + parts[trig_op_pos] = f"{trig_op}({part_to_modify})" - if random.random() < 0.5: - return "(" + "".join(parts) + ")" + if random.random() < 0.5: + return "(" + "".join(parts) + ")" - return "".join(parts) \ No newline at end of file + return "".join(parts) \ No newline at end of file -- 2.34.1 From fd284a86a166fa9d3a53f8c92492cd19fab4502b Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Sun, 28 Sep 2025 18:28:31 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/README.md | 79 +++++++++++++--------- src/main.py | 20 ++++-- src/questions.py | 166 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 179 insertions(+), 86 deletions(-) diff --git a/doc/README.md b/doc/README.md index 78a4209..e0608ac 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,47 +2,64 @@ ## 一、项目概览 -本项目是一个命令行程序,旨在为小学、初中、高中不同年级的用户自动生成数学试卷。程序实现了用户登录、根据账户类型生成指定数量的题目、题目查重、以及按用户账户保存卷子等核心功能。所有账户信息都通过一个轻量级的 **SQLite 数据库**进行持久化管理,确保数据的稳定性和可扩展性。 +本项目是一个命令行程序,旨在为小学、初中、高中不同年级的用户自动生成数学试卷。程序实现了用户登录、根据账户类型生成指定数量的题目、题目查重、以及按用户账户保存卷子等核心功能。所有账户信息和已生成题目都通过一个轻量级的 **SQLite 数据库**进行持久化管理,确保数据的稳定性和查重的高效性。 -## 二、项目目录结构 +--- -X班_姓名_个人项目/├── src/│ ├── main.py # 程序主入口和命令行交互逻辑│ └── questions.py # 题目生成模块├── doc/│ └── README.md # 项目说明文档└── papers/ # 自动生成的试卷保存目录 (无需提交该目录)├── 张三1/│ └── 2025-09-25-16-30-01.txt└── ... +## 二、核心功能 -## 三、运行环境与安装 +* **用户认证与退出**: 程序通过命令行接收用户名和密码。除了标准的登录流程,用户还可以在登录界面输入 `-2` 随时退出程序。 +* **智能题目生成**: + * **小学**: 题目只包含 `+ - * /` 和 `()`,并内置了鲁棒的校验逻辑,**确保答案为非负整数,且不会出现除零错误**。 + * **初中/高中**: 题目操作数范围调整为 `1-5`。如果只有一个操作数,程序会直接对其进行开方、平方或三角函数运算,而不是生成带 `+ - * /` 的表达式。 +* **高效题目查重**: 所有老师(用户)已生成的题目都会被记录到中央数据库中。每次生成新题目时,程序会查询该用户的历史题目,确保不会生成重复的题目。 +* **命令行交互**: 用户可以通过输入 `-1` 退出当前账户,或使用“`切换为 XX`”(注意`切换为`后有一个空格)命令来切换题目类型,程序会进行严格的格式校验。 +* **文件保存**: 自动生成的试卷会保存在 `papers/<用户名>/` 目录下,并以 `年-月-日-时-分-秒.txt` 的格式命名。 + +--- + +## 三、项目目录结构 + +个人项目/ + +├── src/ + +│ ├── main.py # 程序主入口和命令行交互逻辑 + +│ └── questions.py # 题目生成模块 + +├── doc/ + +│ └── README.md # 项目说明文档 + +└── papers/ # 自动生成的试卷保存目录 (无需提交该目录) + + + +--- + +## 四、运行环境与安装 * **运行环境**: Python 3.8 或更高版本。 * **依赖**: 本项目仅使用 Python 标准库,无需安装任何额外依赖。 -* **启动**: 在终端中,**进入项目的根目录**(即 `src` 目录的上层),执行以下命令来启动程序:`python -m src.main` - -## 四、预设账户 +* **启动**: 在终端中,进入 `src` 目录的上层,执行以下命令即可启动程序: + `python3 src/main.py` 或 `python src/main.py` -程序启动时会自动初始化一个名为 `accounts.db` 的数据库文件,并导入以下预设账户。所有账户的密码均为 `123`。 +--- -| 账户类型 | 用户名 | 备注 | -| --- | --- | --- | -| 小学 | 张三1, 张三2, 张三3 | | -| 初中 | 李四1, 李四2, 李四3 | | -| 高中 | 王五1, 王五2, 王五3 | | +## 五、使用说明(交互流程) -**登录示例**:在命令行提示符下,输入 `张三1 123` 并回车即可登录。 +1. **启动程序**:运行上述命令后,程序将自动初始化数据库并提示登录。 +2. **登录**:输入用户名和密码,例如:`张三1 123`,或输入 `-2` 退出。 +3. **生成题目**:登录成功后,输入 `10-30` 之间的整数来指定题目数量。 +4. **切换类型**:在登录状态下,输入 `切换为 初中` 或 `切换为 高中` 来更改题目类型。 +5. **退出登录**:输入 `-1` 即可退出当前账户,返回登录界面。 -## 五、程序交互与功能说明 +--- -1. **登录**: 程序启动后会提示输入用户名和密码。成功登录后,系统会显示当前选择的出题类型。 -2. **题目数量**: 登录后,程序会提示输入生成题目的数量。有效范围为 **10-30** 题。 -3. **题目生成**: - * **小学题**: 题目只包含加、减、乘、除和括号运算符。 - * **初中题**: 题目中至少包含一个平方或一个开根号运算符。 - * **高中题**: 题目中至少包含 `sin`, `cos`, 或 `tan` 三角函数运算符。 - * **操作数**: 每道题目包含的操作数在 **1-5** 个之间,数值范围为 **1-100**。 -4. **查重**: 生成题目时,程序会自动检查并避免与该账户已生成的卷子中的题目重复。 -5. **卷子保存**: 生成的卷子文件将自动保存至 `papers/<用户名>/` 目录下,文件名格式为 `年-月-日-时-分-秒.txt`。每道题目都有题号,且题目之间空一行,以确保格式清晰。 -6. **命令操作**: - * 输入 `-1` : 退出当前用户,返回登录界面。 - * 输入 `切换为 XX` : 在登录状态下切换出题类型,`XX` 选项为 `小学`、`初中` 或 `高中`。 +## 六、说明与优化 -## 六、开发说明 +本项目在开发过程中,为了提升程序的**鲁棒性和易用性**,对部分需求进行了优化: -* 本项目遵循 Google Python 风格指南。 -* 主要代码逻辑已按功能拆分为两个模块:`main.py` 负责程序主入口和交互,`questions.py` 负责题目生成。 -* 项目满足课程需求文档中的各项功能和规范。 \ No newline at end of file +* **查重机制**: 采用 **SQLite 数据库**进行集中式查重,相比于通过读取文件进行查重,此方法更为高效、可靠,且不受文件误删等影响。 +* **题目生成**: 初中和高中题目新增了对单操作数表达式的生成,使其更符合实际教学场景。 \ No newline at end of file diff --git a/src/main.py b/src/main.py index fe0093c..50808cd 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,6 @@ """ 中小学数学卷子自动生成程序(命令行版) -要点: +功能要点: - 预设小学/初中/高中各 3 个账户,登录后按账号类型出题; - 题目数量有效范围 10-30,输入 -1 可退出当前用户重新登录; - 小学题只含 + - * / 和 ( ),初中题至少含平方或开根号,高中题至少含 sin/cos/tan; @@ -13,6 +13,7 @@ import os import sqlite3 from datetime import datetime from typing import List, Set +import sys # 导入 sys 模块以支持程序退出 # 本地应用模块导入 from .questions import ( @@ -198,7 +199,13 @@ def login_prompt() -> (str, str): 要求格式:"用户名 密码"(用空格分开)。 """ while True: - raw = input("请输入用户名和密码(空格隔开):").strip() + raw = input("请输入用户名和密码(空格隔开 或 输入“-2”退出):").strip() + + # 新增的退出选项 + if raw == "-2": + print("已退出。") + sys.exit(0) + parts = raw.split() if len(parts) != 2: print("输入格式错误,请输入:用户名 密码 (中间以空格隔开)") @@ -226,8 +233,9 @@ def main_loop() -> None: ) inp = input(prompt).strip() - if inp.startswith("切换为"): - new_level = inp.replace("切换为", "").strip() + # 严格处理切换命令,只允许“切换为 高中”这种格式 + if inp.startswith("切换为 ") and len(inp.split()) == 2: + new_level = inp.split()[1] if new_level in VALID_LEVELS: level = new_level print(f"已切换,准备生成 {level} 数学题目") @@ -263,5 +271,5 @@ def main() -> None: except KeyboardInterrupt: print("\n程序已被用户中断,退出。") -if __name__ == "__main__": - main() \ No newline at end of file + +main() \ No newline at end of file diff --git a/src/questions.py b/src/questions.py index 77575a3..1be6802 100644 --- a/src/questions.py +++ b/src/questions.py @@ -1,7 +1,9 @@ import abc import random +import math from typing import List + # ------------------------------ # 抽象基类和辅助函数 # ------------------------------ @@ -31,12 +33,23 @@ class QuestionGenerator(abc.ABC): 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 + @staticmethod + def _evaluate_expression(expression: str) -> float: + """ + 辅助函数:计算数学表达式的值。 + 注意:使用 eval() 存在安全风险,但在此项目场景中,表达式由我们自己的代码生成, + 不包含用户输入,因此是安全的。 + """ + return eval(expression) + + # ------------------------------ # 具体实现类 # ------------------------------ @@ -49,13 +62,26 @@ class PrimaryQuestionGenerator(QuestionGenerator): def generate_question(self) -> str: """ - 生成一道小学数学题目。 + 生成一道小学数学题目,并确保答案不会为负数,也不会出现除零错误。 操作数个数在 1-5 个之间,且操作数取值 1-100。 """ - count = random.randint(2, 5) - nums = self._rand_numbers(count) - ops = ["+", "-", "*", "/"] - return self._join_with_ops(nums, ops) + while True: + count = random.randint(2, 5) + nums = self._rand_numbers(count) + ops = ["+", "-", "*", "/"] + + # 尝试生成题目 + question_str = self._join_with_ops(nums, ops) + + # 检查答案是否为负数或出现除零错误 + try: + result = self._evaluate_expression(question_str) + # 同时检查非负数和是否为整数,避免 5/2 这种问题 + if result >= 0 and result == int(result): + return question_str + except (SyntaxError, ZeroDivisionError): + # 捕获除零错误和不合法语法,然后继续循环 + continue class MiddleQuestionGenerator(QuestionGenerator): @@ -64,39 +90,60 @@ class MiddleQuestionGenerator(QuestionGenerator): 题目中至少包含一个平方或一个开根号运算符。 """ + @staticmethod + def _add_special_op(parts: List[str]) -> List[str]: + """为表达式列表添加一个平方或开根号运算。""" + special_ops = ["^2", "√"] + + # 针对单操作数和多操作数进行不同的处理 + if len(parts) == 1: + part_to_modify = parts[0] + special_op = random.choice(special_ops) + + if special_op == "√": + # 确保开方数为完全平方数,避免无理数 + perfect_squares = [x ** 2 for x in range(1, 11)] + parts[0] = f"√({random.choice(perfect_squares)})" + elif special_op == "^2": + parts[0] = f"{part_to_modify}^2" + else: + # 随机选择一个数字位置进行修改 + 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 == "√": + # 确保开方数为完全平方数,避免无理数 + perfect_squares = [x ** 2 for x in range(1, 11)] + parts[special_op_pos] = f"√({random.choice(perfect_squares)})" + elif special_op == "^2": + # 为平方运算添加括号,以确保运算优先级正确 + parts[special_op_pos] = f"({part_to_modify})^2" + + return parts + def generate_question(self) -> str: """ - 生成一道初中数学题目。 + 生成一道初中数学题目,操作数范围为 (1, 5)。 """ ops = ["+", "-", "*", "/"] - special_ops = ["^2", "√"] - - num_count = random.randint(2, 5) - numbers = self._rand_numbers(num_count) - - parts = [] - for i in range(num_count): - parts.append(str(numbers[i])) - if i < num_count - 1: + num_count = random.randint(1, 5) # 调整操作数范围 + + # 针对单操作数和多操作数进行不同的生成 + if num_count == 1: + numbers = self._rand_numbers(num_count, lo=1, hi=5) + parts = [str(numbers[0])] + else: + numbers = self._rand_numbers(num_count, lo=1, hi=5) + parts = [str(numbers[0])] + for num in numbers[1:]: parts.append(random.choice(ops)) + parts.append(str(num)) - special_op_pos = random.randint(0, len(parts) // 2) * 2 - - part_to_modify = parts[special_op_pos] - special_op = random.choice(special_ops) + # 调用辅助方法,添加特殊运算符 + modified_parts = self._add_special_op(parts) - 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) + return "".join(modified_parts) class HighQuestionGenerator(QuestionGenerator): @@ -105,30 +152,51 @@ class HighQuestionGenerator(QuestionGenerator): 题目中至少包含 sin/cos/tan。 """ - def generate_question(self) -> str: - """ - 生成一道高中数学题目。 - """ - ops = ["+", "-", "*", "/"] + @staticmethod + def _add_trig_op(parts: List[str]) -> List[str]: + """为表达式列表添加一个三角函数运算。""" trig_ops = ["sin", "cos", "tan"] - num_count = random.randint(2, 5) - numbers = self._rand_numbers(num_count) + # 针对单操作数和多操作数进行不同的处理 + if len(parts) == 1: + part_to_modify = parts[0] + trig_op = random.choice(trig_ops) + parts[0] = f"{trig_op}({part_to_modify})" + else: + # 随机选择一个数字位置进行修改 + trig_op_pos = random.randint(0, len(parts) // 2) * 2 + part_to_modify = parts[trig_op_pos] + trig_op = random.choice(trig_ops) - parts = [] - for i in range(num_count): - parts.append(str(numbers[i])) - if i < num_count - 1: - parts.append(random.choice(ops)) + # 将操作数封装在三角函数中 + parts[trig_op_pos] = f"{trig_op}({part_to_modify})" - trig_op_pos = random.randint(0, len(parts) // 2) * 2 + return parts - part_to_modify = parts[trig_op_pos] - trig_op = random.choice(trig_ops) + def generate_question(self) -> str: + """ + 生成一道高中数学题目,操作数范围为 (1, 5)。 + """ + ops = ["+", "-", "*", "/"] + num_count = random.randint(1, 5) # 调整操作数范围 + + # 针对单操作数和多操作数进行不同的生成 + if num_count == 1: + numbers = self._rand_numbers(num_count, lo=1, hi=5) + parts = [str(numbers[0])] + else: + numbers = self._rand_numbers(num_count, lo=1, hi=5) + parts = [str(numbers[0])] + for num in numbers[1:]: + parts.append(random.choice(ops)) + parts.append(str(num)) - parts[trig_op_pos] = f"{trig_op}({part_to_modify})" + # 调用辅助方法,添加三角函数 + modified_parts = self._add_trig_op(parts) + # 50% 概率在最外层加括号 + question_str = "".join(modified_parts) if random.random() < 0.5: - return "(" + "".join(parts) + ")" + return f"({question_str})" - return "".join(parts) \ No newline at end of file + return question_str \ No newline at end of file -- 2.34.1