看了一下網友的思路,大致都如下:
-
運行主程序,同時檢查是否有更新
-
下載更新包
-
退出主程序
-
解壓更新包,覆蓋舊版本的文件(主程序退出以后,才能解壓覆蓋)
這里的問題就是如何覆蓋舊版本文件,因為 exe 不像 py 文件,py 文件運行時是可以自己刪除自己,或者覆蓋自身的,譬如:
test.py
import subprocess
subprocess.Popen('del test.py', shell=True) # 運行后,會刪除自身:test.py
但是一旦你將它打包成 .exe 文件,就不行了。這是因為 exe 本身在執行時,文件是被占用的狀態,而我們不能在文件占用的時候刪除自身。
所以想要解決這個問題,就需要先制作一個程序,這個程序和主程序完全獨立,作為一個獨立的進程來運行。它專門用來進行更新包的解壓覆蓋,假設叫 update.exe, 下面是一個偽代碼,來模擬更新:
主程序:
if old_version < new_version:
download(file) # 下載更新包
subprocess.Popen('update.exe -args') # update.exe 這個程序啟動時,可以先延遲兩秒,等待當前程序退出,然后自動去解壓更新包,來覆蓋主程序, 想要傳遞參數,可以將 update.exe 寫成一個可接受命令行參數的程序,來傳遞主程序的安裝位置等信息
sys.exit() # 退出主程序
更新程序:update.exe
import time
def unzip(src, dst):
...
if __name__ == "__main__":
time.sleep(2)
unzip(src, dst)
下面的代碼來源於 CSDN博主「Linuxnot」:https://blog.csdn.net/u013193899/article/details/78686039
這個例子更加簡潔直觀,它臨時寫一個 bat 文件,來刪除和運行新程序
import os
import sys
import subprocess
#編寫bat腳本,刪除舊程序,運行新程序
def WriteRestartCmd(exe_name):
b = open("upgrade.bat",'w')
TempList = "@echo off\n"; # 關閉bat腳本的輸出
TempList += "if not exist "+exe_name+" exit \n"; # 新文件不存在,退出腳本執行
TempList += "sleep 3\n" # 3秒后刪除舊程序(3秒后程序已運行結束,不延時的話,會提示被占用,無法刪除)
TempList += "del "+ os.path.realpath(sys.argv[0]) + "\n" # 刪除當前文件
TempList += "start " + exe_name # 啟動新程序
b.write(TempList)
b.close()
subprocess.Popen("upgrade.bat")
sys.exit() # 進行升級,退出此程序
def main():
# 新程序啟動時,刪除舊程序制造的腳本
if os.path.isfile("upgrade.bat"):
os.remove("upgrade.bat")
WriteRestartCmd("newVersion.exe")
if __name__ == '__main__':
main()
sys.exit()
打包的坑
當用 pyinstaller 打包成一個單獨的 exe 執行程序時,不要在腳本中使用 __file__ 來作為當前程序的路徑(打包成文件夾形式沒關系,但如果使用 pyinstaller -F 打包成單獨的exe就不行),在 .py 文件中這個路徑是正確的,但是一旦你將 .py 文件打包成 .exe, 這個路徑就不對了! 譬如下面的腳本:
print(__file__)
當作 py 文件運行時,結果為: C:\Users\UNCO9CA\Desktop\test\test.py,這是正確的路徑。
打包成單獨的 exe 文件時(即使用 pyinstaller -F test.py 選項打包):
C:\Users\UNCO9CA\Desktop\test\dist>test.exe
C:\Users\UNCO9CA\AppData\Local\Temp\1\_MEI114762\test.py # 跟預想的不一樣!這是因為當打包成一個單獨的 exe 文件后,運行它時它會現在一個臨時文件夾解壓然后運行,所以路徑就變得不可知了。而且后綴依然是 .py
因此,不要使用 __file__ 來獲取運行程序的路徑,而是使用: sys.argv[0]
