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 来放整个项目:(当然名称随意)

  1. Python解释器:python-3.11.5-embed-amd64
  2. 项目代码:src
  3. 第三方库:site-packages
  4. 启动bat文件: run.bat
  5. 其它相关文件
  • 目录结构
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 解释器

  1. 解压 python-3.11.5-embed-amd64.zippython-3.11.5-embed-amd64 放到 my_project 目录
  2. 嵌入式Python的版本最好和我们开发环境的版本保持一至。
  3. 我偷懒不改名了,大家可以随意改成 python-3.11.5py3115-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
  1. my_project 创建批处理 run.bat 用嵌入式的 python-3.11.5-embed-amd64\python.exe 来执行 py
  2. chcp 65001 是使用 utf-8 编码,解决批处理中文乱码问题。

开发阶段 - 编译 pyd 再发布版本

上面我们直接将 src 目录复制到客户电脑去跑。但有时可能不方便直接给源码,需要稍微处理一下。
这时可以用 cythonize.py 编译成 .pyd

  1. 使用时 整个 src 目录解构保持不变,只是把 py 全换成 pyd
  2. sql_util.py 编译成 pyd 后名字类似这样 sql_util.cp311-win_amd64.pyd 这不影响导入,直接用就行。
    (我们严格遵循解释器使用相同版本的原则,所以不用改成 sql_util.pyd)

安装 Cythonize

1. pip 安装 Cython

CythonizeCython 的一个组件,安装 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 来实现。

  1. my_project 创建批处理 HTA-DEMO.hta,用文本编辑器,打开把下面代码粘贴进去。
  2. 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

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑虾

多情黯叹痴情癫。情癫苦笑多情难

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值