11.1 打開文件
open函數用來打開文件,語法如下:
open(name[, mode[, buffering]])
open函數使用一個文件名作爲唯一的強制參數,然後返回一個文件對象。模式(mode)和緩衝(buffering)參數都是可選的,我會在後面的內容中對它們進行解釋。
因此,假設有一個名爲somefile.txt的文本文件(可能是用文本編輯器創建的),其存儲路徑是c:\text(或者在UNIX下的~/text),那麼可以像下面這樣打開文件。
>>> f = open(r"C:\text\somefile.txt")
如果文件不存在,則會看到一個類似下面這樣的異常回溯:
Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: [Errno 2] No such file or directory: 'C:\\text\\somefile.txt'
稍後會介紹文件對象的用處。在此之前,先來看看open函數的其他兩個參數。
11.1.1 文件模式
如果open函數只帶一個文件名參數,那麼我們可以獲得能讀取文件內容的文件對象。如果要向文件內寫入內容,則必須提供一個模式參數(稍後會具體地說明讀和寫方式)來顯式聲明。
open函數中的模式參數只有幾個值,如表11-1所示。
明確地指出讀模式和什麼模式參數都不用的效果是一樣的。使用寫模式可以向文件寫入內容。
'+'參數可以用到其他任何模式中,指明讀和寫都是允許的。比如'r+'能在打開一個文本文件用來讀寫時使用(也可以使用seek方法來實現,請參見本章後面的"隨機訪問"部分)。
表11-1 open函數中模式參數的常用值
'r' 讀模式
'w' 寫模式
'a' 追加模式
'b' 二進制模式(可添加到其他模式中使用)
'+' 讀/寫模式(可添加到其他模式中使用)
'b'模式改變處理文件的方法。一般來說,Python假定處理的是文本文件(包含字符)。通常這樣做不會有任何問題。但是如果處理的是一些其他類型的文件(二進制文件),比如聲音剪輯或者圖像,那麼應該在模式中增加'b'。參數'rb'可以用來讀取一個二進制文件。
爲什麼使用二進制模式
如果使用二進制模式來讀取(寫入)文件的話,與使用文本模式不會有很大區別。仍然能讀一定數量的字節(基本上和字符一樣),並且能執行和文本文件有關的操作。關鍵是,在使用二進制模式時,Python會原樣給出文件中的內容——在文本模式下則不一定。
Python對於文本文件的操作方式令人有些驚訝,但不必擔心。其中唯一要用到的技巧就是標準化換行符。一般來說,在Python中,換行符(\n)表示結束一行並另起一行,這也是UNIX系統中的規範。但在Windows中一行結束的標誌是\r\n。爲了在程序中隱藏這些區別(這樣的程序就能跨平臺運行),Python在這裏做了一些自動轉換:當在Windows下用文本模式讀取文件中的文本時,Python將\r\n轉換成\n。相反地,當在Windows下用文本模式向文件寫文本時,Python會把\n轉換成\r\n(Macintosh系統上的處理也是如此,只是轉換是在\r和\n之間進行)。
在使用二進制文件(比如聲音剪輯)時可能會產生問題,因爲文件中可能包含能被解釋成前面提及的換行符的字符,而使用文本模式,Python能自動轉換。但是這樣會破壞二進制數據。因此爲了避免這樣的事發生,要使用二進制模式,這樣就不會發生轉換了。
需要注意的是,在UNIX這種以換行符爲標準行結束標誌的平臺上,這個區別不是很重要,因爲不會發生任何轉換。
注:通過在模式參數中使用U參數能夠在打開文件時使用通用的換行符支持模式,在這種模式下,所有的換行符/字符串(\r\n、\r或者是\n)都被轉換成\n,而不用考慮運行的平臺。
11.1.2 緩衝
open函數的第3個參數(可選)控制着文件的緩衝。如果參數是0(或者是False),I/O(輸入/輸出)就是無緩衝的(所有的讀寫操作都直接針對硬盤);如果是1(或者True),I/O就是有緩衝的(意味着Python使用內存來代替硬盤,讓程序更快,只有使用flush或者close時纔會更新硬盤上的數據——參見11.2.4節)。大於1的數字代表緩衝區的大小(單位是字節),-1(或者是任何負數)代表使用默認的緩衝區大小。
11.2 基本的文件方法
打開文件的方法已經介紹了,那麼下一步就是用它們做些有用的事情。接下來會介紹文件對象(和一些類文件對象,有時稱爲*流*)的一些基本方法。
注:你可能會在Python的職業生涯多次遇到類文件這個術語(我已經使用了好幾次了)。類文件對象是支持一些file類方法的對象,最重要的是支持read方法或者write方法,或者兩者兼有。那些由urllib.urlopen(參見第14章)返回的對象是一個很好的例子。它們支持的方法有read、readline和readlines。但(在本書寫作期間)也有一些方法不支持,如isatty方法。
三種標準的流
第10章中關於sys模塊的部分曾經提到過3種流。它們實際上是文件(或者是類文件對象):大部分文件對象可用的操作它們也可以使用。
數據輸入的標準源是sys.stdin。當程序從標準輸入讀取數據時,你可以通過輸入或者使用管道把它和其他程序的標準輸出鏈接起來提供文本(管道是標準的UNIX概念)。
要打印的文本保存在sys.stdout內。input和raw_input函數的提示文字也是寫入在sys.stdout中的。寫入sys.stdout的數據一般是出現在屏幕上,但也能使用管道連接到其他程序的標準輸入。
錯誤信息(如棧追蹤)被寫入sys.stderr。它和sys.stdout在很多方面都很像。
11.2.1 讀和寫
文件(或流)最重要的能力是提供或者接受數據。如果有一個名爲f的類文件對象,那麼就可以用f.write方法和f.read方法(以字符串形式)寫入和讀取數據。
每次調用f.write(string)時,所提供的參數string會被追加到文件中已存在部分的後面。
>>> f = open("somefile.txt", "w") >>> f.write("Hello, ") >>> f.write("World!") >>> f.close()
在完成了對一個文件的操作時,調用close。這個方法會在11.2.4節進行詳細的介紹。
讀取很簡單,只要記得告訴流要讀多少字符(字節)即可。例子(接上例)如下:
>>> f = open("somefile.txt", "r") >>> f.read(4) 'Hell' >>> f.read() 'o, World!'
首先指定了我要讀取的字符數"4",然後(通過不提供要讀取的字符數的方式)讀取了剩下的文件。注意,在調用open時可以省略模式,因爲'r'是默認的。
11.2.2 管式輸出
在UNIX的shell(就像GUN bash)中,使用*管道*可以在一個命令後面續寫其他的多個命令,就像下面這個例子(假設是GUN bash)。
$ cat somefile.txt | python somescript.py | sort
注:GUN bash在Windows中也是存在的。http://www.cygwin.com 上面有更多的信息。在Mac OS X中,是通過Terminal程序,可以使用shell文件。
這個管道由以下三3個命令組成。
☑ cat somefile.txt:只是把somefile.txt的內容寫到標準輸出(sys.stdout)。
☑ python somescript.py:這個命令運行了Python腳本somescript。腳本應該是從標準輸入讀,把結果寫入到標準輸出。
☑ sort:這條命令從標準輸入(sys.stdin)讀取所有的文本,按字母排序,然後把結果寫入標準輸出。
但管道符號(|)的作用是什麼?somescript.py的作用又是什麼呢?
管道符號講一個命令的標準輸出和下一個命令的標準輸入連接在一起。明白了嗎?這樣,就知道somescript.py會從它的sys.stdin中讀取數據(cat somefile.txt寫入的),並把結果寫入它的sys.stdout(sort在此得到數據)中。
使用sys.stdin的一個簡單的腳本(somescript)如代碼清單11-1所示。somefile.txt文件的內容如代碼清單11-2所示。
# 代碼清單 11-1 統計sys.stdin中單詞數的簡單腳本 # somescript.py import sys text = sys.stdin.read() words = text.split() wordcount = len(words) print "Wordcount:", wordcount # 代碼清單 11-2 包含示例文本的文件 Your mother was a hamster and your father smelled of elderberries.
下面是cat somefile.txt | python somescript.py的結果。
Wordcount: 11
隨機訪問
本章內的例子把文件都當成流來操作,也就是說只能按照從頭到尾的順序讀數據。實際上,在文件中隨意移動讀取位置也是可以的,可以使用類文件對象的方法seek和tell來直接訪問感興趣的部分(這種做法稱爲隨機訪問)。
seek(offset[, whence])
這個方法把當前位置(進行讀和寫的位置)移動到由offset和whence定義的位置。Offset類是一個字節(字符)數,表示偏移量。whence默認是0,表示偏移量是從文件開頭開始計算的(偏移量必須是非負的)。whence可能被設置爲1(相對於當前位置的移動,此時偏移量offset可以是負的)或者2(相對於文件結尾的移動)。
考慮下面這個例子:
>>> f = open(r"c:\text\somefile.txt", "w") >>> f.write("01234567890123456789") >>> f.seek(5) >>> f.write("Hello, World!") >>> f.close() >>> f = open(r"c:\text\somefile.txt") >>> f.read() >>> '01234Hello, World!89' # tell方法返回當前文件的位置如下例所示: >>> f = open(r"c:\text\somefile.txt") >>> f.read(3) >>> '012' >>> f.read(2) >>> '34' >>> f.tell() >>> 5L
11.2.3 讀寫行
實際上,程序到現在做的工作都是很不實用的。通常來說,逐個字符串讀取文件也是沒問題的,進行逐行的讀取也可以。還可以使用file.readline讀取單獨的一行(從當前位置開始直到一個換行符出現,也讀取這個換行符)。不使用任何參數(這樣,一行就被讀取和返回)或者使用一個非負數的整數作爲readline可以讀取的字符(或字節)的最大值。因此,如果someFile.readline()返回"Hello, World!\n",someFile.readline(5)返回"Hello"。readlines方法可以讀取一個文件中的所有行並將其作爲列表返回。
writelines方法和readlines相反:傳給它一個字符串的列表(實際上任何序列或者可迭代的對象都行),它會把所有的字符串寫入文件(或流)。注意,程序不會增加新行,需要自己添加。沒有writeline方法,因爲能使用write。
注:在使用其他的符號作爲換行符的平臺上,用\r(Mac中)和\r\n(Windows中)代替\n(有os.linesep決定)。
11.2.4 關閉文件
應該牢記使用close方法關閉文件。通常來說,一個文件對象在退出程序後(也可能在退出前)自動關閉,儘管是否關閉文件不是很重要,但關閉文件是沒有什麼害處的,可以避免在某些操作系統或設置中進行無用的修改,這樣做也會避免用完系統中所打開文件的配額。
寫入過的文件總是應該關閉,是因爲Python可能會緩存(出於效率的考慮而把數據臨時地存儲在某處)寫入的數據,如果程序因爲某些原因崩潰了,那麼數據根本就不會被寫入文件。爲了安全起見,要在使用完文件後關閉。
如果想確保文件被關閉了,那麼應該使用try/finally語句,並且在finally子句中調用close方法。
# Open your file here try: # Write data to your file finally: file.close()
事實上,有專門爲這種情況設計的語句(在Python2.5中引入),即with語句:
with open("somefile.txt") as somefile: do_something(somefile)
with語句可以打開文件並且將其賦值到變量上(本例是somefile)。之後就可以將數據寫入語句體中的文件(或許執行其他操作)。文件在語句結束後會被自動關閉,即使是處於異常引起的結束也是如此。
在Python2.5中,with語句只有在導入如下的模塊後纔可以用:
from __future__ import with_statement
而2.5之後的版本中,with語句可以直接使用。
注:在寫入了一些文件的內容後,通常的想法是希望這些改變會立刻體現在文件中,這樣一來其他讀取這個文件的程序也能知道這個改變。哦,難道不是這樣嗎?不一定。數據可能被緩存了(在內存中臨時性地存儲),直到關閉文件纔會被寫入到文件。如果需要繼續使用文件(不關閉文件),又想將磁盤上的文件進行更新,以反映這些修改,那麼就要調用文件對象的flush方法(注意,flush方法不允許其他程序使用該文件的同時訪問文件,具體的情況依據使用的操作系統和設置而定。不管在什麼時候,能關閉文件時最好關閉文件)。
上下文管理器
with語句實際上是很通用的結構,允許使用所謂的上下文管理器(context manager)。上下文管理器是一種支持__enter__和__exit__這兩個方法的對象。
__enter__方法不帶參數,它在進入with語句塊的時候被調用,返回值綁定到在as關鍵字之後的變量。
__exit__方法帶有3個參數:異常類型、異常對象和異常回溯。在離開方法(通過帶有參數提供的、可引發的異常)時這個函數被調用。如果__exit__返回false,那麼所有的異常都不會被處理。
文件可以被用作上下文管理器。它們的__enter__方法返回文件對象本身,__exit__方法關閉文件。有關這個強大且高級的特性的更多信息,請參看Python參考手冊中的上下文管理器部分。或者可以在Python庫參考中查看上下文管理器和contextlib部分。
11.2.5 使用基本文件方法
假設somefile.txt包含如代碼清單11-3所示的內容,能對它進行什麼操作?
# 代碼清單11-3 一個簡單的文本文件 Welcome to this file There is nothing here except This stupid haiku
讓我們試試已經知道的方法,首先是read(n):
>>> f = open(r"C:\text\somefile.txt") >>> f.read(7) 'Welcome' >>> f.read(4) ' to ' >>> f.close()
然後是read():
>>> f = open(r"C:\text\somefile.txt") >>> print f.read() Welcome to this file There is nothing here except This stupid haiku >>> f.close()
接着是readline():
>>> f = open(r"C:\text\somefile.txt") >>> for i in range(3): ... print str(i) + ": " + f.readline(), ... 0: Welcome to this file 1: There is nothing here except 2: This stupid haiku >>> f.close()
以及readlines():
>>> import pprint >>> pprint.pprint(open(r"C:\text\somefile.txt").readlines()) ['Welcome to this file\n', 'There is nothing here except\n', 'This stupid haiku']
注意,本例中我所使用的是文件對象自動關閉的方式。
下面是寫文件,首先是write(string):
>>> f = open(r"C:\text\somefile.txt", "w") >>> f.write("this\nis no\nhaiku") >>> f.close()
在運行這個程序後,文件包含的內容如代碼清單11-4所示。
# 代碼清單11-4 修改了的文本文件 this is no haiku
最後是writelines(list):
>>> f = open(r"C:\text\somefile.txt") >>> lines = f.readlines() >>> f.close() >>> lines[1] = "isn't a\n" >>> f = open(r"C:\text\somefile.txt", "w") >>> f.writelines(lines) >>> f.close()
運行這個程序後,文件包含的文本如代碼清單11-5所示。
# 代碼清單11-5 再次修改的文本文件 this isn't a haiku
11.3 對文件內容進行迭代
前面介紹了文件對象提供的一些方法,以及如何獲取這樣的文件對象。對文件內容進行迭代以及重複執行一些操作,是最常見的文件操作之一。儘管有很多方法可以實現這個功能,或者可能有人會偏愛某一種並堅持只使用那種方法,但是還有一些人使用其他的方法,爲了能理解他們的程序,你就應該瞭解所有的基本技術。其中的一些技術是使用曾經見過的方法(如read、readline和readlines),另一些方法是我即將介紹的(比如xreadlines和文件迭代器)。
在這部分的所有例子中都使用了一個名爲process的函數,用來表示每個字符或每行的處理過程。讀者也可以用你喜歡的方法自行實現這個函數。下面就是一個例子:
def process(string): print "Processing: ", string
更有用的實現是在數據結構中存儲數據,計算和值,用re模塊來代替模式或者增加行號。
如果要嘗試實現以上功能,則應該把filename變量設置爲一個實際的文件名。
11.3.1 按字節處理
最常見的對文件內容進行迭代的方法是在while循環中使用read方法。例如,對每個字符(字節)進行循環,可以用代碼清單11-6所示的方法實現。
# 代碼清單11-6 用read方法對每個字符進行循環 f = open(filename) char = f.read(1) while char: process(char) char = f.read(1) f.close()
這個程序可以使用是因爲當到達文件的末尾時,read方法返回一個空的字符串,但在那之前返回的字符串會包含一個字符(這樣布爾值是真)。如果char是真,則表示還沒有到文件末尾。
可以看到,賦值語句char = f.read(1)被重複地使用,代碼重複通常被認爲是一件壞事。(懶惰是美德,還記得嗎?)爲了避免發生這種情況,可以使用在第五章介紹過的while true/break語句。最終的代碼如代碼清單11-7所示。
# 代碼清單11-7 用不同的方式寫循環 f = open(filename) while True: char = f.read() if not char: break process(char) f.close
如在第五章提到的,break語句不應該頻繁地使用(因爲這樣會讓代碼很難懂);儘管如此,代碼清單11-7中使用的方法比代碼清單11-6中的方法要好,因爲前者避免了重複的代碼。
11.3.2 按行操作
當處理文本文件時,經常會對文件的行進行迭代而不是處理單個字符。處理行使用的方法和處理字符一樣,即使用readline方法(先前在11.2.3節介紹過),如代碼清單11-8所示。
# 代碼清單11-8 在while循環中使用readline f = open(filename) while True: line = f.readline() if not line: break process(line) f.close()
11.3.3 讀取所有內容
如果文件不是很大,那麼可以使用不帶參數的read方法一次讀取整個文件(把整個文件當做一個字符串來讀取),或者使用readlines方法(把文件讀入一個字符串列表,在列表中每個字符串就是一行)。代碼清單11-9和代碼清單11-10展示了在讀取這樣的文件時,在字符串和行上進行迭代是多麼容易。注意,將文件的內容讀入一個字符串或者是讀入列表在其他時候也很有用。比如在讀取後,就可以對字符串使用正則表達式操作,也可以將行列表存入一些數據結構中,以備將來使用。
# 代碼清單11-9 用read迭代每個字符 f = open(filename) for char in f.read(): process(char) f.close() # 代碼清單11-10 用readlines迭代行 f = open(filename) for line in f.readlines(): process(line) f.close()
11.3.4 使用fileinput實現懶惰行迭代
在需要對一個非常大的文件進行行迭代的操作時,readlines會佔用太多的內存。這個時候可以使用while循環和readline方法來替代。當然,在Python中如果能使用for循環,那麼它就是首選。本例恰好可以使用for循環可以使用一個名爲懶惰行迭代的方法:說它懶惰是因爲它只是讀取實際需要的文件部分。
第十章內已經介紹過fileinput,代碼清單11-11演示了它的用法。注意,fileinput模塊包含了打開文件的函數,只需要傳一個文件名給它。
# 代碼清單11-11 用fileinput來對行進行迭代 import fileinput for line in fileinput.input(filename): process(line)
注:在舊式代碼中,可使用xreadlines實現懶惰行迭代。它的工作方式和readlines很類似,不同點在於,它不是將全部的行讀到列表中而是創建了一個xreadlines對象。注意,xreadlines是舊式的,在你自己的代碼中最好用fileinput或文件迭代器(下面來介紹)。
11.3.5 文件迭代器
現在是展示所有最酷的技術的時候了,在Python中如果一開始就存在這個特性的話,其他很多方法(至少包括xreadlines)可能就不會出現了。那麼這種技術到底是什麼?在Python的近幾個版本中(從2.2開始),文件對象是可迭代的,這就意味着可以直接在for循環中使用它們,從而對它們進行迭代。如代碼清單11-12所示,很優雅,不是嗎?
# 代碼清單11-12 迭代文件 f = open(filename) for line in f: process(line) f.close()
在這些迭代的例子中,都沒有顯式的關閉文件的操作,儘管在使用完以後,文件的確應該關閉,但是隻要沒有向文件內寫入內容,那麼不關閉文件也是可以的。如果希望由Python來負責關閉文件(也就是剛纔所做的),那麼例子應該進一步簡化,如代碼清單11-13所示。在那個例子中並沒有把一個打開的文件賦給變量(就像我在其他例子中使用的變量f),因此也就沒辦法顯式地關閉文件。
# 代碼清單11-13 對文件進行迭代而不使用變量存儲文件對象 for line in open(filename): process(line)
注意sys.stdin是可迭代的,就像其他的文件對象。因此如果想要迭代標準輸入中的所有行,可以按如下形式使用sys.stdin。
import sys for line in sys.stdin: process(line)
可以對文件迭代器執行和普通迭代器相同的操作。比如將它們轉換爲字符串列表(使用list(open(filename))),這樣所達到的效果和使用readlines一樣。
考慮下面的例子:
>>> f = open("somefile.txt", "w") >>> f.write("First line\n") >>> f.write("Second line\n") >>> f.write("Third line\n") >>> f.close() >>> lines = list(open("somefile.txt")) >>> lines ['First line\n', 'Second line\n', 'Third line\n'] >>> first, second, third = open("somefile.txt") >>> first 'First line\n' >>> second 'Second line\n' >>> third 'Third line\n'
在這個例子中,注意下面的幾點很重要。
☑ 在使用print來向文件內寫入內容,這會在提供的字符串後面增加新的行。
☑ 使用序列來對一個打開的文件進行解包操作,把每行都放入一個單獨的變量中(這麼做是很有實用性的,因爲一般不知道文件中有多少行,但它演示了文件對象的"迭代性")。
☑ 在寫文件後關閉了文件,是爲了確保數據被更新到硬盤(你也看到了,在讀取文件後沒有關閉文件,或許是太馬虎了,但並沒有錯)。
11.4 小結
本章中介紹瞭如何通過文件對象和類文件對象與環境互動,I/O也是Python中最重要的技術之一。下面是本章的關鍵知識。
☑ 類文件對象:類文件對象是支持read和readline方法(可能是write和writelines)的非正式對象。
☑ 打開和關閉文件:通過提供一個文件名,使用open函數打開一個文件(在新版的Python中實際上是file的別名)。如果希望確保文件被正常關閉,即使發生錯誤時也是如此可以使用with語句。
☑ 模式和文件類型:當打開一個文件時,也可以提供一個模式,比如'r'代表讀模式,'w'代表寫模式。還可以將文件作爲二進制文件打開(這個只在Python進行換行符轉換的平臺上才需要,比如Windows,或許其他地方也應該如此)。
☑ 標準流:3個標準文件對象(在sys模塊中的stdin、stdout和stderr)是一個類文件對象,該對象實現了UNIX標準的I/O機制(Windows中也能用)。
☑ 讀和寫:使用read或是write方法可以對文件對象或類文件對象進行讀寫操作。
☑ 讀寫行:使用readline和readlines和(用於有效迭代的)xreadlines方法可以從文件中讀取行,使用writelines可以寫入數據。
☑ 迭代文件內容:有很多方法可以迭代文件的內容。一般是迭代文本中的行,通過迭代文件對象本身可以輕鬆完成,也有其他的方法,就像readlines和xreadlines這兩個倩兼容Python老版本的方法。
11.4.1 本章的新函數
本章涉及的新函數如表11-2所示。
表11-2 本章的新函數
file(name[, mode[, buffering]]) 打開一個文件並返回一個文件對象
open(name[, mode[, buffering]]) file的別名;在打開文件時,使用open而不是file