Think Python/檔案
到目前為止,我們看到的大多數程式都是臨時的,因為它們只執行一小段時間併產生一些輸出,但是當它們結束時,它們的資料就會消失。如果你再次執行程式,它將從一個乾淨的狀態開始。
其他程式是永續性的:它們執行很長時間(或一直執行);它們將至少一部分資料儲存在永久儲存器(例如硬碟驅動器)中;如果它們關閉並重新啟動,它們將從上次中斷的地方繼續。
永續性程式的示例包括作業系統,它幾乎在計算機開機時一直執行,以及 Web 伺服器,它一直執行,等待網路上的請求。
程式維護其資料最簡單的方法之一是讀寫文字檔案。我們已經看到了讀取文字檔案的程式;在本章中,我們將看到編寫它們的程式。
另一種方法是將程式的狀態儲存在資料庫中。在本章中,我將介紹一個簡單的資料庫和一個模組,醃製,它使儲存程式資料變得容易。
文字檔案是儲存在永久介質(如硬碟驅動器、快閃記憶體或 CD-ROM)上的字元序列。我們在第 9.1 節中看到了如何開啟和讀取檔案。
要寫入檔案,你必須用模式 'w' 作為第二個引數開啟它
>>> fout = open('output.txt', 'w')
>>> print fout
<open file 'output.txt', mode 'w' at 0xb7eb2410>
如果檔案已經存在,用寫入模式開啟它會清除舊資料並從頭開始,所以要小心!如果檔案不存在,則會建立一個新檔案。
該寫方法將資料放入檔案。
>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)
同樣,檔案物件會跟蹤它所在的位置,因此如果你呼叫寫再次,它會將新資料新增到末尾。
>>> line2 = "the emblem of our land.\n"
>>> fout.write(line2)
當你完成寫入時,你必須關閉檔案。
>>> fout.close()
的實參必須是字串,因此如果我們想要將其他值放入檔案,我們必須將它們轉換為字串。最簡單的方法是使用寫str另一種方法是使用格式化運算子,:
>>> x = 52
>>> f.write(str(x))
。當應用於整數時,%是模運算子。但當第一個運算元是字串時,%是格式化運算子。%第一個運算元是格式化字串,第二個運算元是表示式元組。結果是一個字串,其中包含表示式的值,根據格式化字串進行格式化。
例如,格式化序列 '%d' 表示元組中的第一個表示式應格式化為整數(
d代表“十進位制”)結果是字串 '42',不要與整數 42 混淆。
>>> camels = 42
>>> '%d' % camels
'42'
格式化序列可以出現在格式化字串中的任何位置,因此你可以將一個值嵌入句子中42.
格式化序列 '%g' 將元組中的下一個元素格式化為浮點數(不要問為什麼),'%s' 將下一個元素格式化為字串
>>> camels = 42
>>> 'I have spotted %d camels.' % camels
'I have spotted 42 camels.'
元組中的元素數量必須與字串中的格式化序列數量匹配。此外,元素的型別必須與格式化序列匹配
>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels')
'In 3 years I have spotted 0.1 camels.'
在第一個示例中,元素數量不足;在第二個示例中,元素型別錯誤。
>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dollars'
TypeError: illegal argument type for built-in operation
格式化運算子功能強大,但難以使用。你可以在以下位置閱讀有關它的更多資訊:
docs.python.org/lib/typesseq-strings.html檔名和路徑.
os
該模組提供用於處理檔案和目錄的函式(“os”代表“作業系統”)。os.getcwd返回當前目錄的名稱cwd
>>> import os
>>> cwd = os.getcwd()
>>> print cwd
/home/dinsdale
代表“當前工作目錄”。本示例中的結果為/home/dinsdale,這是名為dinsdale的使用者的主目錄。.
像代表“當前工作目錄”。本示例中的結果為這樣的字串標識檔案被稱為路徑。相對路徑從當前目錄開始;絕對路徑從檔案系統中的最頂層目錄開始。
到目前為止,我們看到的路徑都是簡單檔名,因此它們相對於當前目錄。要查詢檔案的絕對路徑,可以使用os.path.abspath:
>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'
os.path.exists檢查檔案或目錄是否存在
>>> os.path.exists('memo.txt')
True
如果存在,os.path.isdir檢查它是否是目錄
>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('music')
True
類似地,os.path.isfile檢查它是否是檔案。
os.listdir返回給定目錄中的檔案(和其他目錄)列表
>>> os.listdir(cwd)
['music', 'photos', 'memo.txt']
為了演示這些函式,以下示例“遍歷”一個目錄,列印所有檔案的名稱,並在所有目錄上遞迴呼叫自身。
def walk(dir):
for name in os.listdir(dir):
path = os.path.join(dir, name)
if os.path.isfile(path):
print path
else:
walk(path)
os.path.join獲取一個目錄和一個檔名,並將它們組合成一個完整路徑。
修改'walk',使其不再列印檔案的名稱,而是返回一個名稱列表。
該 'os' 模組提供一個名為 'walk' 的函式,它類似於此函式,但功能更強大。閱讀文件並使用它列印給定目錄及其子目錄中檔案的名稱。
當你嘗試讀寫檔案時,很多事情都可能出錯。如果你嘗試開啟一個不存在的檔案,你會得到一個IOError:
>>> fin = open('bad_file')
IOError: [Errno 2] No such file or directory: 'bad_file'
如果你沒有許可權訪問檔案
>>> fout = open('/etc/passwd', 'w')
IOError: [Errno 13] Permission denied: '/etc/passwd'
如果你嘗試開啟一個目錄進行讀取,你會得到
>>> fin = open('/home')
IOError: [Errno 21] Is a directory
為了避免這些錯誤,你可以使用像os.path.exists和os.path.isfile這樣的函式,但這將花費很多時間和程式碼來檢查所有可能性(如果“Errno 21”有任何指示,至少有 21 件事可能出錯)。
最好是繼續嘗試,並在發生問題時處理它們,這正是try語句所做的。語法類似於if語句
try:
fin = open('bad_file')
for line in fin:
print line
fin.close()
except:
print 'Something went wrong.'
Python 從執行try子句開始。如果一切順利,它將跳過except子句並繼續執行。如果發生異常,它將跳出try子句並執行except子句。
使用try語句處理異常被稱為捕獲異常。在本示例中,except子句列印了一個不太有幫助的錯誤訊息。通常,捕獲異常可以讓你有機會修復問題,或重試,或至少以優雅的方式結束程式。
資料庫是一個組織用於儲存資料的檔案。大多數資料庫的組織方式類似於字典,因為它們從鍵對映到值。最大的區別在於資料庫位於磁碟(或其他永久儲存器)上,因此在程式結束時它會保留下來。
該模組anydbm提供用於建立和更新資料庫檔案的介面。例如,我將建立一個包含影像檔案字幕的資料庫。
開啟資料庫類似於開啟其他檔案
>>> import anydbm
>>> db = anydbm.open('captions.db', 'c')
模式 'c' 表示如果資料庫不存在,則應該建立它。結果是一個數據庫物件,它可以像字典一樣使用(對於大多數操作)。如果你建立一個新專案,anydbm更新資料庫檔案。
>>> db['cleese.png'] = 'Photo of John Cleese.'
當你訪問其中一個專案時,anydbm讀取檔案
>>> print db['cleese.png']
Photo of John Cleese.
如果你對現有鍵進行另一個賦值,anydbm替換舊值
>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.'
>>> print db['cleese.png']
Photo of John Cleese doing a silly walk.
許多字典方法,如keys和items,也適用於資料庫物件。迭代使用for語句也是如此。
for key in db:
print key
與其他檔案一樣,當你完成時,你應該關閉資料庫
>>> db.close()
的侷限性在於anydbm鍵和值必須是字串。如果嘗試使用任何其他型別,則會收到錯誤。
該醃製模組可以提供幫助。它將幾乎任何型別的物件轉換為適合儲存在資料庫中的字串,然後將字串轉換回物件。
pickle.dumps以物件作為引數並返回字串表示(dumps是“轉儲字串”的縮寫)
>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
'(lp0\nI1\naI2\naI3\na.'
該格式對人類讀者來說並不明顯;它旨在易於醃製解釋。pickle.loads(“載入字串”)重建物件
>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> print t2
[1, 2, 3]
雖然新物件的值與舊物件相同,但它(通常)不是同一個物件
>>> t == t2
True
>>> t is t2
False
換句話說,醃製然後取消醃製的效果與複製物件相同。
可以使用醃製在資料庫中儲存非字串。事實上,這種組合非常常見,以至於它已被封裝在一個名為shelve.
如果您完成了練習 '12.4',請修改您的解決方案,使其建立一個將列表中的每個單詞對映到使用相同字母集的單詞列表的資料庫。
大多數作業系統都提供命令列介面,也稱為shell。Shell 通常提供命令來導航檔案系統和啟動應用程式。例如,在 Unix 中,可以使用cd更改目錄,使用ls顯示目錄的內容,並透過鍵入(例如)啟動 Web 瀏覽器Firefox.
可以使用管道從 Python 啟動從 shell 啟動的任何程式。管道是一個表示正在執行的程序的物件。
例如,Unix 命令ls -l通常顯示當前目錄的內容(以長格式)。可以使用ls與os.popen:
>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)
引數是一個包含 shell 命令的字串。返回值是一個檔案指標,其行為與開啟的檔案完全相同。可以使用ls程序一次一行地讀取輸出readline或一次獲取所有內容read:
>>> res = fp.read()
完成後,像檔案一樣關閉管道
>>> stat = fp.close()
>>> print stat
None
返回值是ls程序的最終狀態;None表示它正常結束(沒有錯誤)。
管道的一個常見用途是增量讀取壓縮檔案;也就是說,無需一次性解壓縮整個檔案。以下函式以壓縮檔案的名稱作為引數,並返回使用gzip解壓縮內容的管道
def open_gzip(filename):
cmd = 'gunzip -c ' + filename
fp = os.popen(cmd)
return fp
如果從fp一次讀取一行,則無需將解壓縮的檔案儲存在記憶體或磁碟中。
任何包含 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:
>>> print wc
<module 'wc' from 'wc.py'>
它提供了一個名為 linecount 的函式
>>> wc.linecount('wc.py')
7
這就是在 Python 中編寫模組的方式。
此示例中唯一的問題是,當匯入模組時,它會在底部執行測試程式碼。通常,當匯入模組時,它會定義新函式,但不會執行它們。
將作為模組匯入的程式通常使用以下習語
if __name__ == '__main__':
print linecount('wc.py')
__name__ 是一個內建變數,在程式啟動時設定。如果程式正在作為指令碼執行,__name__ 的值為 __main__;在這種情況下,測試程式碼將被執行。否則,如果正在匯入模組,則跳過測試程式碼。
__name__ 的值是什麼?警告:如果匯入已匯入的模組,Python 不會執行任何操作。它不會重新讀取檔案,即使它已更改。
如果要重新載入模組,可以使用內建函式 'reload',但這可能很棘手,因此最安全的操作是重新啟動直譯器,然後再次匯入模組。
在讀取和寫入檔案時,可能會遇到與空格相關的錯誤。這些錯誤可能很難除錯,因為空格、製表符和換行符通常是不可見的
>>> 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。有些系統同時使用這兩種方式。如果在不同系統之間移動檔案,這些不一致可能會導致問題。
對於大多數系統,都有應用程式可以將一種格式轉換為另一種格式。您可以在wikipedia.org/wiki/Newline上找到它們(並詳細瞭解此問題)。或者,當然,您也可以自己編寫一個。
- 持久
- 與無限期執行並將至少部分資料儲存在永久儲存中的程式相關。
- 格式運算子
- 一個運算子,%,它接受一個格式字串和一個元組,並生成一個包含元組元素的字串,這些元素根據格式字串中指定的格式進行格式化。
- 格式字串
- 與格式運算子一起使用的字串,其中包含格式序列。
- 格式序列
- 格式字串中的一系列字元,例如%d,它指定了如何格式化值。
- 文字檔案
- 儲存在永久儲存(如硬碟驅動器)中的字元序列。
- 目錄
- 檔案的有命名集合,也稱為資料夾。
- 路徑
- 標識檔案的字串。
- 相對路徑
- 從當前目錄開始的路徑。
- 絕對路徑
- 從檔案系統中最頂層目錄開始的路徑。
- 捕獲
- 使用try和except語句阻止異常終止程式。
- 資料庫
- 其內容組織得像字典一樣的檔案,其中鍵對應於值。
import urllib
conn = urllib.urlopen('http://thinkpython.com/secret.html')
for line in conn.fp:
print line.strip()
執行此程式碼並按照您看到的說明進行操作。
- 編寫一個程式,遞迴地搜尋目錄及其所有子目錄,並返回具有給定字尾(如 '.mp3')的所有檔案的完整路徑列表。提示:'os.path' 提供了幾個用於操作檔案和路徑名的有用函式。
- 要識別重複項,可以使用雜湊函式,該函式讀取檔案並生成內容的簡短摘要。例如,MD5(訊息摘要演算法 5)接受任意長度的“訊息”,並返回一個 128 位的“校驗和”。兩個內容不同的檔案返回相同校驗和的可能性非常小。
您可以在 'wikipedia.org/wiki/Md5' 上閱讀有關 MD5 的資訊。在 Unix 系統上,可以使用程式 'md5sum' 和管道從 Python 計算校驗和。
我編寫了一個程式來解析這些檔案並將它們拆分為演員姓名、電影標題等。您可以從 'thinkpython.com/code/imdb.py' 下載它。
如果將 'imdb.py' 作為指令碼執行,它將讀取 'actors.list.gz' 並每行列印一個演員-電影對。或者,如果 'import imdb',則可以使用函式 process_file 來處理檔案。引數是檔名、函式物件和可選的要處理的行數。以下是一個示例:
''import imdb
def print_info(actor, date, title, role):
print actor, date, title, role
imdb.process_file('actors.list.gz', print_info)
''
當您呼叫 process_file 時,它將開啟 'filename',讀取內容,並針對檔案中的每一行呼叫 print_info。print_info 以演員、日期、電影標題和角色作為引數,並列印它們。
- 編寫一個程式,讀取 'actors.list.gz' 和 'actresses.list.gz' 並使用 'shelve' 構建一個數據庫,將每個演員對映到他的電影列表。
- 如果兩個演員至少共同出演過一部電影,那麼他們就是“搭檔”。處理上一步中構建的資料庫,並構建第二個資料庫,將每個演員對映到他的搭檔列表。
- 編寫一個程式,可以玩“凱文·貝肯六度分離”遊戲,您可以在'wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon' 上閱讀有關此遊戲的更多資訊。這個問題很有挑戰性,因為它需要您在圖中找到最短路徑。您可以在 'wikipedia.org/wiki/Shortest_path_problem' 上閱讀有關最短路徑演算法的更多資訊。