pyinstaller打包成無控制檯程序時運行出錯,與popen衝突的解決方法

有時候我們需要在程序裏執行一些cmd命令,使用os或者其它模塊中的popen方法去執行

這個問題一般是程序內有輸入導致的,這個輸入可以是input(),也可以是其它的一些stdin操作(如os.popen實際上會造成輸入請求)

本質上就是:使用-w參數(無控制檯)打包時程序裏不要請求輸入

或者,你也可以不用-w參數,手動隱藏控制檯!


有一天,我把使用了os.popen方法的python程序用pyinstaller打包成exe(用了無控制檯打包參數-w
雙擊運行時程序卻彈框報錯!
在這裏插入圖片描述

我就有點納悶:爲什麼有控制檯打包出來的exe(不使用-w參數)可以運行,使用-w參數(無控制檯)打包的卻不能運行呢?


首先,調用os.popen部分的代碼大概是下面這樣的:

with os.popen('taskkill /f /t /im nginx.exe') as re: # 殺掉nginx
    result = re.read()

執行cmd,殺死nginx。


經過研究,上結論:

os.popen 會打開一個管道執行命令,而管道是有輸入(stdin)、輸出(stdout) 的!

  • 重點就在輸入(stdin)這裏:

當我們使用pyinstaller的-w 參數(或Console=False)打包exe時,python解釋器是不帶控制檯的,

所以它沒有辦法處理輸入(stdin) !
包括使用python的input()函數也是不行的,都會彈框報錯。

  • 那麼怎麼辦呢?接着看!

os.popen 實際上是一個簡單的封裝,我們先來看他的原型:subprocess.popen

subprocess.Popen(
	args, 
	bufsize=0, 
	executable=None, 
	stdin=None, 
	stdout=None, 
	stderr=None, 
	preexec_fn=None, 
	close_fds=False, 
	shell=False, 
	cwd=None, 
	env=None, 
	universal_newlines=False, 
	startupinfo=None, 
	creationflags=0
)

args 是一個字符串(如cmd命令),或者是包含程序參數的列表。要執行的程序一般就是這個列表的第一項,或者是字符串本身。但是也可以用executable參數來明確指出。當executable參數不爲空時,args裏的第一項被認爲是“命令名”,不同於真正的可執行文件的文件名,這個“命令名”是一個用來顯示的名稱,例如執行unix/linux下的 ps 命令,顯示出來的就是這個“命令名”。

bufsize 作用就跟python函數open()buffering參數一樣:0表示不緩衝,1表示行緩衝,其他正數表示近似的緩衝區字節數,負數表示使用系統默認值。默認是0。

executable 參數指定要執行的程序。它很少會被用到,一般程序可以由args參數指定。如果shell參數爲True,executable可以用於指定用哪個shell來執行(比如bash、csh、zsh等)。windows下,只有當你要執行的命令是shell內建命令(比如dircopy等) 時,你才需要指定shell=True,而當你要執行一個基於命令行的批處理腳本(bat啥的)的時候,不需要指定此項。

stdinstdoutstderr分別表示子程序的標準輸入標準輸出標準錯誤。 可選的值有PIPE或者一個有效的文件描述符(其實是個正整數)或者一個文件對象,還有None。如果是PIPE,則表示需要創建一個新的管道,如果是 None,不會做任何重定向工作,子進程的文件描述符會繼承父進程的。另外,stderr的值還可以是STDOUT,表示子進程的標準錯誤也輸出到標準輸出。

如果把preexec_fn設置爲一個可調用的對象(比如函數),就會在子進程被執行前被調用。(僅限unix/linux)

如果把close_fds設置成True,unix/linux下會在開子進程前把除了0、1、2以外的文件描述符都先關閉。在 Windows下也不會繼承其他文件描述符。

如果把shell設置成True,指定的命令會在shell裏解釋執行,這個前面已經說得比較詳細了。

如果cwd(工作目錄)不是None,則會把cwd做爲子程序的當前目錄。注意,並不會把該目錄做爲可執行文件的搜索目錄,所以不要把程序文件所在目錄設置爲cwd。

如果env不是None,則子程序的環境變量由env的值來設置,而不是默認那樣繼承父進程的環境變量。注意,即使你只在env裏定義了某一個環境變量的值,也會阻止子程序得到其他的父進程的環境變量(也就是說,如果env裏只有1項,那麼子進程的環境變量就 只有1個了)。

如果把universal_newlines設置成True,則子進程的stdoutstderr被視爲文本對象,並且不管是unix/linux的換行符(’\n’),還是老mac格式的換行符(’\r’),還是windows 格式的換行符(’\r\n’)都將被視爲’\n’ 。

如果指定了startupinfocreationflags,它們將會被傳遞給後面的CreateProcess()函數,用於指定子程序的各種其他屬性,比如主窗口樣式或者是子進程的優先級等。(僅限Windows)


再解釋一下兩個我們後面要用到的東西:

subprocess.PIPE
一個可以用於Popen的stdinstdoutstderr參數的特殊值,它指示應打開到標準流的管道。

subprocess.STDOUT
一個可以被用於Popen的stderr參數的特殊值,表示子程序的標準錯誤與標準輸出匯合到同一句柄。


現在回到我們將要解決的問題

已知:

  • 用pyinstaller的-w參數打包導致python無法處理輸入值(stdin)
  • os.popen 打開的管道卻需要處理輸入值(stdin)

所以,我們不使用os.popen這個簡單的封裝,改成使用subprocess.popen,接着將subprocess.popen打開管道的輸入值(stdin)重定向,即可解決問題!

請看下列示例:

proc = subprocess.Popen(
    'cmd命令', 
    shell=True, 
    stdout=subprocess.PIPE, 
    stderr=subprocess.STDOUT, 
    stdin=subprocess.PIPE # 重定向輸入值
)
proc.stdin.close() # 既然沒有命令行窗口,那就關閉輸入
proc.wait()
result = proc.stdout.read() # 讀取cmd執行的輸出結果(是byte類型,需要decode)
proc.stdout.close()

這樣處理後我們用-w參數打包就不會再報錯了!


也可以將輸出值(stdout)定向到文件輸出,請看:

with open('輸出文件.txt' , 'w+', encoding='utf-8') as out_file:
    proc = subprocess.Popen(
	    'cmd命令', 
	    shell=True, 
	    stdout=out_file,  # 注意這裏!變成了文件對象!
	    stderr=subprocess.STDOUT, 
	    stdin=subprocess.PIPE
	)
	ret = proc.wait() # 此處其實有返回值
with open('輸出文件.txt', 'r', encoding='utf-8' as read_file:
    output = read_file.read() # 這樣就得到cmd命令的輸出結果了

稍微封裝一下,就可以直接拿來用了

def execute_cmd(cmd):
	proc = subprocess.Popen(
	    cmd, 
	    shell=True, 
	    stdout=subprocess.PIPE, 
	    stderr=subprocess.STDOUT, 
	    stdin=subprocess.PIPE
	)
	proc.stdin.close()
	proc.wait()
	result = proc.stdout.read().decode('gbk') # 注意你電腦cmd的輸出編碼(中文是gbk)
	proc.stdout.close()
	return result

result = execute_cmd('taskkill /f /t /im nginx.exe')
print(result)

舒服了!!!!

當然,實在要用輸入,又不想要控制檯怎麼辦?很簡單,把控制檯隱藏了就行!

下列兩個方法,試試看:

import ctypes
def hideConsole():
    """
    Hides the console window in GUI mode. Necessary for frozen application, because
    this application support both, command line processing AND GUI mode and theirfor
    cannot be run via pythonw.exe.
    """

    whnd = ctypes.windll.kernel32.GetConsoleWindow()
    if whnd != 0:
        ctypes.windll.user32.ShowWindow(whnd, 0)
        # if you wanted to close the handles...
        #ctypes.windll.kernel32.CloseHandle(whnd)

def showConsole():
    """Unhides console window"""
    whnd = ctypes.windll.kernel32.GetConsoleWindow()
    if whnd != 0:
        ctypes.windll.user32.ShowWindow(whnd, 1)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章