Python文件讀寫、StringIO和BytesIO

1 IO的含義

在計算機中,IO是Input/Output的簡寫,也就是輸入和輸出。

由于程序和運行時數(shù)據(jù)是在內(nèi)存中駐留,由CPU這個超快的計算核心來執(zhí)行,涉及到數(shù)據(jù)交換的地方,通常是磁盤、網(wǎng)絡等,就需要IO接口。

比如你訪問百度首頁,瀏覽器就需要通過網(wǎng)絡IO獲取網(wǎng)頁。瀏覽器先會發(fā)送請求給百度服務器,告訴它想要的html網(wǎng)址,這個動作是往外發(fā)數(shù)據(jù),叫Output。接著百度服務器把網(wǎng)頁的內(nèi)容發(fā)送過來,這個動作是從外面接收數(shù)據(jù),叫Input。

所以,通常,程序完成IO操作會有Input和Output兩個數(shù)據(jù)流。當然也有只用一個的情況,比如,從磁盤讀取文件到內(nèi)存,就只有Input操作,反過來,把數(shù)據(jù)寫到磁盤文件里,就只是一個Output操作。

輸入輸出是相對的,要考慮具體的對象是什么。一般,當我們寫的程序需要讀取磁盤文件時,相當于把磁盤的數(shù)據(jù)輸入到程序中,對于程序來說,讀取的數(shù)據(jù)就屬于Input,但是對于磁盤來說,相當于把數(shù)據(jù)輸出給程序,輸出的數(shù)據(jù)屬于Output。

IO編程中,Stream(流)是一個很重要的概念,可以把流想象成一個水管,數(shù)據(jù)就是水管里的水,但是只能單向流動。Input Stream就是數(shù)據(jù)從外面(磁盤、網(wǎng)絡)流進內(nèi)存,Output Stream就是數(shù)據(jù)從內(nèi)存流到外面去。對于瀏覽網(wǎng)頁來說,瀏覽器和服務器之間至少需要建立兩根水管,才可以既能發(fā)數(shù)據(jù),又能收數(shù)據(jù)。

由于CPU和內(nèi)存的速度遠遠高于外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數(shù)據(jù)寫入磁盤,CPU輸出100M的數(shù)據(jù)只需要0.01秒,可是磁盤要接收這100M數(shù)據(jù)可能需要10秒,怎么辦呢?有兩種辦法:

  • 第一種是CPU等著,也就是程序暫停執(zhí)行后續(xù)代碼,等100M的數(shù)據(jù)在10秒后寫入磁盤,再接著往下執(zhí)行,這種模式稱為同步IO。
  • 另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不著急,我接著干別的事去了”,于是,后續(xù)代碼可以立刻接著執(zhí)行,這種模式稱為異步IO。

同步和異步的區(qū)別就在于是否等待IO執(zhí)行的結果。好比你去麥當勞點餐,你說“來個漢堡”,服務員告訴你,對不起,漢堡要現(xiàn)做,需要等5分鐘,于是你站在收銀臺前面等了5分鐘,拿到漢堡再去逛商場,這是同步IO。

你說“來個漢堡”,服務員告訴你,漢堡需要等5分鐘,你可以先去逛商場,等做好了,我們再通知你,這樣你可以立刻去干別的事情(逛商場),這是異步IO。

很明顯,使用異步IO來編寫程序性能會遠遠高于同步IO,但是異步IO的缺點是編程模型復雜。想想看,你得知道什么時候通知你“漢堡做好了”,而通知你的方法也各不相同。如果是服務員跑過來找到你,這是回調(diào)模式,如果服務員發(fā)短信通知你,你就得不停地檢查手機,這是輪詢模式??傊?,異步IO的復雜度遠遠高于同步IO。

操作IO的能力都是由操作系統(tǒng)提供的,每一種編程語言都會把操作系統(tǒng)提供的低級C接口封裝起來方便使用,Python也不例外。

讀寫文件是最常見的IO操作。Python內(nèi)置了讀寫文件的函數(shù),用法和C是兼容的。

讀寫文件前,我們先必須了解一下,在磁盤上讀寫文件的功能都是由操作系統(tǒng)提供的,現(xiàn)代操作系統(tǒng)不允許普通的程序直接操作磁盤,所以,讀寫文件就是請求操作系統(tǒng)打開一個文件對象(通常稱為文件描述符),然后,通過操作系統(tǒng)提供的接口從這個文件對象中讀取數(shù)據(jù)(讀文件),或者把數(shù)據(jù)寫入這個文件對象(寫文件)。


2 讀文件

要以讀文件的模式打開一個文件對象,使用Python內(nèi)置的open()函數(shù),傳入文件名和標示符:

>>> f = open('C:\\Users\\hfqn\\Desktop\\test.txt', 'r')

標示符'r'表示讀,這樣,我們就成功地打開了一個文件。

如果文件不存在,open()函數(shù)就會拋出一個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'

如果文件打開成功,接下來,調(diào)用read()方法可以一次讀取文件的全部內(nèi)容,Python把內(nèi)容讀到內(nèi)存,用一個str對象表示:

>>> f.read()
'Hello, world!'

最后一步是調(diào)用close()方法關閉文件。文件使用完畢后必須關閉,因為文件對象會占用操作系統(tǒng)的資源,并且操作系統(tǒng)同一時間能打開的文件數(shù)量也是有限的:

>>> f.close()

由于文件讀寫時都有可能產(chǎn)生IOError,一旦出錯,后面的f.close()就不會調(diào)用。所以,為了保證無論是否出錯都能正確地關閉文件,我們可以使用try ... finally來實現(xiàn):

try:
    f = open('\path\to\file', 'r')
    print(f.read())
finally:
    if f:
        f.close()

但是每次都這么寫實在太繁瑣,所以,Python引入了with語句來自動幫我們調(diào)用close()方法:

with open('\path\to\file', 'r') as f:
    print(f.read())

這和前面的try ... finally是一樣的,但是代碼更佳簡潔,并且不必調(diào)用f.close()方法。

調(diào)用read()會一次性讀取文件的全部內(nèi)容,如果文件有10G,內(nèi)存就爆了,所以,為保險起見,可以反復調(diào)用read(size)方法,每次最多讀取size個字節(jié)的內(nèi)容。

另外,調(diào)用readline()可以每次讀取一行內(nèi)容,調(diào)用readlines()一次讀取所有內(nèi)容并按行返回list。因此,要根據(jù)需要決定怎么調(diào)用。

如果文件很小,read()一次性讀取最方便;如果不能確定文件大小,反復調(diào)用read(size)比較保險;如果是配置文件,調(diào)用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...'     # 十六進制表示的字節(jié)

要讀取非UTF-8編碼的文本文件,需要給open()函數(shù)傳入encoding參數(shù),例如,讀取GBK編碼的文件:

>>> f = open('C:\\Users\\hfqn\\Desktop\\gbk.txt','r',encoding='gbk')
>>> f.read()
'人生苦短,我用python'

遇到有些編碼不規(guī)范的文件,你可能會遇到UnicodeDecodeError,因為在文本文件中可能夾雜了一些非法編碼的字符。遇到這種情況,open()函數(shù)還接收一個errors參數(shù),表示如果遇到編碼錯誤后如何處理。最簡單的方式是直接忽略:

>>> f = open('C:\\Users\\hfqn\\Desktop\\gbk.txt','r',encoding='gbk', errors='ignore')

關于Python的編碼,可參考《Python2與Python3的字符編碼與解碼》:http://www.itdecent.cn/p/19c74e76ee0a


3 寫文件

寫文件和讀文件是一樣的,唯一區(qū)別是調(diào)用open()函數(shù)時,傳入標識符'w'或者'wb'表示寫文本文件或?qū)懚M制文件:

>>> f = open('C:\\Users\\hfqn\\Desktop\\test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()

你可以反復調(diào)用write()來寫入文件,但是務必要調(diào)用f.close()來關閉文件。

當我們寫文件時,操作系統(tǒng)往往不會立刻把數(shù)據(jù)寫入磁盤,而是放到內(nèi)存緩存起來,空閑的時候再慢慢寫入。只有調(diào)用close()方法時,操作系統(tǒng)才保證把沒有寫入的數(shù)據(jù)全部寫入磁盤。忘記調(diào)用close()的后果是數(shù)據(jù)可能只寫了一部分到磁盤,剩下的丟失了。所以,還是用with語句來得保險:

with open('C:\\Users\\hfqn\\Desktop\\test.txt', 'w') as f:
    f.write('Hello, world!')

要寫入特定編碼的文本文件,請給open()函數(shù)傳入encoding參數(shù),將字符串自動轉(zhuǎn)換成指定編碼。


4 StringIO和BytesIO

很多時候,數(shù)據(jù)讀寫不一定是文件,也可以在內(nèi)存中讀寫。StringIO就是在內(nèi)存中讀寫str。

要把str寫入StringIO,我們需要先創(chuàng)建一個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,然后,像讀文件一樣讀?。?/p>

>>> 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,如果要操作二進制數(shù)據(jù),就需要使用BytesIO。

BytesIO實現(xiàn)了在內(nèi)存中讀寫bytes,我們創(chuàng)建一個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,而是經(jīng)過UTF-8編碼的bytes。

和StringIO類似,可以用一個bytes初始化BytesIO,然后,像讀文件一樣讀?。?/p>

>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

StringIO和BytesIO是在內(nèi)存中操作str和bytes的方法,使得和讀寫文件具有一致的接口。


如果您發(fā)現(xiàn)文中有不清楚或者有問題的地方,請在下方評論區(qū)留言,我會根據(jù)您的評論,更新文中相關內(nèi)容,謝謝!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,802評論 11 349
  • 接下來的更新有些特別,早期的虛擬現(xiàn)實代表《 Jerry’s Place VR 》將會引入 VR 聊天模塊。 《 J...
    ty晌午之月閱讀 524評論 0 0
  • 有了傅里葉變換,為什么又要來一個拉普拉斯變換?又既然是信號與系統(tǒng),就要從兩個方面加以敘述。 ①對信號來說,等幅的正...
    夏夜明閱讀 2,640評論 0 1
  • 昨天莫名其妙收到菜鳥的短信,說有個中通快件。首先思考了一下自己最近沒買東西,然后心里小鹿就開始亂撞了:哎呀會不會又...
    圈圈又青青閱讀 228評論 0 0

友情鏈接更多精彩內(nèi)容