
1. 项目缘起与整体设计思路把一套依赖 Geopandas、Pandas、Fiona、Requests、Openpyxl 的数据处理脚本打包成单个 exe交给不装 Python 的同事双击就能跑这件事听起来像是“一条命令的事”实际动手才知道坑有多密。我自己前前后后折腾过好几个类似项目从最早的纯 Pandas 脚本到后来带空间数据的 Geopandas 工具每次打包都要重新踩一遍坑。这篇就把我积累下来的方案、参数、排查思路完整梳理一遍尽量做到你照着抄就能跑通。先说清楚这个项目到底解决什么问题。典型场景是这样的你写了一个脚本功能大概是读取 Excel 台账、调用接口拉取数据、用 Geopandas 做空间匹配或坐标转换、最后再导出成带格式的 Excel。这套东西在你本机跑得好好的因为你的环境里 GDAL、Fiona、Shapely 这些库都装齐了。但一旦要交付给不懂技术的使用者让他们装 Python、配环境、处理 DLL 缺失基本等于劝退。所以打包成 exe 是刚需。那为什么不用 PyInstaller 而选 py2exe这是很多人第一个疑问。我的实际体验是PyInstaller 生态更活跃、文档更多对大多数纯 Python 项目确实更省心但 py2exe 在 Windows 平台上的产物结构更“干净”生成的 exe 加依赖目录的组合对杀毒软件误报相对少一些而且它对某些老版本 Python 的兼容性更稳。不过要坦白讲Geopandas 这类带二进制扩展的库两个工具都会遇到 DLL 收集不全的问题核心难点不在打包器本身而在于怎么把 GDAL 那一整套动态库和数据结构正确塞进产物里。所以下面的方案里很多技巧对 PyInstaller 同样适用。整体设计思路我总结成三条主线。第一条是环境隔离绝对不要在全局环境里打包一定用干净的虚拟环境把依赖版本锁死。第二条是依赖显式声明Geopandas 的隐式依赖Fiona、Shapely、pyproj、GDAL必须手动确认不能指望打包器自动发现。第三条是产物验证打包完不能只看“生成成功”必须在一台没装 Python 的机器上实测这一步能暴露 90% 的问题。适合谁来参考这篇内容如果你已经能用 Python 写出可运行的数据处理脚本但对打包发布不熟那这篇正好如果你已经被 “ImportError: DLL load failed” 折磨过那更能对上号。下面从环境准备开始一步步拆。2. 环境准备与依赖版本锁定2.1 为什么虚拟环境和版本锁定是第一步我见过太多人打包失败根源不是打包配置写错而是环境本身就乱。比如你机器上装过多个版本的 GDAL或者 conda 和 pip 混着装 Geopandas导致打包器抓到的 DLL 是 A 版本而 Python 导入时找的是 B 版本运行时直接崩。所以第一步永远是建一个全新的虚拟环境从零装依赖。用 conda 还是 venv对 Geopandas 这种带地理库的项目我强烈建议用 conda或者 mamba速度更快。原因是 conda 对 GDAL、Fiona 这些库的二进制依赖管理得更好pip 装 Geopandas 在 Windows 上经常需要自己编译或者找 wheel容易出问题。命令大概是这样conda create -n geo_pack python3.9 -y conda activate geo_pack conda install -c conda-forge geopandas pandas fiona requests openpyxl py2exe -y这里 Python 版本我选 3.9不是随便选的。py2exe 对新版本 Python 的支持有滞后3.9 到 3.10 是比较稳的区间而 Geopandas 从 0.10 之后对 Python 3.9 支持很好。如果你用 3.11 以上可能会遇到 py2exe 还没适配的情况打包直接报错。2.2 关键依赖版本对照表版本兼容是这类项目的命门。下面这张表是我实测下来比较稳的一组组合你可以作为起点再根据自己项目微调库名推荐版本说明Python3.9.xpy2exe 兼容性最佳区间Geopandas0.12.x与 Fiona 1.8 配合稳定Pandas1.5.x避免 2.x 的部分 API 变动Fiona1.8.xGDAL 依赖的关键承载库GDAL3.4.x由 conda 自动匹配Shapely1.8.x注意 2.x 有较大改动pyproj3.4.x坐标转换依赖Requests2.28.x网络请求版本较宽松Openpyxl3.0.xExcel 读写py2exe0.13.x打包器本体注意不要盲目追新。Geopandas 和 Shapely 的大版本升级经常带来 API 不兼容打包前先确认你的脚本用的是哪套 API再决定锁哪个版本。2.3 验证环境是否干净可用装完之后别急着打包先跑一段验证脚本确认所有库都能正常导入并且核心功能可用import geopandas as gpd import pandas as pd import fiona import requests import openpyxl from shapely.geometry import Point # 验证 Geopandas 核心功能 gdf gpd.GeoDataFrame( {name: [a, b]}, geometry[Point(0, 0), Point(1, 1)], crsEPSG:4326 ) print(gdf.crs) print(fiona.__version__) print(环境验证通过)这段能跑通说明基础环境没问题。如果这里就报 DLL 错误那打包肯定也过不了先解决环境问题再说。我踩过的坑是conda 环境里同时存在 pip 装的同名库导致导入时路径混乱。检查方法是pip list和conda list对比一下看有没有重复项。3. 核心细节解析与打包配置实操3.1 py2exe 配置文件的写法与关键参数py2exe 的配置通常写在一个setup.py里。很多人卡在第一步就是不知道 Geopandas 的隐式依赖怎么声明。下面是我实际用的配置骨架from distutils.core import setup import py2exe import geopandas import fiona import os # 收集 Geopandas 和 Fiona 的数据文件目录 geo_data [] for pkg in [geopandas, fiona]: pkg_path os.path.dirname(pkg.__file__) for root, dirs, files in os.walk(pkg_path): for f in files: full os.path.join(root, f) rel os.path.relpath(full, os.path.dirname(pkg_path)) geo_data.append((os.path.dirname(rel), [full])) setup( namegeo_tool, console[{script: main.py}], options{ py2exe: { bundle_files: 1, compressed: True, includes: [ geopandas, fiona, shapely, pyproj, pandas, requests, openpyxl, fiona.ogrext, shapely.geos ], packages: [geopandas, fiona, shapely], dll_excludes: [MSVCP90.dll], } }, data_filesgeo_data, zipfileNone, )这里有几个参数必须解释清楚不然你不知道为什么这么写。bundle_files: 1表示把所有东西打进单个 exe。听起来很美但对 Geopandas 这种项目我不建议用 1因为 GDAL 的 DLL 和数据结构文件比如gdal_data、proj_data很难完全塞进单文件运行时解压路径也容易出问题。我一般用bundle_files: 3也就是不打包生成 exe 加一堆依赖文件虽然目录看着乱但稳定性高得多。includes里手动列出的fiona.ogrext和shapely.geos是关键。这两个是 C 扩展模块py2exe 的静态分析经常发现不了它们不显式声明就会在运行时 ImportError。data_files那段循环是收集 Geopandas 和 Fiona 包目录下的所有数据文件。GDAL 需要gdal_data里的坐标系统定义文件pyproj 需要proj_data里的投影参数文件这些不带上程序一调用坐标转换就崩。3.2 GDAL 数据目录的环境变量处理这是整个打包里最容易翻车的地方。GDAL 运行时需要知道gdal_data和proj_data在哪默认它去系统路径找但打包后的 exe 在别人机器上没有这些路径。解决办法是在程序入口最前面设置环境变量import os import sys if getattr(sys, frozen, False): base os.path.dirname(sys.executable) os.environ[GDAL_DATA] os.path.join(base, gdal_data) os.environ[PROJ_LIB] os.path.join(base, proj_data)sys.frozen是打包后运行时的标志用它判断当前是开发环境还是 exe 环境。这段代码必须在import geopandas之前执行否则 GDAL 已经初始化完了再设环境变量就晚了。我踩过的坑就是把这行放在 import 之后结果本机测试没问题因为本机有系统 GDAL_DATA一到别人机器就报 “Unable to open EPSG support file”。3.3 依赖收集的自动化辅助脚本手动列 includes 容易漏。我后来写了个小脚本自动扫描项目里实际用到的模块生成 includes 列表import pkgutil import importlib def collect_submodules(package_name): pkg importlib.import_module(package_name) mods [package_name] if hasattr(pkg, __path__): for _, name, _ in pkgutil.walk_packages(pkg.__path__, package_name .): mods.append(name) return mods targets [geopandas, fiona, shapely, pyproj] all_mods [] for t in targets: all_mods.extend(collect_submodules(t)) print(all_mods)把输出贴进includes能大幅减少“某个子模块没被打进去”的问题。不过要注意全量收集会让产物变大如果在意体积可以再手动剔除明显用不到的模块。4. 完整打包流程与现场记录4.1 从脚本到 exe 的完整步骤把前面的准备串起来完整流程是这样的。第一步确认入口脚本main.py里已经加了环境变量设置那段。第二步把setup.py写好includes 和 data_files 按上面的方式配置。第三步在激活的虚拟环境里执行打包命令python setup.py py2exe执行过程中会看到大量编译和收集日志。重点观察有没有 “missing module” 警告尤其是和 fiona、shapely 相关的。如果有说明 includes 还漏了东西。打包完成后产物在dist目录下结构大概是dist/ main.exe python39.dll gdal_data/ proj_data/ fiona/ shapely/ ...第四步也是最关键的一步把整个dist目录拷到一台没装 Python、没装 conda的机器上双击main.exe测试。这一步能暴露所有环境依赖问题。我自己的习惯是准备一台干净的虚拟机专门做这个验证每次打包都测一遍。4.2 参数选择背后的计算与权衡有人会问bundle_files到底选几compressed要不要开。我的经验是这样compressed: True会压缩产物体积能小 30% 左右但启动时会先解压首次启动慢几秒。如果你的程序是给内部同事用、启动频率不高开压缩没问题如果是频繁启动的小工具建议关掉换启动速度。关于产物体积Geopandas 全家桶打出来通常 200MB 到 400MB这是正常的因为 GDAL 本身就大。想瘦身的话可以在 data_files 里只保留用得到的坐标系统文件而不是整个 gdal_data 目录。但这一步需要你知道项目实际用到哪些 EPSG 代码风险是漏了某个导致运行时找不到。我的建议是先保证能跑再考虑瘦身别一上来就抠体积。4.3 入口脚本的健壮性处理打包后的程序异常处理要比开发时更完善。因为用户看不到控制台报错一出问题就是闪退。我在入口脚本里会加一层全局异常捕获把错误写到日志文件import traceback import datetime def main(): # 你的业务逻辑 pass if __name__ __main__: try: main() except Exception: with open(error_log.txt, a, encodingutf-8) as f: f.write(f\n{datetime.datetime.now()}\n) f.write(traceback.format_exc()) input(程序出错详情见 error_log.txt按回车退出)input那行是为了让窗口停住不然用户还没看清就关了。这个技巧看着土但实际交付时特别有用能省掉大量“你那边报什么错”的来回沟通。5. 常见问题与排查技巧实录5.1 典型报错速查表下面这张表是我这些年遇到的高频问题汇总基本覆盖了 80% 的打包故障报错信息根本原因解决方向ImportError: DLL load failedGDAL/Fiona 的 DLL 没收集全检查 data_files 和 includesUnable to open EPSG support fileGDAL_DATA 环境变量没设或路径错入口脚本最前面设环境变量ModuleNotFoundError: fiona.ogrextC 扩展没被静态分析发现includes 里显式声明proj.db not foundPROJ_LIB 路径不对确认 proj_data 被打进产物程序闪退无提示未捕获异常加全局异常捕获写日志杀毒软件报毒打包产物被误判换 bundle_files 配置或加白名单5.2 独家避坑经验第一个坑不要在中文路径下打包。py2exe 处理中文路径时偶尔会出问题产物里的路径编码可能错乱。我现在的习惯是项目放在纯英文路径下比如D:\projects\geo_tool省心。第二个坑Requests 的证书问题。打包后 Requests 调用 HTTPS 接口时可能找不到 certifi 的证书文件。解决办法是把 certifi 的cacert.pem也加进 data_files并在代码里显式指定import certifi import os import sys if getattr(sys, frozen, False): os.environ[REQUESTS_CA_BUNDLE] os.path.join( os.path.dirname(sys.executable), cacert.pem )第三个坑Openpyxl 读写大文件时的内存问题。打包本身不影响这个但交付后用户拿大 Excel 一跑就崩。建议在代码里用read_onlyTrue模式读大文件写的时候用write_onlyTrue能显著降低内存占用。第四个坑测试机器和打包机器的位数要一致。32 位 Python 打出来的 exe 在 64 位系统上能跑但反过来不行。而且 32 位 Python 处理大文件时内存受限Geopandas 处理稍大的空间数据就容易 MemoryError。所以统一用 64 位。5.3 持续更新的维护思路标题里写了“持续更新”这不是客套。这类项目的依赖会随库版本变化而失效我的做法是维护一个requirements_lock.txt把每次验证通过的版本组合记下来同时保留一份打包成功的dist作为基线。下次要升级某个库时先在小范围测试确认打包和运行都正常再更新基线。这样即使新版本出问题也能快速回退到已知可用的组合。另外每次遇到新报错我都会把报错信息、原因、解决步骤追加到自己的排查笔记里时间长了就形成了上面那张速查表。这个习惯比任何文档都管用因为记录的是你自己环境里真实发生过的问题。6. 产物分发与用户侧注意事项打包完成只是交付的一半怎么把产物给到用户、怎么让他们顺利运行同样有讲究。我一般会把整个dist目录压缩成一个 zip附一个简短的使用说明.txt写清楚解压后双击哪个 exe、如果报错去哪里看日志。不要只发一个 exe因为依赖文件是分开的单独发 exe 用户肯定跑不起来。还有一点如果用户的机器装了杀毒软件首次运行可能被拦截。这不是你的程序有问题而是打包产物的通病。提前在说明里告知用户“如遇拦截请添加信任”能减少很多沟通成本。我甚至遇到过企业环境直接禁止运行未签名 exe 的情况这种就只能走内部审批流程不是技术能解决的。最后分享一个我常用的验证清单每次交付前过一遍干净机器能启动、核心功能跑通、日志文件能正常生成、异常时窗口不闪退、大文件不崩。这五条都过了基本就能放心交付了。