Python
之旅 (五)文
/馬兒 (marr@linux.org.tw)本期將介紹幾個
Unix/Linux 上常見的延伸應用,諸如 grep、find、regular expression 等,這些模組的實作方式,以 Mandrake Linux 7.2 為例,讀者都可以在 /usr/lib/python1.5/ 目錄下找到原始碼。另外,我們也將接觸 Python 物件導向語言裡的類別與個體物件,透過一些簡單的範例,慢慢走入物件導向程式的核心世界。與字串、檔案名稱有關的模組
grep
模組Python 的 grep 模組裡,同樣包含 grep()、egrep()、emgrep()、ggrep() 等不同的 grep 物件方法,為了簡便學習,讀者先熟悉 grep() 即可。
>>> import grep
>>> grep.grep("lib", "/etc/passwd")
16: gopher:x:13:30:gopher:/usr/lib/gopher-data:
20: htdig:x:101:104::/var/lib/htdig:
23: postgres:x:40:235:PostgreSQL Server:/var/lib/pgsql:/bin/bash
>>> grep.grep("bin", "/etc/passwd", "/etc/group")
/etc/passwd: 1: root:x:0:0:root:/root:/bin/bash
/etc/passwd: 2: bin:x:1:1:bin:/bin:
/etc/passwd: 3: daemon:x:2:2:daemon:/sbin:
/etc/passwd: 6: sync:x:5:0:sync:/sbin:/bin/sync
/etc/passwd: 7: shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
/etc/passwd: 8: halt:x:7:0:halt:/sbin:/sbin/halt
/etc/passwd: 23: postgres:x:40:235:PostgreSQL Server:/var/lib/pgsql:/bin/bash
/etc/group: 2: bin:x:1:root,bin,daemon
/etc/group: 3: daemon:x:2:root,bin,daemon
/etc/group: 4: sys:x:3:root,bin,adm
>>>
使用時請載入 grep 模組。
由上述範例,可以得知模組 grep 裡的 grep() 物件方法,其工作方式和 Unix/Linux 上的grep 指令一致,將一個字串當作搜尋值,再接所要進行搜尋的檔案名稱。其傳回值會包括所找到的行內容,並在行內容前標明其所在行數。
值得注意的是,所接檔案名稱可以超過一個以上,如上例以檔案名稱字串接續輸入,其不同檔案內所搜尋到的內容,會在輸出結果前,標明其檔案名稱以作區別。glob
模組如果我們想要使用類似
"*.txt" 這樣的檔案名稱描述方式,可以藉助於 glob 模組的服務,這在之前的檔案系統服務說明部份,已是簡單介紹過,在此我們再溫故知新一番。若忘了先 import glob 模組的話,很可能會回報錯誤訊息,請參考下列範例。>>> import grep
>>> grep.grep("John", "*.txt")
Traceback (innermost last):
File "<stdin>", line 1, in ?
File "/usr/lib/python1.5/grep.py", line 12, in grep
return ggrep(RE_SYNTAX_GREP, pat, files)
File "/usr/lib/python1.5/grep.py", line 31, in ggrep
fp = open(filename, 'r')
IOError: [Errno 2] No such file or directory: '*.txt'
>>> import glob
>>> grep.grep("John", glob.glob("*.txt"))
beatles.txt: 1: John Lennon
smiths.txt: 2: Johnny Marr
>>> glob.glob('[0-9].*')
['1.gif', '2.txt']
>>> glob.glob('*.gif')
['1.gif', 'card.gif']
>>> glob.glob('?.gif')
['1.gif']
>>>
不過,要分清楚的是,
glob 模組用於檔案名稱的解析,其解析規則通常與 Unix/Linux shell 的檔名解析規則相同,因為 glob 模組的實作方式,使用了 fnmatch 模組。而正規表示式則是使用於字串的解析,通常與檔名解析的規則不完全相同。find
模組Python 裡的 find 模組,其所提供的功能並不如 Unix/Linux 環境下的 find 程式一般豐富,而且實作方式有其特別之處。
>>> import find
>>> find.find("*")
['./.dot_file.txt', './1.gif', './2.txt', './beatles.txt', './card.gif', './smiths.txt']
>>> find.find("*.txt")
['./.dot_file.txt', './2.txt', './beatles.txt', './smiths.txt']
>>> find.find("*.txt", "..")
['../bob_dylan.txt', '../british/.dot_file.txt', '../british/2.txt', '../british/beatles.txt', '../british/smiths.txt']
>>>
find() 物件方法最簡單的範例,便是以 "*" 為輸入參數,它會傳回現行目錄下的所有檔案名稱。值得注意的是,包括以「點符號」(.) 為首的檔案名稱,也會被符合比對。因為其實作方法,是將所有的檔案名稱最前面加上 ./ (代表 os.curdir),然後再進行比對。
find() 物件方法可以接第二個參數,指定開始搜尋的起始目錄,如例中的 "..",代表上一層目錄。正規表示式
(regular expression)如果讀者之前已有
Unix 的使用經驗,很可能已經透過 shell 或 Perl 的學習過程,認識到正規表示式的功能。正規表示式的內容相當龐雜,如果按部就班、從頭學習至尾,將近是一本書的份量,在此並不打算將正規表示式的內容悉數介紹,重點在於讓讀者了解,Python 一般的正規表示式功能,和 Perl 相較並無遜色之處。正規表示式非常實用,系統管理員、資料庫管理員、與網頁設計員,相信於此感受更深。其主要應用場合包括「搜尋」「取代」「解析」
(複雜之) 文字字串,值得說明的是,之前已介紹過的 string 模組,該模組中也包括字串搜尋 (index、find、count)、取代 (replace)、解析 (split) 等功能,但大抵僅限於基本而簡單的部份,諸如單一字串、明確字串、字母大小區分的場合,讀者必須適當分辨應用場合,以便採用最佳的解決方案。>>> import string
>>> s = '100 NORTH MAIN ROAD'
>>> string.replace(s, 'ROAD', 'RD.')
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> string.replace(s, 'ROAD', 'RD.')
'100 NORTH BRD. RD.'
>>> s[:-4] + string.replace(s[-4:], 'ROAD', 'RD.')
'100 NORTH BROAD RD.'
>>> import re
>>> re.sub('ROAD$', 'RD.', s)
'100 NORTH BROAD RD.'
>>>
先試試 string 模組的功能,將其 import 來使用。
string 模組裡的 replace() 物件方法,可以接受三個參數,分別是等待處理的字串 s,以及取代前的字串 'ROAD' 和取代後的字串 'RD.',目的是讓 'ROAD' 以縮寫字 'RD.' 來取代。
之前的範例很順利地達到需求,但此例可就把事情搞砸了,照本宣科的話,會把原本不該取代的 BROAD 也換掉。
直覺的變通方法,可以將字串 s 先分成兩段處理,由於 'ROAD' 是 4 個字母長度,而且是位於字串最後面,利用字串分割,將倒數 4 個字母前的字串保留,而最後 4 個字母的字串進行取代動作。
上述的變通方法顯然缺乏彈性,比如說,若是遇到 'STREET' 要改成縮寫字 'ST.' 時,字串分割的字母數必須改為 6,很容易會讓人疲於奔命。比較理想的方式,便是利用正規表示式的功能,在此我們載入 re 模組。
re 模組裡有個 sub() 物件方法,其接受三個參數,分別是「想要取代的字串樣版」,在此例中為 'ROAD$',而第二個參數是「取代後的字串」'RD.',最後的參數是字串 s。字串樣版 'ROAD$' 表示 'ROAD' 將出現在待搜尋字串的結尾處,此時 'BROAD' 因為不在字串結尾處,所以不會被取代,符合我們的需求。Python 裡的正規表示式功能由 re 模組提供,其伴隨幾個有用的模組方法,配合字串樣版來進行搜尋、取代、解析等動作。字串樣版 (pattern) 是指一組包含有一般文字以及特殊字元序列的字串,例如 '?(P<int>\d+)\.(\d*)' 就是一個複雜點的字串樣版,由於字串樣版裡經常會包含有星號 (*) 或反斜線 (\) 等特殊符號,因此慣例上,會配合原始字串 (raw string) 表示法來處理它,這點我們在之前也提示過。
常見的正規表示式功能介紹
下列的特殊字元最簡單且常用,可以先單獨予以記憶,並相互對照其使用意義。
特殊字元 |
功能說明 |
. |
代表除了換行字元以外的所有字元。 |
^ |
代表字串位於開頭。 |
$ |
代表字串位於結尾。 |
* |
代表一組出現一次或多次以上的表示模式。 |
+ |
代表一組出現零次或多次以上的表示模式。 |
? |
代表一組出現零次或一次的表示模式。 |
下列再整理另一組字串樣版表示法範例,易學易懂。
字串樣版範例 |
功能說明 |
hello|Hello |
代表 hello或Hello兩個字串均符合條件。 |
(h|H)ello |
代表 hello或Hello兩個字串均符合條件。 |
[hH]ello |
代表 hello或Hello兩個字串均符合條件。 |
[0-9] |
代表 0至9的數字均符合條件。 |
[^0-9] |
代表 0至9數字之外的字元符合條件。 |
另外,用於字串樣版中的特殊字元可以在前面加上反斜線,用以代表特殊字元本身,但有些反斜線的特別應用,值得額外注意,將常見者整理如下:
字元 |
功能說明 |
\ number |
代表除了換行字元以外的所有字元。 |
\ d |
代表字串位於開頭。 |
\ D |
代表字串位於結尾。 |
\ s |
代表空格字元,即 r'[ \t\n\r\f\v]'。 |
\ S |
代表非空格字元,即 r'[^ \t\n\r\f\v]'。 |
\ w |
代表英數字,即 [0-9a-zA-Z]。 |
\ W |
代表\ w定義以外的所有字元。 |
example$ cat dirbook.txt
Beatles, Liverpool
Lennon, John Winston: 0800-123456
McCartney, James Paul: 0204-123999
Harrison, George: 0800-999333
Starkey, Richard: 0204-123777
Starr, Ringo: 0204-456777
Smiths, Manchester
Morrissey, Steven Patrick: 0928-987654
Marr, Johnny: 0928-849952
Rourke, Andy: 0938111999
Joyce, Mike: 0936555444
上述是一個記錄姓名、電話的檔案,我們藉其內容先來練習
re 模組所附簡單的物件方法,例如搜尋及取代。>>> import re
>>> s = "Johnny Marr"
>>> re.sub("Marr", "Maher", s)
'Johnny Maher'
>>> t = open("dirbook.txt").read()
>>> pat = re.compile(r'John.*:')
>>> re.findall(pat, t)
['John Winston:', 'Johnny:']>>>
根據上述
dirbook.txt 檔案的內容,我們再繼續來測試正規表示式的其他功能。其內容格式大致為:Last_Name, First_Name Middle_Name: Phone_Number
觀察
Last_Name 的組成,只有英文字母而無數字,可用 [a-zA-Z] 來表示,而完整的 Last_Name 包括一個以上的英文字母,所以用 [a-zA-Z]+ 來表示。Last_Name 後緊跟著一個逗號 (,)。First_Name 部份原則上如法泡製,但 Middle_Name 算是可有可無,因此可用 [a-zA-Z]+( [a-zA-Z]+)? 來表示。之後再緊跟著一個冒號 (:)。
Phone_Number 部份,本範例所呈現者並不算複雜,可分成 4 個數字 (數字可用 \d 表示),緊跟一個可有可無的 - 符號,再接 6 個數字,可以用 \d\d\d\d-?\d\d\d\d\d\d 來表示。
檔案中,除了兩行 "Beatles, Liverpool" 與 "Smiths, Manchester" 之外,都符合上述的表示式分析。
import re
regexp = re.compile(r"[a-zA-Z]+,"
r" [a-zA-Z]+"
r"( [a-zA-Z]+)?"
r": \d\d\d\d-?\d\d\d\d\d\d")
file = open("dirbook.txt", 'r')
for line in file.readlines():
if regexp.search(line):
print "found"
file.close()
執行上述的程式內容,可以得到
9 個 "found" 字串回應,表示從記錄檔的內容中,成功解析出 9 個符合正規表示式的字串。實務上,透過正規表示式所解析出來的資料,都會希望額外做些加工,再重新以使用者想要的格式來呈現。有個
?P<name> 的應用技巧,可以幫助我們達到這樣的功能,請仔細觀察下列的範例:(?P<last>[a-z A-Z]+), (?P<first>[a-zA-Z]+)( (?P<middle>([a-zA-Z]+)))?: (?P<phone> (\d\d\d\d-?\d\d\d\d\d\d)
也就是說,每一組具備意義的正規表示式,我們可以個別賦予其一個「別名」,如
<last>、<first>、<middle>、<phone> 等。值得注意的是,?P<name> 表示法中的問號 (?),與代表「可有可無」的特殊字元 ? 符號,兩者各自獨立運作,並無相關,請不要混淆。認識了上述的「別名」設定技巧,我們就可以再透過
group() 物件方法來取得符合的字串資料。可參考下面的例子:example$ cat re_group.py
import re
regexp = re.compile(r"(?P<last>[a-zA-Z]+),"
r" (?P<first>[a-zA-Z]+)"
r"( (?P<middle>([a-zA-Z]+)))?"
r": (?P<phone>\d\d\d\d-?\d\d\d\d\d\d)"
)
file = open("dirbook.txt", 'r')
for line in file.readlines():
result = regexp.search(line)
if result == None:
print "Not found."
else:
last_name = result.group('last')
first_name = result.group('first')
middle_name = result.group('middle')
if middle_name == None:
middle_name = ""
phone_no = result.group('phone')
print 'Name: ' + first_name + ' ' \
+ middle_name + ' ' \
+ last_name + '\n' \
+ 'Phone: ' + phone_no
file.close()
執行上述程式,可以得到重新整理過的資料內容。
example$ python re_group.py
Not found.
Name: John Winston Lennon
Phone: 0800-123456
Name: James Paul McCartney
Phone: 0204-123999
Name: George Harrison
Phone: 0800-999333
Name: Richard Starkey
Phone: 0204-123777
Name: Ringo Starr
Phone: 0204-456777
Not found.
Name: Steven Patrick Morrissey
Phone: 0928-987654
Name: Johnny Marr
Phone: 0928-849952
Name: Andy Rourke
Phone: 0938111999
Name: Mike Joyce
Phone: 093655544
類別功能
(class)和
Java 語言一樣,Python 是一個物件導向式程式語言,其類別功能及類別體系,是物件導向式語言的重要成份。不過,在此我們很難逐步詳述物件導向語言的細節內容,以下的說明或範例,重點不在於概念或基礎名詞的介紹,而是帶出 Python 的語法結構。如何定義一個類別
Python 使用 class 敘述來定義一個類別,而由呼叫某一類別名稱 (類似函式的呼叫方式) 而建立的新物件,被稱為該類別的一個「個體」(instance),例如:
class MyClass:
body
instance = MyClass()
慣例上,類別在定義時會以大寫字母來表示。而一個
instance 可以內含「資料結構」或「資料記錄」,感覺上與 C 語言的結構語法很像,但實際用法上不必像 C 語言必須事先將結構完成宣告。我們可以直接觀察下列的簡單範例:class Circle:
pass
myCircle = Circle()
myCircle.radius = 5
print 2 * 3.14 * myCircle.radius
定義一個 Circle 類別,其內容「空空如也」。
建立一個新個體,名為 myCircle。
設定 myCircle 的 radius 值為 5。其設定方式,是在個體物件名稱後加上 . 符號,成為 myCircle.radius 這樣的描述方式。個體物件的設定值,可由
__init__ 這個起始函式自動完成,通常 __init__ 又被稱為類別的「建構子」(constructor)。每次有新的個體物件被建立,都會找尋 __init__ 的設定內容,自動完成新個體物件的起始設定值。class Circle:
def __init__(self):
self.radius = 1
def area(self):
return self.radius * self.radius * 3.14159
myCircle = Circle()
print 2 * 3.14 * myCircle.radius
myCircle.radius = 5
print 2 * 3.14 * myCircle.radius
print myCircle.area()
類別 Circle 裡有個 __init__() 函式,設定其 radius 值為 1。注意到 self 這個參數,慣例上它就是 __init__() 函式的第一個參數,當 __init__ 起始設定時,self 就會被設定為新建立的個體上。
新個體物件建立後,有自己的個體變數 (instance variables),如 radius 即為一例。
個體變數的值可以重新設定。
可以為個體物件設定物件方法 (method),其方式就像定義函式一般。class Circle:
def __init__(self, r=1):
self.radius = r
def area(self):
return self.radius * self.radius * 3.14159
c = Circle(5)
print c.area()
上述則是另一個改進版本的
Circle 類別內容設定,增加預設變數 r,並指定其預設值為 1。如此一來,我們便可透過 Circle(5) 這樣的方式來指定新個體的建立。繼承功能
(inheritance)繼承功能是物件導向式語言的另一項特性,
Python 的繼承功能可由下列範例中觀察了解:class Square:
def __init__(self, side=1, x=0, y=0):
self.side = side
self.x = x
self.y = y
class Circle:
def __init__(self, rad=1, x=0, y=0):
self.radius = rad
self.x = x
self.y = y
上述並列的兩個類別分別是
Square 與 Circle,注意到它們的設定內容有部份雷同,我們可以利用此特性,為它們建立一個「母類別」。class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
class Square(Shape):
def __init__(self, side=1, x=0, y=0):
Shape.__init__(self, x, y)
self.side = side
class Circle(Shape):
def __init__(self, rad=1, x=0, y=0):
Shape.__init__(self, x, y)
self.radius = rad
我們定義了一個母類別
Shape,以 class Square(Shape) 或 class Circle(Shape) 宣告方式,即可完成繼承母類別的第一步驟。值得注意的是,在 Square 與 Circle 繼承 Shape 母類別後,記得要明確地使用 Shape.__init__(self, x, y) 物件方法呼叫,使之生效。物件、類別的內容很重要,而且也很容易混淆,在此我們先簡單介紹上述基本的
Python 類別、物件功能設定,讀者務必要練習熟悉,奠定好基礎。類別庫功能
(package)我們之前已介紹過模組
(module),簡單地看,模組就是一個 Python 程式碼檔案,裡頭包含一些函式與物件功能。而呼叫模組時,就是以程式碼檔案的檔案名稱來命名。一旦上述的運作模式熟悉了,則類別庫 (package) 便不難理解,因為類別庫只是在一個目錄之下,將一堆功能相近而能相互引用的程式碼檔案收集在一起。而呼叫類別庫時,則是以其上層目錄名稱來命名。類別庫呈現檔案目錄架構
類別庫的概念及設計方式,原則上就是模組的擴充,在大型的程式開發專案中,類別庫能夠整理一組相關的函式、物件、變數,分門別類地安排於目錄之下,讓程式人員容易進行管理。底下即是一個簡略的範例。
圖
: 類別庫範例示意圖中的
mathproj 即代表最上層目錄,其底下有一個 __init__.py 檔案,以及一個 comp 目錄,而 comp 目錄下,又類推有 __init__.py 檔案、c1.py 檔案、numeric 目錄,在 numeric 目錄下,則有 __init__.py 檔案、n1.py 檔案、n2.py 檔案。__init__.py 檔案,其功能也就是類似類別內容設定裡的 __init__() 函式,在類別庫的設計方式裡,以檔案型式存在。下列是非常簡略的類別庫範例的程式內容,用以示範其運作概念。
example$ cat mathproj/__init__.py
print "Hello from mathproj init"
__all__ = ['comp']
version = 1.03
example$ cat mathproj/comp/__init__.py
__all__ = ['c1']
print "Hello from mathproj.comp init"
example$ cat mathproj/comp/c1.py
x = 1.00
example$ cat mathproj/comp/numeric/__init__.py
print "Hello from nemeric init"
example$ cat mathproj/comp/numeric/n1.py
from mathproj import version
from mathproj.comp import c1
from n2 import h
def g():
print "version is", version
print h()
example$ cat mathproj/comp/numeric/n2.py
def h():
print "Called function h in module n2"
執行來測試上述的類別庫程式內容時,必須確認
mathproj 目錄位於 Python 的搜尋路徑中,最簡單的方式,便是使用者的現行目錄設定在 mathproj 目錄之上,然後再開始一個 Python 的對話環境。>>> import mathproj
Hello from mathproj init
>>> mathproj.version
1.03
>>> mathproj.comp.numeric.n1
Traceback (innermost last):
File "<stdin>", line 1, in ?
AttributeError: comp
>>> import mathproj.comp.numeric.n1
Hello from mathproj.comp init
Hello from nemeric init
>>> mathproj.comp.numeric.n1.g()
version is 1.03
Called function h in module n2
None
上述的錯誤訊息,在於 mathproj 載入後,並不代表也自動將 comp、numeric 等子目錄裡的檔案設定都載入,因此使用者必須手動額外載入 mathproj.comp.numeric.n1 才能真正使用到子目錄裡的設定值。注意到前述
__init__.py 檔案程式碼裡所出現的 __all__ 屬性值,我們可以看到其以一個字串串列為設定值,而該字串串列即子目錄名稱,明確地說,就是用於當 from ... import * 語法出現時,所要載入的目錄項目是哪些。由於不同的作業系統,對於檔案名稱大小寫的處理方式不同,以 Unix/Linux 為例,檔案名稱大小寫是視為不同的,而 MS Windows 的環境,則將檔案名稱大小寫視為相同。如此一來,載入檔案時便可能造成混淆,而 __all__ 屬性值的設定,即是要明確地指定所要載入的目錄項目有哪些。如果讀者取得
Zope 這套以 Python 所寫成的網站應用程式,安裝之後便可發現其程式開發,大量應用了類別庫功能,有興趣深入了解類別庫設計的讀者,可以多多參考 Zope 的 product 內容。相關說明
[1] Regular Expression HOWTO 網頁,網址 http://py-howto.sourceforge.net/regex/regex.html
[2] PERL5 Regular Expression Description 網頁,網址http://www.cpan.org/doc/FMTEYEWTK/regexps.html
[3] Regular Expression 中文介紹,網址 http://www.cyut.edu.tw/~ckhung/olbook/gnulinux/regexp.shtml
[4] 完整的 Python Regular Expression 語法,網址http://www.python.org/doc/current/lib/module-re.html
[5] Regular Expression Matching 效能比較網頁,網址http://www.bagley.org/~doug/shootout/bench/regexmatch/
[6] How to think like a computer scientist -- Python Version 網頁,教你如何一窺程式設計的堂奧,網址http://www.ibiblio.org/obp/thinkCSpy/
[7] Python 與其他程式語言之比較,網址http://www.python.org/doc/Comparisons.html
[8] 物件導向式語言之簡介,網址 http://catalog.com/softinfo/objects.html
[9] Python Script 能夠設計哪些遊戲,網址 http://www.digivision.com.tw/DGAdvan/Animation/Ani11/Ani11.htm