把 YOLO 训练平台做成 Windows 双击即用:pathlib + psutil 跨平台规矩与 PyInstaller 打包实战(系列第 5 篇)
内部 AI 工具的现实是:公司有闲置的 Windows 台式机(还带显卡),没有专职运维,申请一台 Linux 服务器要走流程,安装这事最终要落到不会敲命令的同事手里。这篇讲怎么把一个 FastAPI + YOLO 的训练管理平台做成"Windows 就是主战场":双击安装、双击启动、关窗即停。写给同样要给非技术用户交付内部工具的人。
为什么敢把 Windows 定为主环境
Web 项目的默认假设几乎都是 Linux 服务器,Windows 是二等公民。但内部工具的验收标准不是"架构漂亮",是"离你最远的那个人能不能自己用起来"。公司的机器是 Windows,使用者和维护者都不是专业运维——与其和环境较劲,不如把 Windows 支持做扎实。
关键是从第一天就定死规矩,而不是写完再移植——移植一个写满了 POSIX 假设的项目,比重写还痛苦。
一、代码层的跨平台规矩
1. 全程 pathlib,禁手拼路径、禁 os.system
# 禁止path="data"+"/"+name os.system(f"yolo train data={yaml_path}")# 必须path=DATA_DIR/"datasets"/str(dataset_id)subprocess.Popen([exe,"detect","train",f"data={yaml_path}",...])subprocess 一律用参数列表形式、不经过 shell,既是跨平台要求,也顺带杜绝了命令注入。
2. 启动脚本第一行chcp 65001
Windows 控制台默认 GBK 代码页,Python 输出中文直接乱码。start.bat第一行切 UTF-8,一行解决。别小看这一行——没它的话,日志里的中文类别名全是问号,排查问题时痛不欲生。
3. zip 中文文件名:cp437→GBK 兜底解码
同事在 Windows 上打的数据集 zip,中文文件名按 GBK 存但标记位常常没设置,Python 解出来是 cp437 乱码:
def_decode_member_name(raw:str)->str:try:returnraw.encode("cp437").decode("gbk")# Windows 打包的中文名except(UnicodeEncodeError,UnicodeDecodeError):returnraw# 本来就是正常 UTF-8 名4. 程序生成的路径保持纯 ASCII
中文用户名 + 中文安装路径 + 深度学习框架,是 Windows 上的经典翻车组合。对策是隔离:用户数据爱叫什么叫什么,程序自己生成的路径一律纯 ASCII——data/runs/task_123/、data/datasets/456/。文件下载 URL 用记录 id(/api/images/123/file),不把中文文件名放进 URL。
5. 进程管理交给 psutil
训练是用 subprocess 调 yolo CLI 跑的。Windows 上terminate()杀不掉整棵进程树——yolo CLI 自己还会再 fork 子进程,只杀直接子进程会留下占着显存的孤儿。用 psutil 递归杀,Windows/Linux 行为一致:
importpsutildefkill_tree(pid:int):try:proc=psutil.Process(pid)exceptpsutil.NoSuchProcess:returnprocs=proc.children(recursive=True)+[proc]forpinprocs:try:p.terminate()exceptpsutil.Error:pass_,alive=psutil.wait_procs(procs,timeout=1)forpinalive:# 1 秒还没退的补一刀try:p.kill()exceptpsutil.Error:pass同理,不用killpg、start_new_session这类 POSIX 专属 API,系统差异全用 psutil 抹平。
6..gitattributes统一行尾
仓库根加.gitattributes(*.py text eol=lf等)。踩过的同类坑:shell 脚本在 Windows 上 checkout 变成 CRLF,拷回 Linux 直接无法执行。
二、GPU 策略:默认 CPU 版 torch,按需换轮子
深度学习项目装环境最大的坑是 torch 的 CUDA 版本。我们的策略:
requirements.txt默认锁CPU 版 torch——谁都能装,永远装得上- 要用 GPU 训练,README 给一行替换命令,按
nvidia-smi右上角显示的 CUDA Version 选轮子:
# nvidia-smi 显示 12.8 → pip install torch --index-url https://download.pytorch.org/whl/cu128- 代码里
device=auto:有 CUDA 用 GPU,没有就 CPU,训练照常能跑(只是慢)
这个"默认 CPU、按需升级"的决定让安装成功率接近 100%,GPU 从"安装前置条件"变成了"性能增强选项"。
三、安装向导:让不会敲命令的人也能装
内部工具的真正验收标准不是"能跑",是**“非技术同事能自己装上”**。为此写了一个图形安装向导(installer.py,tkinter 实现,Python 自带无额外依赖):
- 环境自检:检测 Python 版本、磁盘剩余空间、显卡型号和 CUDA 版本(有 N 卡就自动选对应的 GPU 版 torch 轮子)
- 选安装目录:默认给一个,可自定义
- 自动完成:复制程序 → 创建虚拟环境 → 安装依赖 → 建桌面快捷方式
- 也提供命令行/静默模式:
python installer.py --cli <安装目录> [--no-shortcut],给会敲命令的人用
几个细节:
- 装依赖显示实时进度日志,失败了把 pip 输出原样展示——内部工具的"傻瓜化"是简化操作,不是隐藏信息
- 桌面快捷方式指向图形启动器,而不是命令行窗口(下节讲)
- PowerShell 创建快捷方式时,安装路径要转义单引号再拼进命令——路径含
'会让命令断裂
四、打包与启动:双击图标,关窗即停
日常使用的入口是一个 PyInstaller 打包的图形启动器(launcher.pyw),它做了四件事:
- 后台拉起 uvicorn 服务(参数列表形式的 subprocess,日志重定向到
data/tmp/uvicorn.log) - 窗口里显示服务日志,启动过程可见,出问题不用翻日志文件
- 轮询健康检查,就绪后自动打开浏览器到
http://localhost:8788 - 关闭窗口 = 停掉服务,不留后台进程
核心逻辑就几行,去掉 GUI 部分长这样:
defport_open(port,timeout=0.5):try:withsocket.create_connection(("127.0.0.1",port),timeout=timeout):returnTrueexceptOSError:returnFalseproc=subprocess.Popen([str(venv_python),"-m","uvicorn","backend.main:app","--port","8788"],stdout=log_file,stderr=subprocess.STDOUT,creationflags=subprocess.CREATE_NO_WINDOW,# Windows:不弹黑窗口)whilenotport_open(8788):# 每 0.5 秒试连一次端口,超时 120 秒time.sleep(0.5)os.startfile("http://localhost:8788")# 就绪后用系统默认浏览器打开这套组合下来,同事的使用体验是:双击桌面图标 → 等几秒 → 浏览器自动打开 → 用完关窗。从头到尾不需要知道什么是 uvicorn、什么是端口。
五、验收清单里的 Windows 条目
PLAN.md 里的验收标准专门为 Windows 留了几条,每次发版前过一遍:
- 全新 Windows 机器上按 README 三步(venv → pip → start.bat)能跑起来
- GPU 训练在 Windows 上真实生效(任务管理器/nvidia-smi 可见占用)
- 代码无 POSIX-only 调用(pathlib、psutil、无 os.system)
- 中文路径、中文文件名、中文类别名全流程不乱码
这套清单不是纸面文章:系统目前由 5 名同事日常使用,已导入 10 个数据集、上万张图片(数据还在持续导入),全部按上面的方式装在 Windows 机器上跑。
投入产出也算得过来:安装向导installer.py413 行、图形启动器launcher.pyw383 行、启动脚本start.bat60 行,三个文件加起来不到 900 行,换来的是"部署"这件事从需要我到场变成同事自己点几下。要诚实交代的局限是:这套安装/启动链路没有自动化测试覆盖——仓库里 13 个 pytest 用例测的是认证、数据集、训练接口,安装向导和启动器全靠上面那张人工验收清单每次发版前过一遍。没有可靠数字证明它在所有 Windows 环境上的成功率,原因是样本只有公司这几台机器,"接近 100%"只是这几台上的经验值。
什么时候不需要这么做
这套打法的前提是"Windows 就是主战场、安装要落到非技术同事手里",换个环境很多内容就是过度设计:
- 有专职运维和现成 Linux 服务器:直接按 Linux 部署做扎实,安装向导、图形启动器、cp437 兜底这些都不用写;
- 使用者本身就是开发者:README + pip 就够,tkinter 向导和 PyInstaller 启动器的维护成本收不回来;
- 项目明确只跑单平台、没有分发性:跨平台规矩里仍值得保留的是 pathlib 和 subprocess 参数列表(后者顺带防命令注入),其余可以从简;
- 边界说清楚:CPU 版 torch 训练"只是能跑",速度没法和 GPU 比——"默认 CPU"保的是安装成功率,不是训练性能。
小结
- Windows 优先不是找罪受,是尊重部署环境的现实:机器是 Windows,就没有资格假设 Linux;
- 跨平台规矩就几条——pathlib、psutil、纯 ASCII 路径、UTF-8 控制台、cp437 兜底——不难,难的是从第一天就坚持,事后移植比重写痛苦;
- 默认 CPU 版 torch 把 GPU 从"安装前置条件"降级成"性能增强选项",装机再没被 CUDA 版本卡住过;
- 内部工具做得好不好,不看技术多漂亮,看的是离你最远的那个人能不能自己装上、自己用起来。
FAQ
Q:为什么不直接要求装 GPU 版 torch?
CUDA 版本和 torch 轮子必须对上,是装环境最大的坑。默认锁 CPU 版保证"永远装得上",要用 GPU 再按nvidia-smi显示的 CUDA Version 换一行安装命令,代码里device=auto自动适配。
Q:Windows 上杀训练进程有什么坑?terminate()杀不掉整棵进程树。训练是 subprocess 调 yolo CLI 起的,用 psutil 递归遍历子进程再杀,Windows 和 Linux 行为一致,也顺带避开了killpg这类 POSIX 专属 API。
Q:快捷方式为什么不直接指向 start.bat?
bat 会留下一个命令行黑窗口,使用者容易误关或不敢关。快捷方式指向 PyInstaller 打包的图形启动器:窗口里直接看服务日志、就绪后自动开浏览器、关窗即停服务,体验上就是一个普通桌面软件。
Q:中文安装路径为什么不直接支持?
中文用户名 + 中文路径 + 深度学习框架是 Windows 经典翻车组合,与其逐个框架修兼容,不如隔离:程序自己生成的路径一律纯 ASCII,用户数据随意,下载 URL 用记录 id 而不是文件名。
Q:PyInstaller 启动器和 uvicorn 服务是什么关系?
启动器只是个"壳":用 subprocess 拉起 uvicorn,把日志重定向到文件并在窗口里显示,轮询健康检查通过后自动打开浏览器,窗口关闭时负责把服务停掉,不留后台进程。
技术栈:FastAPI · SQLite · Vue 3 · ultralytics · PyInstaller · tkinter
系列导航
- 系列第 1 篇:开发总览——23 个实践教训
- 系列第 2 篇:需求设计与技术选型
- 系列第 3 篇:subprocess 训练进程管理
- 系列第 4 篇:标注数据一致性的 4 个设计
- 系列第 5 篇:Windows 双击即用与 PyInstaller 打包(本篇)
- 系列第 6 篇:业余时间做内部工具不烂尾的心得
有问题欢迎评论区交流。