對於Linux使用者來說,命令列的名聲相當的高。不像其他動作系統,命令列是一個可怕的命題,但是對於Linux社區中那些經驗豐富的大牛,命令列卻是最值得推薦鼓勵使用的。通常,命令列對比圖形化使用者介面,更能提供更優雅和更高效的解決方案。
命令列伴隨著Linux社區的成長,UNIX shells,例如 bash和zsh,已經成長為一個強大的工具,也是UNIX shell的重要組成部分。使用bash和其他類似的shells,可以得到一些很有用的功能,例如,管道,檔案名稱萬用字元和從檔案中讀取命令,也就是指令碼。
讓我們在實際操作中來介紹命令列的強大功能吧。每當使用者登陸某服務後,他們的使用者名稱都被記錄到一個文字檔。例如,我們來看看有多少獨立使用者曾經使用過該服務。
以下一系列的命令展現了由一個個小的命令串接起來後所實現的強大功能:
$ cat names.log | sort | uniq | wc -l
管道符號(|)把一個命令的標準輸出傳送給另外一個命令的標準輸入。在這個例子中,把cat names.log的輸出傳送給sort命令的輸入。sort命令是把每一行按字母順序重新排序。接下來,管道把輸出傳送至uniq命令,它可以重複資料刪除名字。最後,uniq的輸出又傳送給wc命令。wc是一個字元計數命令,使用-l參數,可以返回行的數量。管道可以讓你把一系列的命令串接在一起。
但是,有時候需求會很複雜,串接命令會變得十分笨重。在這個情況下,shell指令碼可以解決這個問題。shell指令碼就是一系列的命令,被shell程式所讀取,並按順序執行。Shell指令碼同樣支援一些程式設計語言的特性,例如變數,流程式控制制和資料結構。shell腳步對於經常重複啟動並執行批次程式非常有用。但是,shell指令碼也有一些弱點:
- shell指令碼很容易變為複雜的代碼,導致開發人員難於閱讀和修改它們。
- 通常,它的文法和解釋都不是那麼靈活,而且不直觀。
- 它代碼通常不能被其他指令碼使用。指令碼中的代碼重用率很低,並且指令碼通常是解決一些很具體的問題。
- 它們一般不支援庫特性,例如HTML解譯器或者處理HTTP請求庫,因為庫一般都只出現在流行的語言和指令碼語言中。
這些問題通常會導致指令碼變得不靈活,並且浪費開發人員大量的時間。而Python語言作為它的替代品,是相當不錯的選擇。使用python作為shell指令碼的替代,通常有很多優勢:
- python在主流的linux發行版本中都被預設安裝。開啟命令列,輸入python就可以立刻進入python的世界。這個特性,讓它可以成為大多指令碼任務的最好選擇。
- python非常容易閱讀,文法容易理解。它的風格注重編寫簡約和乾淨的代碼,允許開發人員編寫適合shell指令碼的風格代碼。
- python是一個解釋性語言,這意味著,不需要編譯。這讓python成為最理想的指令碼語言。python同時還是讀取,演繹,輸出的迴圈風格,這允許開發人員可以快速的通過解譯器嘗試新的代碼。開發人員無需重新編寫整個程式,就可以實現自己的一些想法。
- python是一個功能齊全的程式設計語言。代碼重用非常簡單,因為python模組可以在指令碼中方便的匯入和使用。指令碼可以輕易的擴充。
- python可以訪問優秀的標準庫,還有大量的實現多種功能的第三方庫。例如解譯器和請求庫。例如,python的標準庫包含時間庫,允許我們把時間轉換為我們想要的各種格式,而且可以和其他日期做比較。
- python可以是命令鏈中的一部分。python不能完全代替bash。python程式可以像UNIX風格那樣(從標準輸入讀取,從標準輸出中輸出),所以python程式可以實現一些shell命令,例如cat和sort。
讓我們基於文章前面提到問題,重新使用python構建。除了已完成的工作,還讓我們來看看某個使用者登陸系統到底有多少次。uniq命令只是簡單的重複資料刪除記錄,而沒有提示到底這些重複記錄重複了多少次。我們使用python指令碼替代uniq命令,而且指令碼可以作為命令鏈中的一部分。以下是python程式實現這個功能(在這個例子中,指令碼叫做namescount.py):
#!/usr/bin/env pythonimport sys if __name__ == "__main__": # 初始化一個names的字典,內容為空白 # 字典中為name和出現數量的索引值對 names = {} # sys.stdin是一個檔案對象。 所有引用於file對象的方法, # 都可以應用於sys.stdin. for name in sys.stdin.readlines(): # 每一行都有一個newline字元做結尾 # 我們需要刪除它 name = name.strip() if name in names: names[name] += 1 else: names[name] = 1 # 迭代字典, # 輸出名字,空格,接著是該名字出現的數量 for name, count in names.iteritems(): sys.stdout.write("%d\t%s\n" % (count, name))
讓我們來看看python指令碼如何在命令鏈中起作用的。首先,它從標準輸入sys.stdin對象讀取資料。所有的輸出都寫到sys.stdout對象裡面,這個對象是python裡面的標準輸出的實現。然後使用python字典(在其他語言中,叫做雜湊表)來儲存名字和重複次數的映射。要讀取所有使用者的登陸次數,只需執行下面的命令:
$ cat names.log | python namescount.py
這裡會輸出某使用者出現的次數還有他的名字,使用tab作為分隔字元。接下來的事情就是,以使用者登陸次數的降序順序輸出。這可以在python中實現,但是讓我們使用UNIX的命令來實現吧。前面已經提到,使用sort命令可以按字母順序排序。如果sort命令接收一個-rn參數,那麼它就會按照數位降序方式做排序。因為python指令碼輸出到標準輸出,所以我們可以使用管道連結sort命令,擷取該輸出:
$ cat names.log | python namescount.py | sort -rn
這個例子使用了python作為命令鏈中的一部分。使用python的優勢是:
- 可以跟例如cat和sort這樣的命令連結在一起。簡單的工具(讀取檔案,給檔案按數字排序),可以使用成熟的UNIX命令。這些命令都是一行一行的讀取,這意味著這些命令可以相容大容量的檔案,而且它們的效率很高。
- 如果命令鏈條中某部分很難實現,很清晰,我們可以使用python指令碼,這可以讓我們做我們想做的,然後減輕鏈條一下個命令的負擔。
- python是一個可重用的模組,雖然這個例子是指定了names,如果你需要處理重複行的其他輸入,你可以輸出每一行,還有該行的重複次數。讓python指令碼模組化,這樣你就可以把它應用到其他地方。
為了示範python指令碼中結合模組和管道風格的強大力量,讓我們擴充一下這個問題。讓我們來找出使用服務最多的前5位使用者。head命令可以讓我們指定需要輸出的行數。在命令鏈中加入這個命令:
$ cat names.log | python namescount.py | sort -rn | head -n 5
這個命令只會列出前5位使用者。類似的,擷取使用該服務最少的5位使用者,你可以使用tail命令,這個命令使用同樣的參數。python命令的結果輸出到標準輸出,這樣可以允許你擴充和構建它的功能。
為了示範指令碼的模組化特性,我們又來擴充一下問題。該服務同樣產生一個以逗號分割的csv的記錄檔,其中包含,一個email地址清單,還有該地址對我們服務的評價。如下是其中一個例子:
"email@example.com", "This service is great."
這個任務是,提供一個途徑,來發送一個感謝函息給使用該服務最多的前10位使用者。首先,我們需要一個指令碼讀取csv和輸出其中某一個欄位。python提供一個標準的csv讀模數塊。以下的python指令碼實現了這個功能:
#!/usr/bin/env python# CSV module that comes with the Python standard libraryimport csvimport sys if __name__ == "__main__": # CSV模組使用一個reader對象作為輸入 # 在這個例子中,就是 sys.stdin. csvfile = csv.reader(sys.stdin) # 這個指令碼必須接收一個參數,指定列的序號 # 使用sys.argv擷取參數. column_number = 0 if len(sys.argv) > 1: column_number = int(sys.argv[1]) # CSV檔案的每一行都是用逗號作為欄位的分隔字元 for row in csvfile: print row[column_number]
這個指令碼可以把csv轉換並返回參數指定的欄位的文本。它使用print代替sys.stout.write,因為print預設使用標準輸出最為它的輸出檔案。
讓我們把這個腳步添加到命令鏈中。新的指令碼跟其他命令組合在一起,實現輸出評論最多的email地址。(假設.csv 檔案名稱為emailcomments.csv,新的指令碼為csvcolumn.py)
接下來,你需要一個發送郵件的方法,在Python 函數標準庫中,你可以匯入smtplib 庫,這是一個用來串連SMTP伺服器並發送郵件的模組。讓我們寫一個簡單的Python指令碼,使用這個模組發送一個郵件給每個top 10 的使用者。
#!/usr/bin/env pythonimport smtplibimport sys GMAIL_SMTP_SERVER = "smtp.gmail.com"GMAIL_SMTP_PORT = 587 GMAIL_EMAIL = "Your Gmail Email Goes Here"GMAIL_PASSWORD = "Your Gmail Password Goes Here" def initialize_smtp_server(): ''' This function initializes and greets the smtp server. It logs in using the provided credentials and returns the smtp server object as a result. ''' smtpserver = smtplib.SMTP(GMAIL_SMTP_SERVER, GMAIL_SMTP_PORT) smtpserver.ehlo() smtpserver.starttls() smtpserver.ehlo() smtpserver.login(GMAIL_EMAIL, GMAIL_PASSWORD) return smtpserver def send_thank_you_mail(email): to_email = email from_email = GMAIL_EMAIL subj = "Thanks for being an active commenter" # The header consists of the To and From and Subject lines # separated using a newline character header = "To:%s\nFrom:%s\nSubject:%s \n" % (to_email, from_email, subj) # Hard-coded templates are not best practice. msg_body = """ Hi %s, Thank you very much for your repeated comments on our service. The interaction is much appreciated. Thank You.""" % email content = header + "\n" + msg_body smtpserver = initialize_smtp_server() smtpserver.sendmail(from_email, to_email, content) smtpserver.close() if __name__ == "__main__": # for every line of input. for email in sys.stdin.readlines(): send_thank_you_mail(email)
這個python指令碼能夠串連任何的SMTP伺服器,不管是在本地還是遠程。為便於使用,我使用了Gmail的SMTP伺服器,正常情況下,應該提供你串連Gmail的密碼口令,這個指令碼使用了smtp庫中的函數發送郵件。再一次證明使用Python指令碼的強大之處,類似SMTP這樣的互動操作使用python來寫的話是比較簡單易讀的。相同的shell指令碼的話,可能是比較複雜並且像SMTP這樣的庫是基本沒有的。
為了寄送電子郵件給評論頻率最高的前十名使用者,首先必須單獨得到電子郵件列的內容。要取出某一列,在Linux中你可以使用cut命令。在下面的例子中,命令是在兩個單獨的串。為了便於使用,我寫輸出到一個臨時檔案,其中可以載入到第二串命令中。這隻是讓過程更具可讀性(Python發送郵件指令碼簡稱為sendemail.py):
$ cat emailcomments.csv | python csvcolumn.py | ?python namescount.py | sort -rn > /tmp/comment_freq$ cat /tmp/comment_freq | head -n 10 | cut -f2 | ?python sendemail.py
這表明Python作為一種工具 + 生產力如bash命令鏈的真正威力。編寫的指令碼從標準輸入接受 資料並且將任何輸出寫入到標準輸出,允許開發人員串起這些命令, 鏈中的這些快速,簡單的命令以及Python程式。這種只為一個目的設計小程式的哲學非常適用於這裡所使用的命令流方式。
通常在命令列中使用的Python指令碼,當他們運行某個命令時,參數由使用者來選擇。例如,head命令取得一個-n的參數標誌和它後面的數字,然後只列印這個數字大小的行數。Python指令碼的每一個參數都是通過sys.argv數組提供,可在import sys後來訪問。下面的代碼顯示了如何使用單個詞語作為參數。此程式是一個簡單的加法器,它有兩個數字參數,將它們相加,並列印輸出給使用者。然而,這種命令列參數使用方式是非常基礎的。這也是很容易出錯誤的 ——例如,輸入兩個字串,如hello和world,這個命令,你會一開始就得到錯誤:
#!/usr/bin/env pythonimport sys if __name__ == "__main__": # The first argument of sys.argv is always the filename, # meaning that the length of system arguments will be # more than one, when command-line arguments exist. if len(sys.argv) > 2: num1 = long(sys.argv[1]) num2 = long(sys.argv[2]) else: print "This command takes two arguments and adds them" print "Less than two arguments given." sys.exit(1) print "%s" % str(num1 + num2)
慶幸的是,Python有很多處理有關命令列參數的模組。我個人比較喜歡OptionParser。OptionParser是標準庫提供的optparse模組的一部分。OptionParser允許你對命令列參數做一系列非常有用的操作。
- 如果沒有提供具體的參數,可以指定預設的參數
- 它支援參數標誌(顯示或不顯示)和參數值(-n 10000)。
- 它支援傳遞參數的不同格式——例如,有差別的-n=100000和-n 100000。
我們來用OptionParser來改進sending-mail指令碼。原來的指令碼有很多的變數硬式編碼地方,比如SMTP細節和使用者的登入憑據。在下面提供的代碼,在這些變數是用來傳遞命令列參數:
#!/usr/bin/env pythonimport smtplibimport sys from optparse import OptionParser def initialize_smtp_server(smtpserver, smtpport, email, pwd): ''' This function initializes and greets the SMTP server. It logs in using the provided credentials and returns the SMTP server object as a result. ''' smtpserver = smtplib.SMTP(smtpserver, smtpport) smtpserver.ehlo() smtpserver.starttls() smtpserver.ehlo() smtpserver.login(email, pwd) return smtpserver def send_thank_you_mail(email, smtpserver): to_email = email from_email = GMAIL_EMAIL subj = "Thanks for being an active commenter" # The header consists of the To and From and Subject lines # separated using a newline character. header = "To:%s\nFrom:%s\nSubject:%s \n" % (to_email, from_email, subj) # Hard-coded templates are not best practice. msg_body = """ Hi %s, Thank you very much for your repeated comments on our service. The interaction is much appreciated. Thank You.""" % email content = header + "\n" + msg_body smtpserver.sendmail(from_email, to_email, content) if __name__ == "__main__": usage = "usage: %prog [options]" parser = OptionParser(usage=usage) parser.add_option("--email", dest="email", help="email to login to smtp server") parser.add_option("--pwd", dest="pwd", help="password to login to smtp server") parser.add_option("--smtp-server", dest="smtpserver", help="smtp server url", default="smtp.gmail.com") parser.add_option("--smtp-port", dest="smtpserverport", help="smtp server port", default=587) options, args = parser.parse_args() if not (options.email or options.pwd): parser.error("Must provide both an email and a password") smtpserver = initialize_smtp_server(options.stmpserver, options.smtpserverport, options.email, options.pwd) # for every line of input. for email in sys.stdin.readlines(): send_thank_you_mail(email, smtpserver) smtpserver.close()
這個指令碼顯示OptionParser 的作用。它提供了一個簡單、便於使用的介面給命令列參數, 允許你為每個命令列選項定義某些屬性。它還允許你指定預設值。如果沒有給出某些參數,它可以給你報出特定錯誤。
現在你學到了多少?並不是使用一個python指令碼替代所有的bash命令,我們更推薦讓python完成其中某些困難的任務。這需要更多的模組化和重用的指令碼,還要好好利用python的強大功能。
使用stdin作為檔案對象,這可以允許python讀取輸入,這個輸入是由管道傳輸其他命令的輸出給它的,而把輸出輸出到stout,可以允許python把資訊傳遞到管道系統的下一環節。結合這些功能,可以實現強大的程式。在這裡提到的例子,就是要實現一個處理服務的記錄檔。
在實際應用中,我最近在處理一個GB層級的CSV檔案,我需要使用python指令碼轉換一個包含插入資料的SQL命令。瞭解我需要處理的檔案,並在一個表中處理這些資料,指令碼需要23個小時來執行並產生20GB的SQL檔案。使用文章提到的python編程風格的優勢在於,我們不需要把這個檔案讀取到記憶體中。這意味著整個20GB+的檔案可以一行一行的處理。而且我們更清晰的分解每一個步驟(讀取,排序,維護和輸出)為一些邏輯步驟。還有我們得到這些命令的保障,其中這些命令都是UNIX類型的環境的核心工具,它們十分高效和穩定,可以協助我們構建穩定安全的程式。
另外一個優點在於,我們不需要寫入程式碼檔案名稱。這樣可以使得程式更靈活,只需傳遞一個參數。例如,如果指令碼在某個檔案在20000中斷了,我們不需要重新運行指令碼,我們可以使用tail來指定失敗的行數,來讓指令碼在這個位置繼續運行。
python在shell中的應用範圍很廣,不局限於本文所述,例如os模組和subprocess模組。os模組是一個標準庫,可以執行很多作業系統層級的操作,例如列出目錄的結構,檔案的統計資訊,還有一個優秀的os.path子模組,可以處理規範目錄路徑。subprocess模組允許python程式運行系統命令和其他進階命令,例如,上文提到的使用python代碼和spawned進程之間的管道處理。如果你需要編寫python的shell指令碼,這些庫都值得去研究的。