上一篇文章中已经实现了打包以及版本管理的功能,软件已经可以发布到服务器上,只实现简单的版本检查,要实现自动下载并更新软件还有不少问题要处理。
实现方式
在启动程序的时候开启一条后台线程去检查更新,如果有更新就提示客户是否更新,软件默认是强制更新,不更新直接退出程序。
更新又分为手动更新和自动更新:
- 手动更新,自动打开浏览器跳转下载地址,需要手动去覆盖
- 自动更新,自动下载并更新软件,自动覆盖
检查更新
和上次文章的差不多,实现检查更新功能,需要在启动程序的时候开启一条后台线程去检查更新:
class Updater:
def __init__(self, base_url: str):
self.base_url = base_url
def check_for_update(self, version):
check_url = self.base_url + '/versions.json'
res = requests.get(check_url).json()
if len(res) and semver.compare(res[0]['version'], version) > 0:
return res[0]
if __name__ == '__main__':
app = tkinter.Tk()
t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
t.daemon = True # 守护为True,设置True线程会随着进程一同关闭
t.start()
app.mainloop()
然后在此基础上实现手动和自动更新
手动更新
第一步先实现检查到新版本能手动下载文件并自己手动覆盖,保证如果自动更新有问题也能手动更新软件。
实现如下:
import threading
import webbrowser
from test import Updater
from versions import current_version
import tkinter
import tkinter.messagebox
def check_for_update(window):
updater = Updater('https://xxxx/app')
ver = updater.check_for_update(current_version)
print(ver)
if ver is not None:
publish_notes = '\n'.join(ver.get('publishNotes', []))
message = f'当前版本[{current_version}], 有新版本[{ver["version"]}],更新前请关闭相关打开文件.\n' \
f'更新内容:\n{publish_notes}\n请选择立即去下载更新[确定],暂不更新[取消]?'
result = tkinter.messagebox.askokcancel(title='更新提示', message=message)
if result:
browser_update(ver, window)
else:
window.destroy()
def browser_update(ver, window):
webbrowser.open(ver.get('updateUrl'))
window.destroy()
if __name__ == '__main__':
app = tkinter.Tk()
t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
t.daemon = True # 守护为True,设置True线程会随着进程一同关闭
t.start()
app.mainloop()
检查结果如下:

点击【确定】跳转到浏览器去下载,点击【取消】自动关闭程序
自动更新
自动更新有一定难度,因为通常情况下app.exe
正在使用中,不能自己把自己更新掉。
常规做法是用另外一个Updater.exe
来实现更新操作,我们目前打包的是单文件exe
,再来一个exe
不太现实,Python
打包包含有Python
的运行环境,软件包一般都很大,目前选择用开启一个bat
文件来更新exe
文件。
更新步骤
步骤如下:
- 使用
requests
下载zip
文件到tmp
目录 - 解压
tmp
目录下的zip文件
- 复制解压文件覆盖现有除
app.exe
之外的其他文件和文件夹 - 用子进程启动
bat
文件,然后关闭当前程序 - 用
bat
文件更新app.exe
文件,启动新app.exe
文件
更新工具
自动更新工具代码如下:
import os
import shutil
import textwrap
import threading
import zipfile
import requests
from tqdm.tk import tqdm
import tkinter
class AutoUpdater:
def __init__(self, download_url, **kwargs):
self.download_url = download_url
self.tmp_path = kwargs.get('tmp_path', 'tmp')
self.file_name = kwargs.get('file_name')
if not os.path.exists(self.tmp_path):
os.mkdir(self.tmp_path)
if not self.file_name:
self.file_name = self.download_url.split('/')[-1]
def download_file(self):
th = threading.Thread(target=self.do_download_file)
th.start()
th.join()
def do_download_file(self):
download_file = os.path.join(self.tmp_path, self.file_name)
if not os.path.exists(download_file):
print(f'downloading file: {self.download_url}')
response = requests.get(self.download_url, stream=True)
chunk_size = 1024 # 每次下载的数据大小
content_size = int(response.headers['content-length']) # 下载文件总大小
tq = tqdm(iterable=response.iter_content(chunk_size=chunk_size), tk_parent=None,
desc=f'下载文件:{self.file_name}',
leave=False, total=content_size, unit='B', unit_scale=True)
with open(download_file, 'wb') as file:
for data in tq:
tq.update(len(data))
file.write(data)
file.flush()
tq.close()
print(f'downloaded file: {self.file_name}')
def extract_files(self):
download_file = os.path.join(self.tmp_path, self.file_name)
extract_dir = self.get_extract_dir()
if not os.path.exists(extract_dir):
os.mkdir(extract_dir)
with zipfile.ZipFile(download_file) as zf:
zf.extractall(path=extract_dir) # 解压目录
print(f'extract file: {self.file_name}')
def get_extract_dir(self):
download_file = os.path.join(self.tmp_path, self.file_name)
return download_file[:download_file.rfind('.')]
def replace_files(self):
self.do_replace_files()
def check_files(self):
# 校验文件完整性,暂未实现
pass
def do_replace_files(self):
extract_dir = self.get_extract_dir()
for file in os.listdir(extract_dir):
if not file.endswith('app.exe'):
try:
print(f'替换文件:{file}')
self.copy_files(os.path.join(extract_dir, file), '.')
except BaseException as e:
if os.path.isdir(file):
tkinter.messagebox.showwarning(title='错误', message=f'文件夹[{file}]有文件正在使用,更新失败,请关闭文件后重试')
else:
tkinter.messagebox.showwarning(title='错误', message=f'文件[{file}]正在使用,更新失败,请关闭文件后重试')
print(f'替换文件错误{e}')
raise e
@staticmethod
def copy_files(src_file, dest_dir):
file_name = src_file.split(os.sep)[-1]
if os.path.isfile(src_file):
shutil.copyfile(src_file, os.path.join(dest_dir, file_name))
else:
dest_path = os.path.join(dest_dir, file_name)
if os.path.exists(dest_path):
shutil.rmtree(dest_path)
shutil.copytree(src_file, os.path.join(dest_dir, file_name))
def make_updater_bat(self):
app_name = 'app.exe'
app_file = os.path.join(self.get_extract_dir(), app_name)
with open('updater.bat', 'w', encoding='utf8') as updater:
updater.write(textwrap.dedent(f'''\
@echo off
echo 正在更新[{app_name}]最新版本,请勿关闭窗口...
ping -n 2 127.0.0.1 > nul
echo 正在复制[{app_file}],请勿关闭窗口...
del app.exe
copy {app_file} . /Y
echo 更新完成,等待自动启动{app_name}...
ping -n 5 127.0.0.1 > nul
start app.exe
exit
'''))
updater.flush()
def auto_update(self):
self.download_file() # 下载更新文件
self.check_files() # 校验文件,暂未实现
self.extract_files() # 解压文件
self.make_updater_bat() # 生成替换脚本文件
self.replace_files() # 更新文件
调用工具更新
调用AutoUpdater
实现自动更新,同时保留手动更新的逻辑,更新代码如下:
import subprocess
import threading
import webbrowser
from autoupdates import AutoUpdater
from test import Updater
from versions import current_version
import tkinter
import tkinter.messagebox
def check_for_update(window):
updater = Updater('https://xxxx/app')
ver = updater.check_for_update(current_version)
if ver is not None:
publish_notes = '\n'.join(ver.get('publishNotes', []))
message = f'当前版本[{current_version}], 有新版本[{ver["version"]}],更新前请关闭相关打开文件.\n' \
f'更新内容:\n{publish_notes}\n请选择立即自动更新[是],手动下载更新[否],暂不更新[取消]?'
force_update = ver.get('forceUpdate', False)
result = tkinter.messagebox.askyesnocancel(title='更新提示', message=message)
if result is not None:
if result:
print(f'自动更新版本[{current_version}]->[{ver["version"]}]')
auto_update(ver, window)
else:
print(f'浏览器更新版本[{current_version}]->[{ver["version"]}]')
browser_update(ver, window)
elif force_update:
window.destroy()
def browser_update(ver, window):
webbrowser.open(ver.get('updateUrl'))
window.destroy()
def auto_update(ver, window):
window.withdraw()
updater = AutoUpdater(ver.get('updateUrl'))
try:
updater.auto_update()
# 使用bat文件更新exe文件,其他文件用python覆盖
subprocess.Popen(f'updater.bat')
finally:
print('关闭主窗口.')
window.destroy()
if __name__ == '__main__':
app = tkinter.Tk()
t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
t.daemon = True # 守护为True,设置True线程会随着进程一同关闭
t.start()
app.mainloop()
检查结果,自动下载也包含手动下载:

更新使用tqdm
展示tkinter
进度条,方便看到下载进度。
自动产生的bat文件如下:
@echo off
echo 正在更新[app.exe]最新版本,请勿关闭窗口...
ping -n 2 127.0.0.1 > nul
echo 正在复制[tmp\xxxx\app.exe],请勿关闭窗口...
del app.exe
copy tmp\xxxx\app.exe . /Y
echo 更新完成,等待自动启动app.exe...
ping -n 5 127.0.0.1 > nul
start app.exe
exit
里面使用ping
命令实现延迟几秒执行下一步操作。
目前已经测试过能够使用这种方式自动更新。
老大 自动更行小窗口不居中 一般咋处理啊,捣鼓了半天,又换回了手动更新, 无赖没解决掉心有不甘的,又起来捣鼓
一直提示程序占用 没办法更新自己本身的exe