Linux -> Linux下的Python指令碼編程
撰寫Linux使用的
Pythonscript
這篇文章寫於兩年前,主題鎖定在以Python寫Linux的script。討論了Python script.的慣用寫法、字串處理、字元編碼、檔案與目錄處理、呼叫外部程式,以及利用內建連結庫進行網路通訊。
1 Linux、指令稿與Python
對Linux來說,指令稿(script)是至為重要的部分。
在主要的Linux distribution中間,從系統的啟動到運作,都離不開shell指令稿撰寫。在我的主機上面執行一下:
$ ls /usr/bin/* /bin/* | wc -l
2585
$ file /usr/bin/* /bin/* | grep "shell script" | wc -l
267
看,可以找到267個shell指令稿程式,超過/usr/bin和/usr目錄下所有(程式)檔案的十分之一。這還只是shell指令稿的部分而已。
在一個像Linux這樣以檔案為操作導向的作業系統上面,script.的活躍是理所當然的事情。絕大部分的系統設定都以字串的形式寫在組態檔案裡面,而作業系統的執行期資訊也存在檔案系統之中(/proc);直接處理這些字串就能管理系統,用指令稿語言來進行自動化是非常合適的。
像Python這種指令稿語言因為開發快速的關係,能夠很快地製作出我們想要的系統管理功能出來。除了開發快速之外,Python也具有容易維護的特性。相比之下,Perl程式雖然可以寫得更短,但也更不容易看懂;shell指令稿則不是完整的開發環境。Python是撰寫系統管理指令稿的理想工具。
2 Python指令稿的格式
Python指令稿與其它語言的指令稿的基本格式完全一樣,本身都是純文字檔案,而在檔案頭要以#!指定直譯程式的位置:
#!/usr/bin/python
print "Hello, world!"
這是我們上一期寫過的hello.py程式,不要忘記chmod a+x hello.py,如此便可以在指令行下執行這個指令稿:
$ ./hello.py
Hello, world
我們習慣上會給Python程式取個副檔名.py,但Linux的指令稿並不需要綴上副檔名;把hello.py改成hello,程式一樣會正常執行。
.py副檔名對Python仍有特別的意義,但只在撰寫Python模組的時候才有用處。
對於指定Python直譯器標題,我們一般有兩種作法。
像以上的hello.py這種寫為絕對路徑的方式其實並非必要,我們可以改用相對路徑的方式來指定:
#!/usr/bin/env python
於是會以/usr/bin/env程式來叫用python直譯器,處理Python程式檔案。這麼作的好處是當系統中安裝有許多個不同的Python直譯器時,會採用路徑在最前面的那一個。如此一來,若使用者另外安裝了一版Python (例如裝在自己的家目錄),又把自己的Python放到路徑設定(PATH環境變數)的最前面,即會採用使用者自己安裝的Python。
每一版Python除了有python這個執行檔之外,還會附有內容完全相同的pythonX.Y這個執行檔,X.Y是該版Python的major version和minor version。譬如Python 2.3就會有python和python2.3這兩個直譯器,用起來是完全一樣的。如果我們寫的指令稿程式必須要使用某一個版本的Python,可以偷偷在指令稿標題上動手腳來進行限制;以Python 2.3為例,就把標題寫成:
#!/usr/bin/env python2.3
Note
Python提供了一套正統的方法來檢查所使用Python及所有相關環境的資訊。在指令稿標題上動手腳雖然方便,但不是保險的正統作法;只是,若程式本身就沒多長(譬如說二三十行),的確不必浪費時間去寫一串檢查程式。
當指令稿只使用了主流版號的標準連結庫時(這是一般的狀況),通常就不必指定Python的版本。
若寫成hello.py裡那種絕對路徑的標題,就會限定使用安裝在某一個位置的Python。
通常我們都會指定在/usr/bin/python或/usr/bin/pythonX.Y (看要指定哪一版),即系統所安裝的Python;寫成這樣的話,使用者就不好改用自己安裝的版本了。
Python直譯器還會讀取另一組格式為# -*- setting -*-的標題(通常接在第一行以後),其中常用的是:
# -*- coding: UTF-8 -*-
用途是指定「指令稿檔案內純文字的字元編碼(為UTF-8)」。如果你想要寫中文批註,這就非常重要;Python自己有一套字元編碼轉換的機制,實作在codecs模組裡面,但直到Python 2.4之前,繁體中文常用的Big5編碼並未進入標準的codecs模組。如果指令稿檔案使用了Python看不懂的字元編碼(就是指華文世界用的Big5和GB),程式雖然仍可執行,但Python直譯器會送出警告。如果想用中文撰寫批註,最好把指令稿檔案轉為UTF-8 Unicode,並如上指定編碼。
上一期已經提過了,Python也是以#當作單行批註符號的(和shell script.一樣);所有在這個符號之後的文字都是批註。
順帶一提,如果你習慣以VIM編輯Python指令稿,可以在檔案尾加上VIM的設定字串:
# vim:set nu et ts=4 sw=4 cino=>4:
設定顯示行號(nu)、展開跳格鍵(et,對Python程式來說,跳格鍵Tab是最要不得的東西),指定跳格字元為4 (ts=4)、位移字元寬為4 (sw=4)、C式縮排為>4 (cino=>4);然後再開啟文法標示(syntax highlighting,這個在.vimrc裡設定比較合適)。
使用這樣的編輯環境,對撰寫Python程式來說會很方便。
Python直譯器會依出現順序來執行程式代碼檔案裡的指令。如果我們想撰寫比較具組織性的指令稿,可以把平鋪直述的:
print "some operations"
改成這樣的程式碼結構:
def main():
print "some operations"
if __name__ == '__main__':
main()
亦即自行製作一個「進入點」main()函式。當指令稿比較長(超過一百行以上),以及將來在擴充指令稿的時候,就會比較方便。
總結來說,一個Python指令稿的常見格式應為:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
def main():
print "Hello, world"
if __name__ == '__main__':
main()
# vim:set nu et ts=4 sw=4 cino=>4:
3 字串處理
在管理Linux系統時,(純文字)設定檔案以及其中的字串處理是至為核心的部分;讓我們來看看Python如何進行這些工作。
因為我們在上一期已經用Python處理過字串和檔案了,所以在這裡,我們應該對字串處理作深入一點的介紹。
首先我們要知道的是,字串在Python裡面是一種對象。開啟Python互動式環境(到shell去執行python即可進入),執行以下動作:
>>> print type( "" )
<type 'str'>
>>> if type( "I am a string" ) is str: print True
...
True
>>> if type( "Another string" ) is str(): print True
...
type()是Python的內建函式,用來取得變數的型態。我們可以從這三個指令看出來,字串"", "I am a string"都是str類別的對象。
查看Python的線上檔案,會發現有兩組關於字串處理的連結庫;一組是string模組裡的函式,另一組則是字串對象專用的方法(String Methods)。兩者雖有一些差別,但功能的重複性相當高;我們討論的重點在字串方法。
我們常常會需要分析檔案中的字串:把字串拆解開來,依照給定的邏輯來判斷字串資料的意義。
因此,最常用的字串方法就是我們上一期有用到的split()
split()傳回的是列表(list),可以用索引值(以0起始)來存取列表中的各個項
再來示範一下:
>>>tokens = "This is a sample string used to demo split()".split()
>>>len(tokens)
9
>>> print tokens
['This', 'is', 'a', 'sample', 'string', 'used', 'to', 'demo', 'split()']
>>> print tokens[0], tokens[2]
This a
>>> print tokens[-1], tokens[-2]
split() demo
>>> print tokens[2:5]
['a', 'sample', 'string']
第一個指令把我們的字串切成了9個字串,存在tokens這個列表裡。
len()是個內建函式,用來量測像列表這種可以存放其它東西的對象的長度(傳回所包含的項目個數)。
列表只要是整數就可以了,但最大不能到項目個數;可以給入負值,表示從列表尾端開始計算。索引值-1即為列表的最後一個項目。
有辦法切開字串進行判斷了之後,我們常常還需要把分析結果給輸出出來,那麼就得接合字串;以字串的格式化操作(string format operations)就能完成這件工作。我們可以寫出以下的表示式:
>>>"%d %f %s" % (1, 1.2, "string")
'1 1.200000 string'
這就是字串格式化操作。以帶有特別轉換字元(conversion character)的格式化字串,後接%運運算元,再接一個tuple作為參數,就能把tuple裡的資料填進格式化字串裡去。常用的%d代表有號整數、%f代表浮點數、%s代表字串,完整的轉換字元表請參考Python的線上檔案。
Note
Python的tuple也是一種可以包含其它對象的資料結構,以整數索引存取其中的對象,但其行為與列表不盡相同。在文法上,tuple用(1, 2, 3)來宣告,而列表用[1, 2, 3]來宣告。如果tuple中只有一個對象,則要寫成(1,),不要忘記右括弧前的逗號,在字串格式化操作時,若轉換字元只有一個,%運算元後的tuple也可以用單一變數來代替。
字串對象另有一個叫作join()的方法可以用來結合字串,用法如下:
>>> "".join([ "a", "b", "c" ])
'abc'
>>> "-".join([ "a", "b", "c" ])
'a-b-c'
在處理字串時,最後要注意的是,Python的字串不可變。也就是說,想變更字串中的某一個字元,不能直接設:
>>> a = "write"
>>> a[2] = "o"
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: object doesn't support item assignment
那是不合法的。那該怎麼辦呢?可以這樣作:
>>> print a[:2]+"o"+a[3:]
wrote
字串的內容雖然不能變更,但字串本身可以加起來(串接)。a[:2]表示取出a字串到索引2為止的部分;a[3:]表示取出a字串從索引3開始到結尾的部分;然後在中間接入"o"。最後我們還是可以得到wrote字串。這種操作索引的技巧,也可以用在一般的列表上。
Python同樣具有常規表示式(regular expression)的操作能力,實作在re模組裡面。用來執行字串取代是非常方便的。
3.1 轉換字元編碼
Python有一套處理字元編碼的codecs模組;我們以之即可自由地將字元轉換為各種不同的編碼,這是我們在處理多國語言資料時常需處理的問題。然而,字串對象本身就提供有encode()與decode()方法,我們不必匯入codecs模組就可以使用這兩個方法為我們提供的codecs能力。
此處我們得要注意一個事實,那就是Python擁有兩種字串對象。其一是我們剛剛一直在處理的str字串,而另一種呢,就是對多國語言處理非常重要的unicode字串。一般我們用引號或雙引號表示的都是普通的字串(str),而用u"string"表示的呢,就是unicode字串。decode()能把一般字元串解碼成unicode對象,而encode()則能把unicode對象編碼成各種支援的字元集。
在處理中文編碼之前,我們要為Python 2.3安裝相關的外加套件:cjkcodecs與iconvcodecs;前者是中日韓專用的codecs對象,而後者允許Python直接使用GNU iconv工具所提供的編碼,作為codecs對象。假設我們得把原本是Big5的編碼重編為UTF-8,那麼可以這樣作:
>>> f = open( "file.big5" )
>>> s = f.read()
>>> f.close()
>>> sp = s.decode('Big5').encode('UTF-8')
你可以在電腦上找一個內容是Big5編碼的檔案,把locale改成UTF-8,然後在Python互動式環境下執行以上的指令(該改的地方請改一下)。最後再用print s, sp比較一下轉換前後的字串。
4 檔案系統與目錄
在Linux系統中複製、搬移、刪除檔案與目錄也是管理時常見的動作。Python提供的os模組能處理作業系統所支援的大部分檔案系統操作,另外還有shutil模組,提供更高階的操作。
4.1 檔案系統操作
檔案系統與檔案內容是不一樣的議題。我們在進行檔案系統操作時,處理的是搬移(更名)、複製與刪除,比較沒有機會直接新增檔案。這些動作在os與shutil模組裡幾乎都有提供;我們應該先匯入這兩個模組。
若要複製檔案,我們可以這樣作:
>>> shutil.copy('data.txt', 'data.new.txt')
刪除檔案則用os.unlink():
>>> os.unlink('data.new.txt')
搬移(更名)有兩種方法:
>>> os.rename('data.txt', 'data.alter.txt')
>>> shutil.move('data.alter.txt', 'data.txt')
第一種方法,若來源檔(第一個參數)與目的檔不在同一個檔案系統內(分割區),此動作可能會失效(不同的Unix有不同的處理方法)。
第二種方法比較高階,無論來源檔與目的檔是否在相同的檔案系統內,都可以使用。
4.2 路徑的處理
管理系統的時候多半不會只處理目前的目錄內的檔案,所以常要對路徑字串進行處理。os.path模組提供了處理路徑的函式,常用的有:
- abspath():接受一個路徑字串,傳回該路徑所代表的絕對路徑。
- realpath():接受一個路徑字串,計算該路徑中包含的符號連結(symbolic link),傳回所代表的真正路徑。
- split(), dirname(), basename():
split()接受一個路徑字串,從最後一個路徑項目前切開,分成包含該項目的目錄與該項目名本身,以tuple傳回。
dirname()是split()傳回值的第一個元素;
basename()是第二個元素。
- join():接受一個路徑列表,把該列表中的每個元素接成一個完整路徑字串後傳回。
- splitext():接受一個路徑字串,分開其副檔名,將主檔名與副檔名用一個tuple傳回。
- exists():測試傳入的路徑字串是否存在,傳回布爾值。
- isfile(), isdir(), islink(), isabs():分別用來測試所傳入的路徑字串是否為檔案、目錄、符號連結或絕對路徑;傳回布爾值。
實際要使用的時候,大概會像是這樣子:
>>> os.path.split( "a/b/c" )
('a/b', 'c')
>>> os.path.join( "a", "b", "c" )
'a/b/c'
>>> os.path.splitext( "dir/file.ext" )
('dir/file', '.ext')
你可以在你的目錄結構裡,用真正的路徑來試試看!
5 外部程式呼叫
許多在shell指令稿中要靠呼叫外部程式才能完成的作業,都能用Python的內建模組來完成,例如上面提到的字串處理、檔案處理、目錄處理等等。而若遇到Python不足的地方,或是有非常特別的操作,當然也可以呼叫外部的程式。
os模組有一個system()函式可以用來呼叫外部程式:
>>> os.system( 'ls' )
weekly20051204.doc
weekly20051211.doc
0
>>>
最後顯示出來的0不是ls程式的輸出,而是其傳回值。
os.system()函式能進行最簡單的外部程式呼叫,不能對該程式的輸出入資料進一步處理;如果我們只想簡單執行程式,os.system()函式將是最佳的選擇。
5.1 管線
當我們也需要對外部程式的輸出入資料進行處理的時候,os.system()就不夠用了。
Python另外有popen2模組,可以讓我們管理外部程式子行程的輸出入管線(pipe)。
在popen2模組裡有popen2(), popen3()和popen4()三個工具函式,分別會重導向子行程的標準輸出入、標準輸出入及錯誤輸出、標準輸出合并錯誤輸出及標準輸入。
簡單用範例來說明最常用的popen2() (別忘了先import popen2喔):
>>> stdout, stdin = popen2.popen2("ls")
>>> str = stdout.read()
>>> print ostr
weekly20051204.doc
weekly20051211.doc
>>>
popen2.popen2()會傳回連結到ls程式輸出入的兩個檔案對象,我們取名為stdout與stdin。呼叫了popen2.popen2()之後,外部程式馬上就會執行,然後我們就能從stdout檔案對象裡讀出該外部程式的標準輸出資料了。如此一來,該程式的執行結果就不會直接顯示在終端機上,我們可以在Python裡面先處理過以後,再決定該怎麼辦。
如果我們想呼叫的程式也會進行錯誤輸出(stderr),而我們想要處理的話,就改用popen3()或popen4()函式。popen3()的錯誤輸出會串連至一個獨立的檔案對象,而popen4()則會把錯誤輸出一起放到標準輸出所連結的檔案對象裡;你可以視需要使用。
Note
在Python 2.4裡有一個新的subprocess模組,可以執行所有的外部程式呼叫功能。所以在Python 2.4裡不再需要os與popen2模組裡的相關函式了;當然,舊模組不會消失,所以在Python 2.4裡還是可以用popen2,我們的舊程式不會出問題。
6 網際網路通訊
Python內建的連結庫裡就具備相當方便的網際網路通訊功能,不必呼叫外部程式。
網際網路通訊是個大範圍,其中最常用到的大概數全球資訊網了;我們舉Zope應用程式服務器來作例子。Zope使用ZODB對象資料庫來儲存資料,這個系統會把存取動作紀錄下來,當使用者刪除其中的資料時,資料不會實際刪除,要等到手動壓縮(pack)資料庫的時候,才會真正把資料刪除。這個壓縮功能的動作選項是放在web-based的ZMI裡面,沒有指令行介面;如果我們不想手動連進ZMI來執行壓縮,就得寫一個能進行HTTP操作的指令稿。
我們要寫的程式應該具有以下的命令列介面:
packzope.py -u<URL of Zope server> -d<day>-U<username> -P<password>
這個packzope.py程式要負責用HTTP和伺服器溝通,把從命令列取得的使用者名稱和密碼提供給Zope伺服器,並且用GET方法把要壓縮的天數(捨棄指定天數前的資料)告訴Zope伺服器。以下是寫好的程式:
#!/usr/bin/env python
import sys
import urllib
class parameters:
def __init__(self):
from optparse import OptionParser, OptionGroup
op = OptionParser(
usage = "usage: %prog -u URL -d DAYS -U USERNAME -P PASSWORD",
version = "%prog, " + "%s" % __revision__
)
op.add_option("-u", action="store", type="string", /
dest="url", /
help="URL of site to open"
)
op.add_option("-d", action="store", type="int", /
dest="days", default=1, /
help="erase days before"
)
op.add_option("-U", action="store", type="string", /
dest="username", /
help="username"
)
op.add_option("-P", action="store", type="string", /
dest="password", /
help="password"
)
self.op = op
(self.options, self.args) = self.op.parse_args()
params = parameters()
if not params.options.url or /
not params.options.username or /
not params.options.password :
params.op.print_help()
sys.exit(1)
url = "%s/Control_Panel/Database/manage_pack?days:float=%d" % /
(params.options.url, params.options.days)
def main(): try: f = MyOpener().open(url).read() print "Successfully packed ZODB on host %s" % params.options.url except: print "Cannot open URL %s, aborted" % url raiseif __name__ == '__main__': main()
程式前半段在處理命令列參數 (classparameters),而在 main()函式裡實際進行連線動作。packzope.py利用
urllib模組來連結 Zope 伺服器,並利用 subclassing urllib.FancyURLopener類別來自訂使用者名稱與密碼的輸入。壓縮完畢之後,程式會輸出以下的字樣:
Successfully packed ZODB on host http://someplace:port
我們可以把 packzope.py放到 crontab 裡定期執行。這就是一種自動化網路操作。
7 結語
本文藉由討論以 Python 進行 Linux 操作自動化的技巧,對 Python 的應用作了進一步的介紹。當然,在進行任何種類的 Python 程式開發時,都可以參考 Python 的線上說明檔案。Dive into Python 是一本容易上手的自由 Python 書籍,你也可以在網路上找到中文譯本。