Python 虚拟环境 + 嵌入式 + 编译pyd 部署方案 + 简易UI
开发阶段 - 直接发源码版本
1. 在虚拟环境下开发 Python 项目
在开发电脑上,我们使用虚拟环境来开发我们的Python项目。
目的就是把所有此项目所需的第三方库,安装在一个单独的虚拟环境中。
方便一波带走。
假设我的项目根目录在 D:\my_project
,
去目标电脑部署时也同样创建一个 my_project
来放文件
1.1. 创建虚拟环境
命令格式:python -m venv <虚拟环境名称>
示例:
D:\my_project>python -m venv my_venv
1.2. 激活虚拟环境
执行此处脚本激活。我是 windows 所以用的是 bat,其它版本自己去目录下看看
D:\my_project>my_venv\Scripts\activate.bat
激活后命令提示会以虚拟环境名称作为前缀。如:
(my_venv) D:\my_project>
1.3. 在虚拟环境下安装依赖、开发
(my_venv) D:\my_project>pip install 包名
装好包,按正常流程写我们自己的代码。
1.4. 退出虚拟环境
(my_venv) D:\my_project>my_venv\Scripts\deactivate.bat
部署阶段
接来下我们来到需要部署的客户电脑,以嵌入式Python解释器执行我们的代码。
这样适应于一些不便在客户机安装Python的情况。
以下步骤适应离线部署,因为我们所有东西都是打包直接复制过去的,不需要通过网络再获取其它东西。
当然我这个项目比较简单,如果用了一个刁钻的依赖,可能还要在此基础上,单独处理一下。
1. 创建项目文件夹
先创建个空文件 my_project
来放整个项目:(当然名称随意)
- Python解释器:
python-3.11.5-embed-amd64
- 项目代码:
src
- 第三方库:
site-packages
- 启动bat文件:
run.bat
- 其它相关文件
- 目录结构
my_project 根目录
│ run.bat 启动脚本
│
├─python-3.11.5-embed-amd64 # 嵌入式 Python 解释器
│ 嵌入式 Python 相关文件
│
├─site-packages # 虚拟环境下安装好的第三方库目录
│ └─ 虚拟环境下安装好的第三方库文件
│
└─src
│ main.py
│ __init__.py
│
└─package # 自定义包
│ __init__.py
├─service
│ __init__.py
│ └─ 业务代码
│
└─utils
│ __init__.py
└─ 工具类
2. 准备嵌入器 Python 解释器
- 解压 python-3.11.5-embed-amd64.zip 为
python-3.11.5-embed-amd64
放到my_project
目录 - 嵌入式Python的
版本
最好和我们开发环境的版本保持一至。 - 我偷懒不改名了,大家可以随意改成
python-3.11.5
,py3115-embed
。。。
3. 处理第三方库
将开发机虚拟环境
下的 site-packages
目录复制到 my_project
目录中
3.1. my_venv\Lib\site-packages
中就是虚拟环境下安装的第三方库
4. 修改 ._pth 文件添加 Python 运行环境
修改 python-3.11.5-embed-amd64
目录下的 python311._pth
文件内容,加入一些所需路径。
311
对应 python 的版本,版本不同文件名会不一样。
- 修改前
python311.zip . # Uncomment to run site.main() automatically #import site
- 修改后
注意: 这里只要这里相对路径指对就行了。可以按自己需要调整目录解构。python311.zip . # 这是 my_project .. # 这里面是我们项目代码 ..\src # 这是虚拟环境下的第三方库 ..\site-packages # 这里去掉注释 import site
5. 添加启动 bat 脚本
@echo off
chcp 65001
python-3.11.5-embed-amd64\python.exe src\main.py
pause
- 在
my_project
创建批处理run.bat
用嵌入式的python-3.11.5-embed-amd64\python.exe
来执行 py chcp 65001
是使用utf-8
编码,解决批处理中文乱码问题。
开发阶段 - 编译 pyd 再发布版本
上面我们直接将 src
目录复制到客户电脑去跑。但有时可能不方便直接给源码,需要稍微处理一下。
这时可以用 cythonize
将 .py
编译成 .pyd
。
- 使用时 整个
src
目录解构保持不变,只是把py
全换成pyd
。 - 如
sql_util.py
编译成pyd
后名字类似这样sql_util.cp311-win_amd64.pyd
这不影响导入,直接用就行。
(我们严格遵循解释器使用相同版本的原则,所以不用改成sql_util.pyd
)
安装 Cythonize
1. pip 安装 Cython
Cythonize
是 Cython
的一个组件,安装 Cython
会自动安装 cythonize
工具。
(时刻牢记,我们要在为当前项目创建的虚拟环境下安装,免得污染全局)
pip install Cython
2. C编译环境
Cythonize
需要 C编译环境
。
gcc -v
查看有没有。我这里之前装了。没装的同学按这个来:MSYS2 安装 gcc、make 很简单。
(my_venv) D:\my_project>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=D:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/13.2.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../gcc-13.2.0/configure --prefix=/mingw64 --with-local-prefix=/mingw64/local --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --with-native-system-header-dir=/mingw64/include --libexecdir=/mingw64/lib --enable-bootstrap --enable-checking=release --with-arch=nocona --with-tune=generic --enable-languages=c,lto,c++,fortran,ada,objc,obj-c++,jit --enable-shared --enable-static --enable-libatomic --enable-threads=posix --enable-graphite --enable-fully-dynamic-string --enable-libstdcxx-filesystem-ts --enable-libstdcxx-time --disable-libstdcxx-pch --enable-lto --enable-libgomp --disable-libssp --disable-multilib --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-libiconv --with-system-zlib --with-gmp=/mingw64 --with-mpfr=/mingw64 --with-mpc=/mingw64 --with-isl=/mingw64 --with-pkgversion='Rev3, Built by MSYS2 project' --with-bugurl=https://github.com/msys2/MINGW-packages/issues --with-gnu-as --with-gnu-ld --disable-libstdcxx-debug --with-boot-ldflags=-static-libstdc++ --with-stage1-ldflags=-static-libstdc++
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 13.2.0 (Rev3, Built by MSYS2 project)
使用 Cythonize
- 编译当前目录下指定文件
main.py
,sql_util.py
(可以看到它是支持多个文件的)cythonize -i main.py sql_util.py
- 编译当前目录下所有
.py
但是排除__init__.py
cythonize -i *.py --exclude "__init__.py"
结果会生成到 项目根路径\build
目录中。
创建 bat 批量编译
在 项目根路径
下创建 编译PYD.bat
它将自动遍历 src
及子目录编译所有 .py
文件。
但是我排除了入口脚本 main.py
和 __init__.py
如果你有需要 __init__.py
也可以不排除。
注意:最终我们的入口脚本 main.py
还是保持源码方式用来执行。.pyd
只能作为包导入,不能直接执行。
@echo off
chcp 65001>nul
rem 先激活虚拟环境
call my_venv\Scripts\activate.bat
echo ----------------- py 》》》 pyd 编译开始 -----------------
setlocal enabledelayedexpansion
rem 设置起始目录,当前目录下的 src
set "startDir=src"
rem 根目录 src 在循环外单独处理
pushd "%startDir%"
echo 开始编译目录: "%cd%"
cythonize -i *.py --exclude "main.py" --exclude "__init__.py"
copy main.py ..\build\lib.win-amd64-cpython-311\src
copy config.json ..\build\lib.win-amd64-cpython-311\src
popd
rem 使用for /d /r循环遍历所有子目录
for /d /r "%startDir%" %%i in (*) do (
rem 检查目录名是否为__pycache__
if /i "%%~ni" NEQ "__pycache__" (
rem 不是__pycache__,则进入并打印目录名
pushd "%%i"
echo 开始编译目录: "%%i"
rem 使用 --exclude 排除文件,此参数可以多次使用
cythonize -i *.py --exclude "__init__.py"
popd
)
)
endlocal
echo ----------------- py 《《《 pyd 编译完成 -----------------
pause
清理编译垃圾
调试过程中我们肯定会反复编译,下面这个脚本用于清理编译生成的文件:.c
, .pyd
是的你没看错 .pyd
也一起删。
因为上面编译时它会在 项目根路径
下成一个 build
目录,编译结果会按 src
中的目录解构存放。
所以在我们源码目录 src
下生成的这些,我们是不需要的。
@echo off
chcp 65001>nul
echo ----------------- 开始清理 -----------------
setlocal enabledelayedexpansion
rem 设置起始目录
set "startDir=src"
rem 递归删除所有 .c .pyd 文件
echo 》》》开始清理 *.c *.pyd
for /r "%startDir%" %%f in (*.c *.pyd) do (
del "%%f"
echo 删除: %%f
)
echo 《《《完成清理 *.c *.pyd
endlocal
echo ----------------- 清理完成 -----------------
pause
build
目录解构如下:
my_project 根目录
│ 编译PYD.bat
│ 清理PYD.bat
└─build
│ 嵌入式 Python 相关文件
│
└─lib.win-amd64-cpython-311 # 这层目录的名字因 python 版本而异
│
└─src
└─ 这下面就和 src 目录结果一样了
HTA 配个 UI (仅限 windows)
有时客户抵触批处理,希望看到启动窗口,点击按钮执行。
如果只是简单的参数处理+启动,在windos上我们可以用 HTA 来实现。
- 在
my_project
创建批处理HTA-DEMO.hta
,用文本编辑器,打开把下面代码粘贴进去。 HTA-DEMO.hta
双击运行。(注意:如果你python解释器
和脚本
的位置与我不同,请自行修改 )
<!-- HTA Document -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="GENERATOR" content="Microsoft FrontPage 4.0">
<meta name="ProgId" content="FrontPage.Editor.Document">
<HTA:APPLICATION
APPLICATIONNAME="Demo"
ID="JHTA"
VERSION="1.0"
BORDER="dialog"
SCROLL="no"
SINGLEINSTANCE="yes"
CONTEXTMENU="yes"
NAVIGABLE="yes"/>
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<script type="text/javascript">
while (true) {
try {
var width = 500, height = 450;
window.resizeTo(width, height);
window.moveTo((window.screen.width - width) / 2, (window.screen.height - height) / 2);
break;
} catch (e) { continue; }
}
</script>
<title>HTA Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: auto;
padding: 20px;
}
.group {
margin-bottom: 15px;
width: 100%;
}
.group .selectFolder-label {
width: 18%;
}
.group .selectFolder-input,#fileInput {
width: 80%;
}
.button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
width: 100%;
}
</style>
</head>
<body>
<h1>HTA Demo</h1>
<!-- Group 1: File Selection -->
<div class="group">
<label for="fileInput">选择文件:</label>
<input type="file" id="fileInput" name="fileInput">
</div>
<!-- Group 2: Input and Output Text Boxes -->
<div class="group">
<label class="selectFolder-label" for="inputFile">输入目录:</label>
<input class="selectFolder-input" type="text" id="inputFile" name="inputFile" value="D:\input" onclick="selectFolder(this)">
</div>
<div class="group">
<label class="selectFolder-label" for="outputFile">输出目录:</label>
<input class="selectFolder-input" type="text" id="outputFile" name="outputFile" value="D:\output" onclick="selectFolder(this)">
</div>
<!-- Debug Switches -->
<div class="group">
<input type="checkbox" id="debugSwitch1" name="debugSwitch1">
<label for="debugSwitch1">调试开关1</label>
</div>
<div class="group">
<input type="checkbox" id="debugSwitch2" name="debugSwitch2">
<label for="debugSwitch2">调试开关2</label>
</div>
<!-- 执行按钮 -->
<div class="group">
<button class="button" onclick="executeFunction()">执行</button>
</div>
<script>
// 执行系统命令
function Run(strPath) {
try {
var objShell = new ActiveXObject("wscript.shell");
objShell.Run(strPath, 1, false);
objShell = null;
}
catch (e){alert('执行失败: "'+strPath+'"')}
}
// 选择目录
function selectFolder(ele) {
var shell = new ActiveXObject("Shell.Application");
var folder = shell.BrowseForFolder(0, "选择目录:", 0, shell.Desktop);
if (folder != null) {
folder = folder.Self;
ele.innerText = folder.Path; // 获取 folder 的路径
}
}
// 读取文本
function loadFile(filePath) {
var fso = new ActiveXObject("Scripting.FileSystemObject");
var file = fso.OpenTextFile(filePath, 1);
var content = file.ReadAll();
file.Close();
return content;
}
// 保存JSON对象到文本文件
function saveJsonFile(data, filePath) {
var fso = new ActiveXObject("Scripting.FileSystemObject");
var file = fso.CreateTextFile(filePath, true);
file.Write(JSON.stringify(data, null, 4));
file.Close();
console.log("文件保存成功");
}
// 获取JSON对象
function getJson(filePath){
jsonStr = loadFile(filePath);
return JSON.parse(jsonStr);
}
// 执行按钮
function executeFunction(){
// var file = fileInput.value;
var filePath = fileInput.value;
if(!filePath){
alert("文件必选");
return;
}
if(!inputFile.value){
alert("输入目录必选");
return;
}
if(!outputFile.value){
alert("输出目录必选");
return;
}
var data = {
file: filePath,
input: inputFile.value,
output: outputFile.value,
debug1: debugSwitch1.checked,
debug2: debugSwitch2.checked
}
var msg = '参数:\n文件名=' + data.file + '\n输入目录=' + data.input + '\n输出目录=' + data.output
+ '\n调试1=' + data.debug1 + '\n调试2=' + data.debug2
// 显示参数
alert(msg);
// 处理参数数据
// ...
// 执行
Run("python-3.11.5-embed-amd64\python.exe src\main.py")
}
</script>
</body>
</html>
参考资料
Python Releases for Windows
笑虾:C++ 开发 + VSCode 调试
VBScript Scripting Techniques > HTAs
HTA & WSC Examples
599cd:HTA Tips