@ -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 ACCOUNT S:
if new_level in VALID_LEVEL S:
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 ( )