【練習題】第十四章--檔案(Think Python)
2.讀寫檔案
要寫入一個檔案,就必須要在開啟它的時候用『w』作為第二個引數(譯者注:w 就是 wirte 的意思了):
>>> fout = open('output.txt', 'w')
如果檔案已經存在了,這樣用寫入的模式來開啟,會把舊的檔案都清除掉,然後重新寫入檔案,所以一定要小心!如果檔案不存在,程式就會建立一個新的。
open 函式會返回一個檔案物件,檔案物件會提供各種方法來處理檔案。write 這個方法就把資料寫入到檔案中了。
>>> line1 = "This here's the wattle,\n" >>> fout.write(line1) 24
返回值是已寫入字元的數量。檔案物件會記錄所在位置,所以如果你再次呼叫write方法,會從檔案結尾的地方繼續新增新的內容。
>>> line2 = "the emblem of our land.\n"
>>> fout.write(line2)
24
寫完檔案之後,你需要用 close 方法來關閉檔案。
>>> fout.close()
如果不 close 這個檔案,就要等你的程式執行結束退出的時候,它自己才關閉了。
3.格式運算子
>>> camels = 42 >>> '%d' % camels '42'
如果格式化序列有一個以上了,那麼第二個引數就必須是一個元組了。每個格式序列對應元組當中的一個元素,次序相同。
下面的例子中,用了'%d'來格式化輸出整型值,用'%g'來格式化浮點數,'%s'就是給字串用的了。
>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels')
'In 3 years I have spotted 0.1 camels.'
這就要注意力,如果字串中格式化序列有多個,那個數一定要和後面的元組中元素數量相等才行。另外格式化序列與元組中元素的型別也必須一樣。
4.檔名與路徑
>>> import os >>> cwd = os.getcwd() >>> cwd '/home/dinsdale' >>> os.path.abspath('memo.txt') '/home/dinsdale/memo.txt' >>> os.path.exists('memo.txt') True >>> os.path.isdir('memo.txt') False >>> os.path.isdir('/home/dinsdale') True >>> os.listdir(cwd) ['music', 'photos', 'memo.txt']
為了展示一下這些函式的用法,下面這個例子中,walks 這個函式就遍歷了一個目錄,然後輸出了所有該目錄下的檔案的名字,並且在該目錄下的所有子目錄中遞迴呼叫自身。
def walk(dirname):
for name in os.listdir(dirname):
path = os.path.join(dirname, name)
if os.path.isfile(path):
print(path)
else:
walk(path)
5.捕獲異常
try:
fin = open('bad_file')
except:
print('Something went wrong.')
Python 會先執行 try 後面的語句。如果執行正常,就會跳過 except 語句,然後繼續執行。如果除了異常,就會跳出 try 語句,然後執行 except 語句中的程式碼。
這種用 try 語句來處理異常的方法,就叫異常捕獲。上面的例子中,except 語句中的輸出資訊並沒有什麼用。一般情況,得到異常之後,你可以選擇解決掉這個問題或者再重試一下,或者就以正常狀態退出程式了。
6.資料庫
資料庫是一個用來管理已儲存資料的檔案。很多資料庫都以類似字典的形式來管理資料,就是從鍵到鍵值成對對映。資料庫和字典的最大區別就在於資料庫是儲存在磁碟(或者其他永久性儲存裝置中),所以程式執行結束退出後,資料庫依然存在。
(譯者注:這裡作者為了便於理解,對資料庫的概念進行了極度的簡化,實際上資料庫的型別、模式、功能等等都與字典有很大不同,比如有關係型資料庫和非關係型資料庫,還有分散式的和單一檔案式的等等。如果有興趣對資料庫進行進一步瞭解,譯者推薦一本書:SQLite Python Tutorial。)
dbm 模組提供了一個建立和更新資料庫檔案的互動介面。下面這個例子中,我建立了一個數據庫,其中的內容是影象檔案的標題。
開啟資料庫檔案就跟開啟其他檔案差不多:
>>> import dbm
>>> db = dbm.open('captions', 'c')
後面這個 c 是一個模式,意思是如果該資料庫不存在就建立一個新的。得到的返回結果就是一個數據庫物件了,用起來很多的運算都跟字典很像。
建立一個新的項的時候,dbm 就會對資料庫檔案進行更新了。
>>> db['cleese.png'] = 'Photo of John Cleese.'
讀取裡面的某一項的時候,dbm 就讀取資料庫檔案:
>>>db['cleese.png']
b'Photo of John Cleese.'
上面的程式碼返回的結果是一個二進位制物件,這也就是開頭有個 b 的原因了。二進位制物件就跟字串在很多方面都挺像的。以後對 Python 的學習深入了之後,這種區別就變得很重要了,不過現在還不要緊,咱們就忽略掉。
如果對一個已經存在值的鍵進行賦值,dbm 就會把舊的值替換成新的值:
>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.'
>>> db['cleese.png']
b'Photo of John Cleese doing a silly walk.'
字典的一些方法,比如 keys 和 items,是不能用於資料庫物件的。但用一個 for 迴圈來迭代是可以的:
for key in db:
print(key, db[key])
然後就同其他檔案一樣,用完了之後你得用 close 方法關閉資料庫:
>>> db.close()
7.Pickle模組
dbm 的侷限就在於鍵和鍵值必須是字串或者二進位制。如果用其他型別資料,就得到錯誤了。
這時候就可以用 pickle 模組了。該模組可以把幾乎所有型別的物件翻譯成字串模式,以便儲存在資料庫中,然後用的時候還可以把字串再翻譯回來。
pickle.dumps 接收一個物件做引數,然後返回一個字串形式的內容翻譯(dumps 就是『dump string』的縮寫):
>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
b'\x80\x03]q\x00(K\x01K\x02K\x03e.'
這種格式讓人讀起來挺複雜;這種設計能讓 pickle 模組解譯起來比較容易。pickle.lods("load string")就又會把原來的物件解譯出來:
>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> t2
[1, 2, 3]
這裡要注意了,新的物件與舊的有一樣的值,但(通常)並不是同一個物件:
>>> t1 == t2
True
>>> t1 is t2
False
換句話說,就是說 pickle 解譯的過程就如同複製了原有物件一樣。
有 pickle了,就可以把非字串的資料也存到資料庫裡面了。實際上這種結合方式特別普遍,已經封裝到一個叫shelve的模組中了。
8.管道
大多數作業系統都提供了一個命令列介面,也被稱作『shell』。Shell 通常提供了很多基礎的命令,能夠來搜尋檔案系統,以及啟動應用軟體。比如,在 Unix 下面,就可以通過 cd 命令來切換目錄,用 ls 命令來顯示一個目錄下的內容,如果裝了火狐瀏覽器,就可以輸入 fireforx 來啟動瀏覽器了。
在 shell 下能夠啟動的所有程式,也都可以在 Python 中啟動,這要用到一個 pipe 物件,這個直接翻譯意思為管道的物件可以理解為 Python 到作業系統的 Shell 進行通訊的途徑,一個 pipe 物件就代表了一個執行的程式。
舉個例子吧,Unix 的 ls -l 命令通常會用長檔名格式來顯示當前目錄的內容。在 Python 中就可以用 os.open 來啟動它:
>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)
引數 cmd 是包含了 shell 命令的一個字串。返回的結果是一個物件,用起來就像是一個打開了的檔案一樣。
可以讀取ls 程序的輸出,用 readline 的話每次讀取一行,用 read 的話就一次性全部讀取:
>>> res = fp.read()
用完之後要關閉,這點也跟檔案一樣:
>>> stat = fp.close()
>>> print(stat)
None
返回值是 ls 這個程序的最終狀態;None 的意思就是正常退出(沒有錯誤)。
舉個例子,大多數 Unix 系統都提供了一個教唆 md5sum 的函式,會讀取一個檔案的內容,然後計算一個『checksum』(校驗值)。你可以點選這裡閱讀更多相關內容。
這個命令可以很有效地檢查兩個檔案是否有相同內容。兩個不同內容產生同樣的校驗值的可能性是很小的(實際上在宇宙坍塌之前都沒戲)。
你就可以用一個 pipe 來從 Python 啟動執行 md5sum,然後獲取結果:
>>> filename = 'book.tex'
>>> cmd = 'md5sum ' + filename
>>> fp = os.popen(cmd)
>>> res = fp.read()
>>> stat = fp.close()
>>> print(res)
1e0033f0ed0656636de0d75144ba32e0 book.tex
>>> print(stat)
None
9.編寫模組
任何包含 Python 程式碼的檔案都可以作為模組被匯入使用。舉個例子,假設你有一個名字叫 wc.py 的檔案,裡面程式碼如下:
def linecount(filename):
count = 0
for line in open(filename):
count += 1
return count
print(linecount('wc.py'))
如果執行這個程式,程式就會讀取自己本身,然後輸出檔案中的行數,也就是7行了。你還可以匯入這個模組,如下所示:
>>> import wc
7
現在你就有一個模組物件 wc 了:
>>> wc
<module 'wc' from 'wc.py'>
該模組提供了數行數的函式linecount:
>>> wc.linecount('wc.py')
7
你看,你就可以這樣來為 Python 寫模組了。
當然這個例子中有個小問題,就是匯入模組的時候,模組內程式碼在最後一行對自身進行了測試。
一般情況你匯入一個模組,模組只是定義了新的函式,但不會去主動執行自己內部的函式。
以模組方式匯入使用的程式一般用下面這樣的慣用形式:
if __name__ == '__main__':
print(linecount('wc.py'))
name 是一個內建變數,當程式開始執行的時候被設定。如果程式是作為指令碼來執行的,name 的值就是'main';這樣的話,if條件滿足,測試程式碼就會執行。而如果該程式碼被用作模組匯入了,if 條件不滿足,測試的程式碼就不會運行了。
10.除錯
讀寫檔案的時候,你可能會碰到空格導致的問題。這些問題很難解決,因為空格、跳錶以及換行,平常就難以用眼睛看出來:
>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2 3
4
這時候就可以用內建函式 repr 來幫忙。它接收任意物件作為引數,然後返回一個該物件的字串表示。對於字串,該函式可以把空格字元轉成反斜槓序列:
>>> print(repr(s))
'1 2\t 3\n 4'
該函式的功能對除錯來說很有幫助。
另外一個問題就是不同作業系統可能用不同字元表示行尾。
有的用一個換行符,也就是\n。有的用一個返回字元,也就是\r。有的兩個都虧。如果你把檔案在不同作業系統只見移動,這種不相容性就可能導致問題了。
對大多數作業系統,都有一些應用軟體來進行格式轉換。你可以在https://en.wikipedia.org/wiki/Newline查詢一下(並且閱讀關於該問題的更多細節)。當然,你也可以自己寫一個轉換工具了。
練習1:
寫一個函式,名為 sed,接收一個目標字串,一個替換字串,然後兩個檔名;讀取第一個檔案,然後把內容寫入到第二個檔案中,如果第二個檔案不存在,就建立一個。如果目標字串在檔案中出現了,就用替換字串把它替換掉。
如果在開啟、讀取、寫入或者關閉檔案的時候發生了錯誤了,你的程式應該要捕獲異常,然後輸出錯誤資訊,然後再退出。
def sed(s1,s2,f1,f2):
try:
fin=open(f1,'r')
fout=open(f2,'w')
except:
print('openError!')
for s in fin:
try:
if(s.strip()==s1):
fout.write(s2+'\r\n')
else:
fout.write(s)
except:
print('writeError!')
fin.close()
fout.close()
def main():
s1='aa'
s2='bbb'
f1='words.txt'
f2='words2.txt'
sed(s1,s2,f1,f2)
if __name__=='__main__':
main()
練習2:
如果你從 這裡下載了我的樣例程式碼,你會發現該程式建立了一個字典,建立了從一個有序字母字串到一個單詞列表的對映,列表中的單詞可以由這些字母拼成。例如'opst'就對映到了列表 [’opts’, ’post’, ’pots’, ’spot’, ’stop’, ’tops’].
寫一個模組,匯入 anagram_sets 然後提供兩個函式:store_anagrams 可以把相同字母異序詞詞典儲存到一個『shelf』;read_anagrams 可以查詢一個詞,返回一個由其 相同字母異序詞 組成的列表。