【python三种打包方式】分别使用PyInstaller & setupTools & 拷贝环境实现项目打包
1、setupTools和PyInstaller打包的区别
pyinstaller
和setuptools
打包有什么区别(来自ChatGPT
的答案)
pyinstaller
和setuptools
都是Python
中常用的打包工具,但它们的主要目的和工作方式略有不同。
pyinstaller
是一个可以将Python
代码打包成一个可执行文件的工具,它会将你的Python
代码和所有依赖项打包成一个单独的可执行文件,让你的代码可以在没有Python
解释器的情况下运行。这使得你可以将你的 Python 应用程序发布给没有安装 Python 的用户,或者将它们放在没有 Python 环境的机器上。
setuptools
则是一个用于构建Python
软件包的工具,它可以帮助你将你的Python
代码打包成一个可安装的软件包,你可以将这个软件包发布到 PyPI 或其他软件包管理器中。setuptools
还提供了许多用于定义软件包元数据、依赖项和其他设置的选项,以及用于打包软件包的命令行工具。因此,尽管
pyinstaller
和setuptools
两者都能将 Python 代码打包成一个独立的分发形式,但它们的主要目的略有不同。
如果你想要将你的
Python
应用程序打包成一个单独的可执行文件,可以选择pyinstaller
。如果你想要将你的
Python
代码打包成一个可安装的软件包并将其发布到PyPI
或其他软件包管理器中,可以选择setuptools
。
2、使用setupTools打包本地项目
参考
-
【python】上传 Python 包到 pypi 官网
-
【Python】如何在PyPI上发布自定义软件包
-
Packaging and distributing projects
-
Python 项目代码写完了,然后怎么打包和发布?
0)项目准备 & 生成requirements.txt
& 编写打包文件
a)项目准备
假设Python项目的目录结构如下:
其中pdf_handler.py
代码为:
import pdfplumber
from PyPDF2 import PdfReader, PdfWriter
import os
import sys
pdf_path = "resume.pdf" #相对位置,不能使用绝对位置
#提取PDF文字
def extract_pdf_text():
# 提取pdf指定页的文字
with pdfplumber.open(pdf_path) as pdf:
page01 = pdf.pages[0] # 指定页码
text = page01.extract_text() # 提取文本
print(f"第一页pdf的文本内容为:{text}",end='\n')
#提取所有页pdf文字
with pdfplumber.open(pdf_path) as pdf:
print(f"所有页pdf的文本内容如下:")
for page in pdf.pages: #遍历所有页
text = page.extract_text() # 提取当前页的文本
print(text)
#提取PDF表格
def extract_pdf_table():
with pdfplumber.open(pdf_path) as pdf:
print(f"所有页pdf的表格内容如下:")
for page in pdf.pages: # 遍历所有页
table = page.extract_table() # 提取当前页的文本
print(table)
#分割PDF
def split_pdf():
#如果使用PdfFileReader会抛出异常:PyPDF2.errors.DeprecationError: PdfFileReader is deprecated and was removed in PyPDF2 3.0.0. Use PdfReader instead.
file_reader = PdfReader(pdf_path) #实例化pdf reader对象
# getNumPages() 获取总页数会报错:PyPDF2.errors.DeprecationError: reader.getNumPages is deprecated and was removed in PyPDF2 3.0.0. Use len(reader.pages) instead.
for page in range(len(file_reader.pages)):
file_writer = PdfWriter() #实例化pdf writer对象
# 将遍历的每一页对象添加到pdf writer对象中,使用file_reader.getPage(page)会报错:PyPDF2.errors.DeprecationError: reader.getPage(pageNumber) is deprecated and was removed in PyPDF2 3.0.0. Use reader.pages[page_number] instead.
file_writer.add_page(file_reader.pages[page]) #PyPDF2.errors.DeprecationError: addPage is deprecated and was removed in PyPDF2 3.0.0. Use add_page instead.
with open(f'{os.path.join(saveDir,str(page)+".pdf")}', 'wb') as out:
file_writer.write(out)
其中main.py
为主函数入口,代码为:
from pdf_task.pdf_handler import extract_pdf_text,extract_pdf_table,split_pdf
import os
if __name__ == '__main__':
print("Hello world")
extract_pdf_text()
extract_pdf_table()
split_pdf()
os.system("pause")
这里创建两个完全一样的pdf_task
,用来模拟pypi_test
包中包含这两个模块。
b)生成requirements.txt
接着在项目根目录下(pypi_test
)使用pipreqs
,生成关于整个包的第三方依赖文件requirements.txt
。(参考python生成requirements.txt的两种方法)
pip install pipreqs -i https://pypi.tuna.tsinghua.edu.***/simple
#生成requirements.txt命令如下
pipreqs . --encoding=utf8 --force #如果requirement.txt已存在,则
requirement.txt
生成内容如下:
pdfplumber==0.8.0
PyPDF2==3.0.1
setuptools==67.3.3
c)编写打包文件
接着分别创建并编写相应的打包文件:
-
README.md
:关于这个项目的具体描述(自定义) -
LICENSE
:对关于项目所有权即使用约束等问题的描述(自定义,参考https://choosealicense.***/
) -
pyproject.toml
:需要指定setuptools
的最低版本,脚本内容如下[build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta"
-
setup.py
:使用setuptools
进行打包的脚本,内容包括如下几个部分,参考Python 项目代码写完了,然后怎么打包和发布?name: 你定义的包名,可以用字母、数字、下划线,需要确保唯一性。 version: 项目的版本号。 author: 你(作者)的名称。 author_email: 你(作者) 的邮箱。 description: 项目的简要描述。 long_description_content_type:长描述内容的使用的标记类型,一般为 markdown 或者 rst。 url: 你这个项目的主页地址,也可以直接链接到你这个项目的Github 地址上面去。 include_package_data: 是否添加 py 以外的文件。 package_data: 需要添加 Python 的额外文件列表。 packages: 直接用 setuptool 找到你项目所有相关的包列表。这里直接使用find_packages()自动寻找 classifiers: 附加说明,比如这里写的就是使用于 Python3 版本,使用的是 MIT 协议,独立于 OS。 python_requires: python 版本要求。
我编写的
setup.py
脚本如下:#参考 https://juejin.***/post/7053009657371033630, https://zhuanlan.zhihu.***/p/527321393 #!/usr/bin/env python from os import path from setuptools import setup, find_packages here = path.abspath(path.dirname(__file__)) with open(path.join(here, 'requirements.txt'), 'r', encoding='utf-8') as f: all_reqs = f.read().split('\n') with open(path.join(here, 'README.md'), 'r', encoding='utf-8') as f: long_description = f.read() install_requires = [x.strip() for x in all_reqs if 'git+' not in x] setup( name='pypi_test', # 必填,项目的名字,用户根据这个名字安装,pip install SpiderKeeper-new version='1.0.0', # 必填,项目的版本,建议遵循语义化版本规范 author='wangxiaoxi', # 项目的作者 description='pypi测试', # 项目的一个简短描述 long_description=long_description, # 项目的详细说明,通常读取 README.md 文件的内容 long_description_content_type='text/markdown', # 描述的格式,可选的值: text/plain, text/x-rst, and text/markdown author_email='1046474088@qq.***', # 作者的有效邮箱地址 url='https://github.***/test', # 项目的源码地址 license='MIT', include_package_data=True, package_data= { 'src' : ["resources/"] }, packages=find_packages(), # 必填,指定打包的目录,默认是当前目录,如果是其他目录比如 src, 可以使用 find_packages(where='src') install_requires=install_requires, # 指定你的项目依赖的 python 包,这里直接读取 requirements.txt # 分类器通过对项目进行分类,帮助用户找到项目 classifiers=[ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', ], python_requires=">=3.8" )
编写好以上所有文件之后,目录结构如下:
1)打包成源码 & 二进制可安装软件包(whl
)并引用
-
先升级一下
setuptools
和wheel
的版本:pip install --upgrade setuptools wheel
-
接着在根目录(
pypi_test
),使用如下命令打包源码:python setup.py sdist
此时会在根目录下产生一个
dist
文件夹,里面有打包好的源码压缩包pypi_test-1.0.0.tar.gz
,解压好后即为根目录下的python
源码。 -
接着在根目录(
pypi_test
),使用如下命令打包源码:python setup.py bdist_wheel
此时会在
dist
文件夹中生成一个该项目的二进制可安装文件pypi_test-1.0.0-py3-none-any.whl
;并且生成一个build
文件夹,其中lib
包含整个项目两个模块的python
源码,不包括静态资源。 -
如果想同时打包源码,并且打包二进制可安装软件包,可以直接使用如下命令:
python setup.py sdist bdist_wheel
打包后的目录结构如下:
如果要想引用这个包,可以直接pip install .\pypi_test-1.0.0-py3-none-any.whl
,
接着在新项目中创建一个python_test
去直接引用这个项目中的pdf_task
,pdf_task2
两个模块。
from pdf_task.pdf_handler import extract_pdf_text
extract_pdf_text()
# 或许会报找不到静态资源的错误:FileNotFoundError: [Errno 2] No such file or directory: 'D:\\programSoftware\\python\\anaconda\\envs\\spider_env\\lib\\site-packages\\pdf_task\\resume.pdf',
# 可以将resume.pdf放到"site-packages/pdf_task/'中即可解决
如果不想使用这个包,可以通过pip uninstall pypi_test
进行卸载,其中pypi_test
是在setup.py
中配置的项目名。
2)上传whl到PyPI
先在https://pypi.org/a***ount/register/注册一个pypi
账号:
接着安装twine
:
pip install twine
最后在pypi_test
目录下,将setupTools
打包好的dist
文件上传到pypi
上:
twine upload dist/*
其中可能存在的问题:
报错1:上传的
dist
文件夹中包含除whl
之外的文件夹(spider_env) PS D:\桌面\pypi_test> twine upload dist/* Uploading distributions to https://upload.pypi.org/legacy/ ERROR InvalidDistribution: Unknown distribution format: 'pypi_test-1.0.0'
解决方法:删除掉
dist
文件夹中多余的文件夹。报错2:该项目名已被别人使用,参考HTTPError: 403 Client Error: The user allowed to upload to project · GitHub
ERROR HTTPError: 403 Forbidden from https://upload.pypi.org/legacy/ The user 'wangxiaoxi' isn't allowed to upload to project 'pypi_test'. See https://pypi.org/help/#project-name for more information.
解决方法:删除掉原来的
whl
文件,修改setup.py
中的项目名并重新打包,重新上传。这里将项目名修改为wangwangwang_pypi_test
。
成功完成上传后,可以通过控制台中的链接访问:
3)可能存在的问题
模块名不能是python
关键字(比如main
),否则打包虽然成功,但是不能直接引用。
3、使用PyInstaller打包本地项目
参考
-
Pyinstaller打包文件太大的解决方案_python_脚本之家
-
使用pyQt5 + agora + leanCloud实现基于学生疲劳检测的在线课堂_学生上课疲劳监测
1)PyInstaller将主函数打包成可执行文件
关于pyinstaller
打包命令参数如下:
--distpath <path>: 打包到哪个目录下
-w: 指定生成 GUI 软件,也就是运行时不打开控制台
-c: 运行时打开控制台
-i <Icon File>: 指定打包后可执行文件的图标
--clean: 在构建之前清理PyInstaller缓存并删除临时文件
这里依然使用上面的项目举例子,但和setuptools
打包不同的是,这里只对pdf_task
单个模块中的main.py
入口函数进行打包(其中main.py
在pdf_task
根目录下),由于不包含GUI桌面,因此使用保留控制台输出,打包命令命令如下:
(spider_env) PS D:\桌面\pypi_test> pyinstaller --distpath D:/pypi_package/Release/ --clean pdf_task/main.py
打包后会在当前目录(pypi_test
)下生成一个build
文件夹,并在distpath
路径下生成发布版(Release
)的打包文件。
2)导包和静态文件引入问题
Note:
-
在打包时要注意将
main.py
放在模块的根目录(这里是pdf_task
)下,并且python
文件在import
导入包内的其他模块时,必须要加上根目录名:比如这样在执行
Release
中的可执行文件时,会抛出No module named 'pdf_handler'
的错误:from pdf_handler import extract_pdf_text,extract_pdf_table,split_pdf import os if __name__ == '__main__': print("Hello world") extract_pdf_text() extract_pdf_table() split_pdf() os.system("pause")
需要修改成如下导入方式才不会报
pdf_handler
没找到的错误(前提是pdf_task
是一个Python包,其中必须包含一个空的__init__.py
,否则还会报错):from pdf_task.pdf_handler import extract_pdf_text,extract_pdf_table,split_pdf import os if __name__ == '__main__': print("Hello world") extract_pdf_text() extract_pdf_table() split_pdf() os.system("pause")
-
如果直接执行上面
Release
文件夹下的main.exe
会报如下错误Note:如果控制台一闪而过,则使用带GUI界面的命令
pyinstaller --distpath D:/pypi_package/Release/ -w --clean pdf_task/main.py
,这样可以在界面上查看报错信息。如果想定位报错的信息,进而引入缺失的静态文件(配置文件或者静态代码库)的话,可以在
python
中使用raise Exception()
来定位。Traceback (most recent call last): File "pdf_task\main.py", line 6, in <module> File "pdf_task\pdf_handler.py", line 17, in extract_pdf_text File "pdfplumber\pdf.py", line 71, in open FileNotFoundError: [Errno 2] No such file or directory: 'D:\\桌面\\pypi_test\\src\\Release\\main\\pdf_task\\resume.pdf'
解决方法是将模块中的静态文件原封不动地拷贝到
Release/main
中(及拷贝整个pdf_task
并删掉所有不必要的py
文件):这样重新执行
Release
文件夹下的main.exe
就没有问题了。
3)其他问题
-
如果项目没有GUI界面,但在
pyinstaller
打包时不小心使用-w
选项,导致在程序启动后并没有关闭的按钮可供操作,这里假设我执行的是job_scheduler.exe
,在windows10
中如果要想结束这个程序,可以如下操作(参考windows环境下,CMD控制台查看进程、结束进程相关命令 | 码农家园):C:\Users\THINKPAD>tasklist | findstr job_scheduler.exe job_scheduler.exe 42180 Console 1 170,928 K C:\Users\THINKPAD>tasklist /f /t /im job_scheduler.exe 错误: 无效参数/选项 - '/f'。 键入 "TASKLIST /?" 以了解用法。 C:\Users\THINKPAD>taskkill /f /t /im job_scheduler.exe 成功: 已终止 PID 42180 (属于 PID 10888 子进程)的进程。
如果能在任务管理器中找到也可以。
-
windows10
开机自启动exe
参考Win10系统下设置软件(.exe可执行程序)开机自启方法_RobVisual -
如果想将
exe
可执行文件发布到gitee
上,可以参考官方教程:什么是 Release(发行版) - Gitee.***
4、拷贝环境并编写执行脚本
还是以上面pypi_test
中的pdf_task
模块为例,如果我想让别人在同样操作系统上,在解压好压缩包后直接调用pdf_task/main.py
方法,可以将python
环境直接拷贝到项目的根目录下,并编写main.py
的启动脚本即可。
-
直接将
python
虚拟环境spider_env
中的所有文件拷贝到根目录下的py38
中; -
编写启动脚本,执行即可运行
main.py
;py38\python.exe pdf_task\main.py pause
项目结构如下:
不同于setuptools
和pyInstaller
打包,直接拷贝Python
环境的好处是可以保证原项目结构的完整性,但缺点是对不同系统的移植性会比较差。