如何跳过各种坑,将python程序用pyinstaller打包为exe?

之前写过一篇文章,讨论如何用selenium爬取句子迷网页上的箴言,以及如何将这些句子写到图片中,再将图片设置为桌面背景,并且定时更新,有兴趣可以瞧瞧。

这篇文章讨论一下如何将以上的python程序用pyinstaller打包成exe,从而可以更加容易的分享给别人,让他人羡慕嫉妒恨。

首先将python程序转换成exe,常用的有pyinstaller,py2exe, nuitka。最后一个nuitka是先将python代码转换成c++代码,然后编译生成exe文件,执行效率会提升很多的。py2exe据说跨平台很差,而且不能打包成单个exe文件。所以根据网上的舆论重点研究了一下pyinstaller,本文的重点和难点在打包定时执行模块apscheduler的时候遇到很多问题,同时如何把用到的图片、字体、文本等数据文件打包进exe

源程序请看之前的链接,在打包过程中遇到各中模块导入的问题,这里有个技巧,生成后的exe文件如果直接双击执行,出错以后,一闪而过,根本来不及看清报错信息,有人聪明的在源码中加入input(),但是实际上出错的时候还没有执行到input,所以还是一闪而过。我开始很愚蠢,企图用windows的截图留下那惊鸿一瞥,可是试了数次都没有成功,毕竟这需要眼疾手快,逼的我甚至想写个自动截图的脚本。幸亏后来急中生智,原来exe也是可以在cmd中直接敲文件名运行,这样即使出错了,也是雁过留声,有迹可循。

好了,关于模块出错的问题,刚开始以为少了apscheduler的hook文件,于是在网上找到了一个apscheduler的hook文件(网页很长,自习找找)。用pyinstaller -F --additional-hooks-dir add_hook set_wall_paper.py 打包,然而生成的exe文件依然报错,No trigger by the name “interval” was found,网上说是要升级setuptools,但是升级后并没有用。于是继续找,有说法是需要立即导入IntervalTrigger这个模块,在程序头加入

from apscheduler.triggers import interval

运行之后还是产生一下错误:No module named 'apscheduler.triggers.interval', 继续谷歌,发现了stactoverflow上的解决方法。

按照解决方法终于改成了,需要手动创建一个

trigger_interval = IntervalTrigger(seconds=60)
scheduler.add_job(set_wallpaper, args=(pic_files, poet_files, fonts_dir), 
                  trigger=trigger_interval)

以上终于通过各种谷歌解决了apscheduler模块的问题,接着想把用到字体、图片、json数据一起打包到exe文件中,按照官网的介绍,直接在命令行添加参数或者修改spec文件即可。生成spec文件用pyi-makespec命令,参数和pyinstaller一样的。之后生成exe的时候把使用pyinstaller -F set_wall_paper.spec即可。修改spec文件更加直观可控,也挺简单的,用个记事本打开,以下是修改后的spec文件:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['set_poet_wallpaper.py'],
             pathex=['D:\\小工具开发结果\\PoetWallPaper'],
             binaries=[],
             datas=[('bgpics/*.jpg', 'bgpics'),
                    ('mottos.json', '.'),
                    ('fonts/*.ttf', 'fonts')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='set_poet_wallpaper',
          debug=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True )

其中datas=[....]中的内容即是添加的数据文件,tuple前项是源文件路径,后项是文件在exe bundle中的相对路径,嗯,现在来说挺简单的,问题出现了在程序中如何使用bundle中的文件,如何不更改源程序,那么程序每次读取的还是源文件,必须把exe文件和那个文件夹放在一起才能执行,这并不是我的目的,我是想让exe随便到哪里都可以独自执行,不依赖他人,如同我一样。

怎么办?官网给出了一点提示,反复看了几遍才懂。 有谷歌了一番,基本都是官网那套说辞,不过这个例子不错,官网上有个例子,显示文件在frozen和不frozen的时候几个参数分别是什么路径。自己打包试一下,就理解了。exe执行的时候会创建一个temp文件夹,打包进入的数据的父目录即是这个temp文件夹,根据官网的例子可以得到父目录的path,于是os.path.join一样就可以了,注意所有从外部打包到exe的数据文件都要join一下。最终的代码如下,有兴趣的同学可以和之前的对比一下:

# -*- coding: utf-8 -*-
"""
Created on Wed Aug 15 11:30:56 2018

@author: xiaozhen
"""

import os, json, sys
from PIL import Image, ImageFont, ImageDraw
import win32api
import win32con
import win32gui
import random
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.interval import IntervalTrigger


def reformat(string):
    string_lst = string.splitlines()
    format_string = []
    for line in string_lst:
        for i in range(len(line)//25+1):
            format_string.append(line[i*25:25*(i+1)])
    return '\n'.join(format_string)


def set_wallpaper_from_bmp(bmp_path):

    # 打开指定注册表路径
    reg_key = win32api.RegOpenKeyEx(
        win32con.HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, win32con.KEY_SET_VALUE)
    # 最后的参数:2拉伸,0居中,6适应,10填充,0平铺
    win32api.RegSetValueEx(reg_key, "WallpaperStyle", 0, win32con.REG_SZ, "2")
    # 最后的参数:1表示平铺,拉伸居中等都是0
    win32api.RegSetValueEx(reg_key, "TileWallpaper", 0, win32con.REG_SZ, "0")
    # 刷新桌面
    win32gui.SystemParametersInfo(
        win32con.SPI_SETDESKWALLPAPER, bmp_path, win32con.SPIF_SENDWININICHANGE)


def random_poems(poet_files):
    poet_file = random.choice(poet_files)
    with open(poet_file, 'r', encoding='utf-8') as json_file:
        poems = json.load(json_file)
    # preferred_poets = ('李白', '杜甫', '孟浩然')
    # for i in range(1000):
    #     poem = random.choice(poems)
    #     if poem['author'] in preferred_poets:
    #         break
    poem = random.choice(poems)
    poem_content = poem['paragraphs'][0]
    poem['paragraphs'] = [reformat(poem_content)]
    poem_string = '\n'.join([poem['title'], poem['author']] + poem['paragraphs'])
    return poem_string


def set_wallpaper(img_files, poem_files, fonts_dir):

    # 把图片格式统一转换成bmp格式,并放在源图片的同一目录
    img_path = random.choice(img_files)
    img_dir = os.path.dirname(img_path)
    bmpImage = Image.open(img_path)
    bmpImage = bmpImage.resize((1920, 1080), Image.ANTIALIAS)
    draw = ImageDraw.Draw(bmpImage)
    font = random.choice([os.path.join(fonts_dir, file)
                            for file in os.listdir(fonts_dir)])
    print(font)
    fnt = ImageFont.truetype(font, 40)
    poem_str = random_poems(poem_files)
    print(poem_str)
    width, height = bmpImage.size
    draw.multiline_text((width/4, height/5), poem_str, fill='#000000', 
                        font=fnt, anchor='center', spacing=10, align="center")
    new_bmp_path = os.path.join(img_dir, 'wallpaper.bmp')
    bmpImage.save(new_bmp_path, "BMP")
    set_wallpaper_from_bmp(os.path.abspath(new_bmp_path))



if __name__ == '__main__':

    if getattr(sys, 'frozen', False):
        # we are running in a bundle
        bundle_dir = sys._MEIPASS
    else:
        # we are running in a normal Python environment
        bundle_dir = os.path.dirname(os.path.abspath(__file__))

    pic_path = os.path.join(bundle_dir, 'bgpics')

    pic_files = [os.path.join(pic_path, file) for file in os.listdir(pic_path)
                    if 'wallpaper' not in file]

    poet_files = [os.path.join(bundle_dir, 'mottos.json')]
    fonts_dir = os.path.join(bundle_dir, 'fonts')
    set_wallpaper(pic_files, poet_files, fonts_dir)
    scheduler = BlockingScheduler()
    trigger_interval = IntervalTrigger(seconds=60)
    scheduler.add_job(set_wallpaper, args=(pic_files, poet_files, fonts_dir), 
                      trigger=trigger_interval)  # 设置间隔时间
    # scheduler.add_job(set_wallpaper, args=(pic_files, poet_files,), 
    #                   trigger='interval', seconds=60)  # 设置间隔时间
    scheduler.start()

    

最后打包出来的exe文件33M左右,这是包含了20多M左右的字体、图片文件的,所以总的来说可以接受。当然还可以用tkinter做个界面,可以设置间隔时间、增加背景图片、字体等,有空再说。 

你可能感兴趣的:(python)