1 IO的含義
在計算機中,IO是Input/Output的簡寫,也就是輸入和輸出。
由於程序和運行時數據是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方,通常是磁盤、網絡等,就需要IO接口。
比如你訪問百度首頁,瀏覽器就需要通過網絡IO獲取網頁。瀏覽器先會發送請求給百度服務器,告訴它想要的html網址,這個動作是往外發數據,叫Output。接着百度服務器把網頁的內容發送過來,這個動作是從外面接收數據,叫Input。
所以,通常,程序完成IO操作會有Input和Output兩個數據流。當然也有只用一個的情況,比如,從磁盤讀取文件到內存,就只有Input操作,反過來,把數據寫到磁盤文件里,就只是一個Output操作。
輸入輸出是相對的,要考慮具體的對象是什么。一般,當我們寫的程序需要讀取磁盤文件時,相當於把磁盤的數據輸入到程序中,對於程序來說,讀取的數據就屬於Input,但是對於磁盤來說,相當於把數據輸出給程序,輸出的數據屬於Output。
IO編程中,Stream(流)是一個很重要的概念,可以把流想象成一個水管,數據就是水管里的水,但是只能單向流動。Input Stream就是數據從外面(磁盤、網絡)流進內存,Output Stream就是數據從內存流到外面去。對於瀏覽網頁來說,瀏覽器和服務器之間至少需要建立兩根水管,才可以既能發數據,又能收數據。
由於CPU和內存的速度遠遠高於外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數據寫入磁盤,CPU輸出100M的數據只需要0.01秒,可是磁盤要接收這100M數據可能需要10秒,怎么辦呢?有兩種辦法:
- 第一種是CPU等着,也就是程序暫停執行后續代碼,等100M的數據在10秒后寫入磁盤,再接着往下執行,這種模式稱為同步IO。
- 另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不着急,我接着干別的事去了”,於是,后續代碼可以立刻接着執行,這種模式稱為異步IO。
同步和異步的區別就在於是否等待IO執行的結果。好比你去麥當勞點餐,你說“來個漢堡”,服務員告訴你,對不起,漢堡要現做,需要等5分鍾,於是你站在收銀台前面等了5分鍾,拿到漢堡再去逛商場,這是同步IO。
你說“來個漢堡”,服務員告訴你,漢堡需要等5分鍾,你可以先去逛商場,等做好了,我們再通知你,這樣你可以立刻去干別的事情(逛商場),這是異步IO。
很明顯,使用異步IO來編寫程序性能會遠遠高於同步IO,但是異步IO的缺點是編程模型復雜。想想看,你得知道什么時候通知你“漢堡做好了”,而通知你的方法也各不相同。如果是服務員跑過來找到你,這是回調模式,如果服務員發短信通知你,你就得不停地檢查手機,這是輪詢模式。總之,異步IO的復雜度遠遠高於同步IO。
操作IO的能力都是由操作系統提供的,每一種編程語言都會把操作系統提供的低級C接口封裝起來方便使用,Python也不例外。
讀寫文件是最常見的IO操作。Python內置了讀寫文件的函數,用法和C是兼容的。
讀寫文件前,我們先必須了解一下,在磁盤上讀寫文件的功能都是由操作系統提供的,現代操作系統不允許普通的程序直接操作磁盤,所以,讀寫文件就是請求操作系統打開一個文件對象(通常稱為文件描述符),然后,通過操作系統提供的接口從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。
2 讀文件
要以讀文件的模式打開一個文件對象,使用Python內置的open()函數,傳入文件名和標示符:
>>> f = open('C:\\Users\\hfqn\\Desktop\\test.txt', 'r')
標示符'r'表示讀,這樣,我們就成功地打開了一個文件。
如果文件不存在,open()函數就會拋出一個IOError的錯誤,並且給出錯誤碼和詳細的信息告訴你文件不存在:
>>> f=open('C:\\Users\\hfqn\\Desktop\\notfound.txt', 'r')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\hfqn\\Desktop\\notfound.txt'
如果文件打開成功,接下來,調用read()方法可以一次讀取文件的全部內容,Python把內容讀到內存,用一個str對象表示:
>>> f.read()
'Hello, world!'
最后一步是調用close()方法關閉文件。文件使用完畢后必須關閉,因為文件對象會占用操作系統的資源,並且操作系統同一時間能打開的文件數量也是有限的:
>>> f.close()
由於文件讀寫時都有可能產生IOError,一旦出錯,后面的f.close()就不會調用。所以,為了保證無論是否出錯都能正確地關閉文件,我們可以使用try ... finally來實現:
try:
f = open('\path\to\file', 'r')
print(f.read())
finally:
if f:
f.close()
但是每次都這么寫實在太繁瑣,所以,Python引入了with語句來自動幫我們調用close()方法:
with open('\path\to\file', 'r') as f:
print(f.read())
這和前面的try ... finally是一樣的,但是代碼更佳簡潔,並且不必調用f.close()方法。
調用read()會一次性讀取文件的全部內容,如果文件有10G,內存就爆了,所以,為保險起見,可以反復調用read(size)方法,每次最多讀取size個字節的內容。
另外,調用readline()可以每次讀取一行內容,調用readlines()一次讀取所有內容並按行返回list。因此,要根據需要決定怎么調用。
如果文件很小,read()一次性讀取最方便;如果不能確定文件大小,反復調用read(size)比較保險;如果是配置文件,調用readlines()最方便:
with open('\path\to\file', 'r') as f:
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'刪掉
前面講的默認都是讀取文本文件,並且是UTF-8編碼的文本文件。要讀取二進制文件,比如圖片、視頻等等,用'rb'模式打開文件即可:
>>> f = open('C:\\Users\\hfqn\\Desktop\\test.jpg', 'rb')
>>> f.read()
b''\x89PNG\r\n\x1a\n\x00\x00\x00...' # 十六進制表示的字節
要讀取非UTF-8編碼的文本文件,需要給open()函數傳入encoding參數,例如,讀取GBK編碼的文件:
>>> f = open('C:\\Users\\hfqn\\Desktop\\gbk.txt','r',encoding='gbk')
>>> f.read()
'人生苦短,我用python'
遇到有些編碼不規范的文件,你可能會遇到UnicodeDecodeError,因為在文本文件中可能夾雜了一些非法編碼的字符。遇到這種情況,open()函數還接收一個errors參數,表示如果遇到編碼錯誤后如何處理。最簡單的方式是直接忽略:
>>> f = open('C:\\Users\\hfqn\\Desktop\\gbk.txt','r',encoding='gbk', errors='ignore')
關於Python的編碼,可參考《Python2與Python3的字符編碼與解碼》:http://www.jianshu.com/p/19c74e76ee0a
3 寫文件
寫文件和讀文件是一樣的,唯一區別是調用open()函數時,傳入標識符'w'或者'wb'表示寫文本文件或寫二進制文件:
>>> f = open('C:\\Users\\hfqn\\Desktop\\test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
你可以反復調用write()來寫入文件,但是務必要調用f.close()來關閉文件。
當我們寫文件時,操作系統往往不會立刻把數據寫入磁盤,而是放到內存緩存起來,空閑的時候再慢慢寫入。只有調用close()方法時,操作系統才保證把沒有寫入的數據全部寫入磁盤。忘記調用close()的后果是數據可能只寫了一部分到磁盤,剩下的丟失了。所以,還是用with語句來得保險:
with open('C:\\Users\\hfqn\\Desktop\\test.txt', 'w') as f:
f.write('Hello, world!')
要寫入特定編碼的文本文件,請給open()函數傳入encoding參數,將字符串自動轉換成指定編碼。
4 StringIO和BytesIO
很多時候,數據讀寫不一定是文件,也可以在內存中讀寫。StringIO就是在內存中讀寫str。
要把str寫入StringIO,我們需要先創建一個StringIO,然后,像文件一樣寫入即可:
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hello world!
getvalue()方法用於獲得寫入后的str。
要讀取StringIO,可以用一個str初始化StringIO,然后,像讀文件一樣讀取:
>>> from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
... s = f.readline()
... if s == '':
... break
... print(s.strip())
Hello!
Hi!
Goodbye!
StringIO操作的只能是str,如果要操作二進制數據,就需要使用BytesIO。
BytesIO實現了在內存中讀寫bytes,我們創建一個BytesIO,然后寫入一些bytes:
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
注意,寫入的不是str,而是經過UTF-8編碼的bytes。
和StringIO類似,可以用一個bytes初始化BytesIO,然后,像讀文件一樣讀取:
>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'
StringIO和BytesIO是在內存中操作str和bytes的方法,使得和讀寫文件具有一致的接口。
如果您發現文中有不清楚或者有問題的地方,請在下方評論區留言,我會根據您的評論,更新文中相關內容,謝謝!