Showing Posts From

FastAPI

用 Python 构建高考志愿填报系统:位次换算与录取概率预测

为什么不用深度学习 这个场景是表格数据回归预测,不是图像识别。数据量有限(全国高校 × 省份 × 年份),LightGBM 等梯度提升模型在表格数据上通常比神经网络效果更好,且训练速度快。 技术栈 Python ├── Pandas 数据清洗和处理 ├── LightGBM 预测最低录取位次 ├── FastAPI 后端 API ├── SQLAlchemy 数据库 ORM └── Vue3 前端展示第一层:位次换算 真正决定录取的是位次,而非分数。同一个位次在不同年份对应的分数不同: import pandas as pd# 读取当年一分一段表 score_table = pd.read_csv("score_rank_2025.csv") # 结构:score, rank(分数→位次)def score_to_rank(score: int, year: int) -> int: df = score_table[score_table["year"] == year] row = df[df["score"] == score] if row.empty: # 取最接近的分数 row = df.iloc[(df["score"] - score).abs().argsort()[:1]] return int(row["rank"].values[0])位次越小录取概率越高(1 代表全省第一名)。 第二层:训练预测模型 历史录取数据结构:year school major province min_rank plan_count2023 清华 计算机 江苏 235 152024 清华 计算机 江苏 198 12import lightgbm as lgb from sklearn.model_selection import train_test_splitdf = pd.read_csv("admission_history.csv")features = ["year", "school_id", "major_id", "province_id", "plan_count"] target = "min_rank"X_train, X_test, y_train, y_test = train_test_split( df[features], df[target], test_size=0.2, random_state=42 )model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05) model.fit(X_train, y_train)也可以替换为 XGBRegressor 或 CatBoostRegressor,效果相近。 第三层:计算录取概率 基于历史波动区间: def admission_probability(school_id, major_id, province_id, student_rank: int, year: int) -> float: # 预测今年最低录取位次 predicted_min_rank = model.predict([[year, school_id, major_id, province_id, plan_count]])[0] # 历史波动标准差 history = df[(df.school_id == school_id) & (df.major_id == major_id)] std = history["min_rank"].std() # 学生位次越小(越靠前)于预测线,概率越高 z = (predicted_min_rank - student_rank) / (std + 1) from scipy.stats import norm return float(norm.cdf(z))输出 0.0~1.0 的概率值。 冲/稳/保 分类 def classify(prob: float) -> str: if prob >= 0.8: return "保" # 稳上 elif prob >= 0.5: return "稳" # 有竞争力 else: return "冲" # 有风险但值得一试FastAPI 接口 from fastapi import FastAPIapp = FastAPI()@app.get("/predict") def predict(school: str, major: str, province: str, score: int, year: int): rank = score_to_rank(score, year) prob = admission_probability(school, major, province, rank, year) label = classify(prob) return { "rank": rank, "probability": round(prob, 3), "label": label }模型效果优化特征工程:加入招生人数变化趋势、学校综合排名、专业热度 位次分段:分段训练(Top 1000 / 1000~10000 / 10000+),各段波动规律不同 时序权重:近年数据给更高权重(sample_weight 参数) 多省合并训练:数据量小时跨省训练,加省份编码作为特征数据来源 历年录取数据可从各省教育考试院官网下载,或使用第三方爬虫整理的开源数据集(GitHub 搜索"高考录取数据")。确保数据包含:学校、专业、省份、年份、最低分、最低位次、计划人数。

uvicorn reload 模式下找不到文件:用 __file__ 解决相对路径问题

uvicorn 加 --reload 参数时,每次代码变更都会重启子进程。子进程在 import 模块时如果代码顶层有相对路径操作,很容易报 FileNotFoundError。 问题复现 # main.py import cv2img = cv2.imread("small.png", 0) # 顶层直接读取uvicorn main:app --reload启动会报: FileNotFoundError: [Errno 2] No such file or directory: 'small.png'原因:uvicorn reload 子进程的 工作目录(cwd) 可能和你预期的不一致,相对路径 "small.png" 找的是 cwd 下的文件而不是脚本旁边的文件。 解决:用 __file__ 构造绝对路径 from pathlib import PathBASE_DIR = Path(__file__).parentimg = cv2.imread(str(BASE_DIR / "small.png"), 0)Path(__file__).parent 是脚本自身所在目录,不受 cwd 影响,任何启动方式都一致。 统一管理项目路径 from pathlib import PathBASE_DIR = Path(__file__).parent MODELS_DIR = BASE_DIR / "models" DATA_DIR = BASE_DIR / "data" CONFIG_FILE = BASE_DIR / "config.yaml"然后全局使用 BASE_DIR / "xxx" 而不是裸字符串路径。 uvicorn 启动方式 字符串形式(支持 reload): uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=True )对象形式(不支持 reload): uvicorn.run( app, # FastAPI 对象 host="0.0.0.0", port=8000 # reload=True 在这里无效,reload 必须用字符串形式 )reload 模式会 fork 子进程并 import 指定模块,所以 module 名必须是字符串。 GPU 模型服务的 worker 数 如果 FastAPI 服务里加载了 GPU 模型(SAM、YOLO、PyTorch 等),只开 1 个 worker: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1多 worker 会导致每个进程加载一份模型,显存翻倍。CPU 服务才适合 workers = CPU核数 × 2 + 1。 Nginx upstream 与单进程 单进程 FastAPI 的 upstream 可以简化: upstream backend { server 127.0.0.1:18001; }location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }weight、least_conn 在只有一个节点时没有任何效果,max_fails + fail_timeout 仍然生效(健康检测)。

PyInstaller 打包 FastAPI + Uvicorn:启动文件写法与依赖收集

直接打包 uvicorn main:app 的方式容易出现模块找不到的问题,推荐单独写入口文件。 项目结构 project/ ├── main.py # FastAPI app 定义 ├── start.py # PyInstaller 打包入口 ├── templates/ # Jinja2 模板(如有) └── static/ # 静态文件(如有)入口文件 start.py 关键点:直接传 app 对象,不用字符串 import uvicorn from main import appif __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)字符串形式 uvicorn.run("main:app", ...) 在 PyInstaller 环境中无法动态导入,会报: Error loading ASGI app. Could not import module "main"打包命令 基础打包: pyinstaller -F start.pyFastAPI/Uvicorn 有大量动态导入,需要显式收集: pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic包含模板和静态文件(macOS/Linux 用 :,Windows 用 ;): # Linux/macOS pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic \ --add-data "templates:templates" \ --add-data "static:static"# Windows pyinstaller -F start.py ^ --collect-all fastapi ^ --collect-all uvicorn ^ --collect-all starlette ^ --collect-all pydantic ^ --add-data "templates;templates" ^ --add-data "static;static"打包后静态文件路径 打包后程序解压到临时目录,__file__ 路径会变。读取打包内的文件需要: import sys import osdef get_resource_path(relative_path): if getattr(sys, 'frozen', False): base = sys._MEIPASS # PyInstaller 临时目录 else: base = os.path.dirname(__file__) return os.path.join(base, relative_path)# 挂载静态文件 from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory=get_resource_path("static")), name="static")常见错误 ModuleNotFoundError: No module named 'uvicorn.loops' --collect-all uvicornpydantic validator 找不到 --collect-all pydanticmultiprocessing 相关报错 确保 start.py 有 if __name__ == "__main__": 保护,避免多进程递归启动。 生产环境建议 PyInstaller 打包适合桌面工具分发,生产服务器更推荐:Docker 容器(环境固定、更新方便) systemd + venv(直接部署,无需打包) Supervisor 管理进程打包后可执行文件体积通常在 50-150 MB,启动时间比直接运行慢 3-10 秒。

PyInstaller 打包 FastAPI + uvicorn:能跑起来的最小写法

想把 FastAPI 项目打包成一个 exe 发给非技术用户,直接: pyinstaller -F main.py跑一下崩得七七八八。因为 uvicorn 走的是字符串式导入,PyInstaller 环境下拿不到;再加上一堆子模块(uvicorn.loops.*、starlette.*)静态分析捞不全。 正确起步:写个 start.py 不要直接打 main.py,写个显式导入 app 对象的启动文件: # start.py from main import app # 直接拿对象,不用字符串 import uvicornif __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)对应的 main.py: from fastapi import FastAPI app = FastAPI()@app.get("/") async def root(): return {"hello": "world"}打包 start.py: pyinstaller -F start.pyCould not import module "main" 的原因 如果 start.py 里写: uvicorn.run("main:app", host="0.0.0.0", port=8000)main:app 是字符串,uvicorn 会在运行时 importlib.import_module("main")。PyInstaller 打包后 main 不在 sys.path 里,直接崩: Error loading ASGI app. Could not import module "main"改成直接 import + 传对象最省事: from main import app uvicorn.run(app, ...)或者告诉 PyInstaller 也带上 main: pyinstaller -F start.py --hidden-import=mainuvicorn 子模块丢失 跑起来抛: ModuleNotFoundError: No module named 'uvicorn.loops'uvicorn 有一堆按需加载的 loop / protocol / logger 子模块,PyInstaller 全漏了。一次性带上: pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic--collect-all X = 把 X 及其所有子模块、数据文件、二进制全打进去。 静态文件 / 模板 用了 StaticFiles 或 Jinja2Templates 得加数据: Windows: pyinstaller -F start.py ^ --add-data "templates;templates" ^ --add-data "static;static"Linux/macOS: pyinstaller -F start.py \ --add-data "templates:templates" \ --add-data "static:static"代码里读路径要兼容 PyInstaller 临时目录: import sys, os from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templatesdef base_path(): return getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))app.mount("/static", StaticFiles(directory=os.path.join(base_path(), "static")), name="static") templates = Jinja2Templates(directory=os.path.join(base_path(), "templates"))后台运行、无 CMD 窗口 想双击不弹黑框: pyinstaller -F -w start.py --collect-all uvicorn ...-w 关控制台。注意:关了控制台,print / uvicorn 的日志就看不到了,改成写日志文件: uvicorn.run( app, host="0.0.0.0", port=8000, log_config={ "version": 1, "handlers": { "file": {"class": "logging.FileHandler", "filename": "app.log"}, }, "root": {"handlers": ["file"], "level": "INFO"}, }, )完整命令模板 一次搞定: pyinstaller -F -w start.py ^ --name myapi ^ --icon app.ico ^ --add-data "templates;templates" ^ --add-data "static;static" ^ --collect-all fastapi ^ --collect-all uvicorn ^ --collect-all starlette ^ --collect-all pydantic产物:dist/myapi.exe,双击就是一个后台服务。 什么时候别打包生产环境线上服务:Docker / systemd 更好 要热更新:exe 每次改代码都要重打 依赖 GPU / CUDA:打包体动辄几百 MB 多用户 / 高并发:exe 里塞 uvicorn 效率一般PyInstaller 更适合:内网小工具、给客户发一个"点两下就跑"的本地 API、演示 demo。 一句话总结 写 start.py + 直接 import app + --collect-all 四个关键字。字符串导入是坑,把 uvicorn 家族收全就通了。