所有程序都要處理輸入和輸出。 這一章將涵蓋處理不同類(lèi)型的文件,包括文本和二進(jìn)制文件,文件編碼和其他相關(guān)的內(nèi)容。 對(duì)文件名和目錄的操作也會(huì)涉及到。
你需要讀寫(xiě)各種不同編碼的文本數(shù)據(jù),比如 ASCII,UTF-8 或 UTF-16 編碼等。
使用帶有 rt
模式的 open()
函數(shù)讀取文本文件。如下所示:
# Read the entire file as a single string
with open('somefile.txt', 'rt') as f:
data = f.read()
# Iterate over the lines of the file
with open('somefile.txt', 'rt') as f:
for line in f:
# process line
...
類(lèi)似的,為了寫(xiě)入一個(gè)文本文件,使用帶有 wt
模式的 open()
函數(shù), 如果之前文件內(nèi)容存在則清除并覆蓋掉。如下所示:
# Write chunks of text data
with open('somefile.txt', 'wt') as f:
f.write(text1)
f.write(text2)
...
# Redirected print statement
with open('somefile.txt', 'wt') as f:
print(line1, file=f)
print(line2, file=f)
...
如果是在已存在文件中添加內(nèi)容,使用模式為at
的open()
函數(shù)。
文件的讀寫(xiě)操作默認(rèn)使用系統(tǒng)編碼,可以通過(guò)調(diào)用 sys.getdefaultencoding()
來(lái)得到。 在大多數(shù)機(jī)器上面都是 utf-8 編碼。如果你已經(jīng)知道你要讀寫(xiě)的文本是其他編碼方式, 那么可以通過(guò)傳遞一個(gè)可選的 encoding
參數(shù)給 open()函數(shù)。如下所示:
with open('somefile.txt', 'rt', encoding='latin-1') as f:
...
Python 支持非常多的文本編碼。幾個(gè)常見(jiàn)的編碼是 ascii, latin-1, utf-8 和 utf-16。 在 web 應(yīng)用程序中通常都使用的是 UTF-8。 ascii 對(duì)應(yīng)從 U+0000 到 U+007F 范圍內(nèi)的7位字符。 latin-1 是字節(jié)0-255到 U+0000 至 U+00FF 范圍內(nèi) Unicode字 符的直接映射。 當(dāng)讀取一個(gè)未知編碼的文本時(shí)使用 latin-1 編碼永遠(yuǎn)不會(huì)產(chǎn)生解碼錯(cuò)誤。 使用 latin-1 編碼讀取一個(gè)文件的時(shí)候也許不能產(chǎn)生完全正確的文本解碼數(shù)據(jù), 但是它也能從中提取出足夠多的有用數(shù)據(jù)。同時(shí),如果你之后將數(shù)據(jù)回寫(xiě)回去,原先的數(shù)據(jù)還是會(huì)保留的。
讀寫(xiě)文本文件一般來(lái)講是比較簡(jiǎn)單的。但是也幾點(diǎn)是需要注意的。 首先,在例子程序中的 with 語(yǔ)句給被使用到的文件創(chuàng)建了一個(gè)上下文環(huán)境, 但 with
控制塊結(jié)束時(shí),文件會(huì)自動(dòng)關(guān)閉。你也可以不使用 with
語(yǔ)句,但是這時(shí)候你就必須記得手動(dòng)關(guān)閉文件:
f = open('somefile.txt', 'rt')
data = f.read()
f.close()
另外一個(gè)問(wèn)題是關(guān)于換行符的識(shí)別問(wèn)題,在 Unix 和 Windows 中是不一樣的(分別是 n 和 rn)。 默認(rèn)情況下,Python 會(huì)以統(tǒng)一模式處理?yè)Q行符。 這種模式下,在讀取文本的時(shí)候,Python 可以識(shí)別所有的普通換行符并將其轉(zhuǎn)換為單個(gè)\n
字符。 類(lèi)似的,在輸出時(shí)會(huì)將換行符\n
轉(zhuǎn)換為系統(tǒng)默認(rèn)的換行符。 如果你不希望這種默認(rèn)的處理方式,可以給open()
函數(shù)傳入?yún)?shù)newline=''
,就像下面這樣:
# Read with disabled newline translation
with open('somefile.txt', 'rt', newline='') as f:
...
為了說(shuō)明兩者之間的差異,下面我在 Unix 機(jī)器上面讀取一個(gè) Windows 上面的文本文件,里面的內(nèi)容是 hello world!\r\n
:
>>> # Newline translation enabled (the default)
>>> f = open('hello.txt', 'rt')
>>> f.read()
'hello world!\n'
>>> # Newline translation disabled
>>> g = open('hello.txt', 'rt', newline='')
>>> g.read()
'hello world!\r\n'
>>>
最后一個(gè)問(wèn)題就是文本文件中可能出現(xiàn)的編碼錯(cuò)誤。 但你讀取或者寫(xiě)入一個(gè)文本文件時(shí),你可能會(huì)遇到一個(gè)編碼或者解碼錯(cuò)誤。比如:
>>> f = open('sample.txt', 'rt', encoding='ascii')
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.3/encodings/ascii.py", line 26, in decode
return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position
12: ordinal not in range(128)
>>>
如果出現(xiàn)這個(gè)錯(cuò)誤,通常表示你讀取文本時(shí)指定的編碼不正確。 你最好仔細(xì)閱讀說(shuō)明并確認(rèn)你的文件編碼是正確的(比如使用 UTF-8 而不是 Latin-1 編碼或其他)。 如果編碼錯(cuò)誤還是存在的話(huà),你可以給 open()
函數(shù)傳遞一個(gè)可選的errors
參數(shù)來(lái)處理這些錯(cuò)誤。 下面是一些處理常見(jiàn)錯(cuò)誤的方法:
>>> # Replace bad chars with Unicode U+fffd replacement char
>>> f = open('sample.txt', 'rt', encoding='ascii', errors='replace')
>>> f.read()
'Spicy Jalape?o!'
>>> # Ignore bad chars entirely
>>> g = open('sample.txt', 'rt', encoding='ascii', errors='ignore')
>>> g.read()
'Spicy Jalapeo!'
>>>
如果你經(jīng)常使用 errors
參數(shù)來(lái)處理編碼錯(cuò)誤,可能會(huì)讓你的生活變得很糟糕。 對(duì)于文本處理的首要原則是確保你總是使用的是正確編碼。當(dāng)模棱兩可的時(shí)候,就使用默認(rèn)的設(shè)置(通常都是 UTF-8)。
你想將 print()
函數(shù)的輸出重定向到一個(gè)文件中去。
在 print()
函數(shù)中指定 file
關(guān)鍵字參數(shù),像下面這樣:
with open('d:/work/test.txt', 'wt') as f:
print('Hello World!', file=f)
關(guān)于輸出重定向到文件中就這些了。但是有一點(diǎn)要注意的就是文件必須是以文本模式打開(kāi)。 如果文件是二進(jìn)制模式的話(huà),打印就會(huì)出錯(cuò)。
你想使用 print()
函數(shù)輸出數(shù)據(jù),但是想改變默認(rèn)的分隔符或者行尾符。
可以使用在 print()
函數(shù)中使用 sep
和 end
關(guān)鍵字參數(shù),以你想要的方式輸出。比如:
>>> print('ACME', 50, 91.5)
ACME 50 91.5
>>> print('ACME', 50, 91.5, sep=',')
ACME,50,91.5
>>> print('ACME', 50, 91.5, sep=',', end='!!\n')
ACME,50,91.5!!
>>>
使用 end
參數(shù)也可以在輸出中禁止換行。比如:
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
>>> for i in range(5):
... print(i, end=' ')
...
0 1 2 3 4 >>>
當(dāng)你想使用非空格分隔符來(lái)輸出數(shù)據(jù)的時(shí)候,給 print()
函數(shù)傳遞一個(gè) seq
參數(shù)是最簡(jiǎn)單的方案。 有時(shí)候你會(huì)看到一些程序員會(huì)使用 str.join()
來(lái)完成同樣的事情。比如:
>>> print(','.join('ACME','50','91.5'))
ACME,50,91.5
>>>
str.join()
的問(wèn)題在于它僅僅適用于字符串。這意味著你通常需要執(zhí)行另外一些轉(zhuǎn)換才能讓它正常工作。比如:
>>> row = ('ACME', 50, 91.5)
>>> print(','.join(row))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence item 1: expected str instance, int found
>>> print(','.join(str(x) for x in row))
ACME,50,91.5
>>>
你當(dāng)然可以不用那么麻煩,僅僅只需要像下面這樣寫(xiě):
>>> print(*row, sep=',')
ACME,50,91.5
>>>
你想讀寫(xiě)二進(jìn)制文件,比如圖片,聲音文件等等。
使用模式為 rb
或 wb
的 open()
函數(shù)來(lái)讀取或?qū)懭攵M(jìn)制數(shù)據(jù)。比如:
# Read the entire file as a single byte string
with open('somefile.bin', 'rb') as f:
data = f.read()
# Write binary data to a file
with open('somefile.bin', 'wb') as f:
f.write(b'Hello World')
在讀取二進(jìn)制數(shù)據(jù)時(shí),需要指明的是所有返回的數(shù)據(jù)都是字節(jié)字符串格式的,而不是文本字符串。 類(lèi)似的,在寫(xiě)入的時(shí)候,必須保證參數(shù)是以字節(jié)形式對(duì)外暴露數(shù)據(jù)的對(duì)象(比如字節(jié)字符串,字節(jié)數(shù)組對(duì)象等)。
在讀取二進(jìn)制數(shù)據(jù)的時(shí)候,字節(jié)字符串和文本字符串的語(yǔ)義差異可能會(huì)導(dǎo)致一個(gè)潛在的陷阱。 特別需要注意的是,索引和迭代動(dòng)作返回的是字節(jié)的值而不是字節(jié)字符串。比如:
>>> # Text string
>>> t = 'Hello World'
>>> t[0]
'H'
>>> for c in t:
... print(c)
...
H
e
l
l
o
...
>>> # Byte string
>>> b = b'Hello World'
>>> b[0]
72
>>> for c in b:
... print(c)
...
72
101
108
108
111
...
>>>
如果你想從二進(jìn)制模式的文件中讀取或?qū)懭胛谋緮?shù)據(jù),必須確保要進(jìn)行解碼和編碼操作。比如:
with open('somefile.bin', 'rb') as f:
data = f.read(16)
text = data.decode('utf-8')
with open('somefile.bin', 'wb') as f:
text = 'Hello World'
f.write(text.encode('utf-8'))
二進(jìn)制 I/O 還有一個(gè)鮮為人知的特性就是數(shù)組和C結(jié)構(gòu)體類(lèi)型能直接被寫(xiě)入,而不需要中間轉(zhuǎn)換為自己對(duì)象。比如:
import array
nums = array.array('i', [1, 2, 3, 4])
with open('data.bin','wb') as f:
f.write(nums)
這個(gè)適用于任何實(shí)現(xiàn)了被稱(chēng)之為”緩沖接口”的對(duì)象,這種對(duì)象會(huì)直接暴露其底層的內(nèi)存緩沖區(qū)給能處理它的操作。 二進(jìn)制數(shù)據(jù)的寫(xiě)入就是這類(lèi)操作之一。
很多對(duì)象還允許通過(guò)使用文件對(duì)象的 readinto()
方法直接讀取二進(jìn)制數(shù)據(jù)到其底層的內(nèi)存中去。比如:
>>> import array
>>> a = array.array('i', [0, 0, 0, 0, 0, 0, 0, 0])
>>> with open('data.bin', 'rb') as f:
... f.readinto(a)
...
16
>>> a
array('i', [1, 2, 3, 4, 0, 0, 0, 0])
>>>
但是使用這種技術(shù)的時(shí)候需要格外小心,因?yàn)樗ǔ>哂衅脚_(tái)相關(guān)性,并且可能會(huì)依賴(lài)字長(zhǎng)和字節(jié)順序(高位優(yōu)先和低位優(yōu)先)。 可以查看5.9小節(jié)中另外一個(gè)讀取二進(jìn)制數(shù)據(jù)到可修改緩沖區(qū)的例子。
你想像一個(gè)文件中寫(xiě)入數(shù)據(jù),但是前提必須是這個(gè)文件在文件系統(tǒng)上不存在。 也就是不允許覆蓋已存在的文件內(nèi)容。
可以在 open()
函數(shù)中使用 x
模式來(lái)代替 w
模式的方法來(lái)解決這個(gè)問(wèn)題。比如:
>>> with open('somefile', 'wt') as f:
... f.write('Hello\n')
...
>>> with open('somefile', 'xt') as f:
... f.write('Hello\n')
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileExistsError: [Errno 17] File exists: 'somefile'
>>>
如果文件是二進(jìn)制的,使用 xb
來(lái)代替 xt
這一小節(jié)演示了在寫(xiě)文件時(shí)通常會(huì)遇到的一個(gè)問(wèn)題的完美解決方案(不小心覆蓋一個(gè)已存在的文件)。 一個(gè)替代方案是先測(cè)試這個(gè)文件是否存在,像下面這樣:
>>> import os
>>> if not os.path.exists('somefile'):
... with open('somefile', 'wt') as f:
... f.write('Hello\n')
... else:
... print('File already exists!')
...
File already exists!
>>>
顯而易見(jiàn),使用 x 文件模式更加簡(jiǎn)單。要注意的是 x 模式是一個(gè) Python3 對(duì) open()
函數(shù)特有的擴(kuò)展。 在 Python 的舊版本或者是 Python 實(shí)現(xiàn)的底層 C 函數(shù)庫(kù)中都是沒(méi)有這個(gè)模式的。
你想使用操作類(lèi)文件對(duì)象的程序來(lái)操作文本或二進(jìn)制字符串。
使用 io.StringIO()
和 io.BytesIO()
類(lèi)來(lái)創(chuàng)建類(lèi)文件對(duì)象操作字符串?dāng)?shù)據(jù)。比如:
>>> s = io.StringIO()
>>> s.write('Hello World\n')
12
>>> print('This is a test', file=s)
15
>>> # Get all of the data written so far
>>> s.getvalue()
'Hello World\nThis is a test\n'
>>>
>>> # Wrap a file interface around an existing string
>>> s = io.StringIO('Hello\nWorld\n')
>>> s.read(4)
'Hell'
>>> s.read()
'o\nWorld\n'
>>>
io.StringIO
只能用于文本。如果你要操作二進(jìn)制數(shù)據(jù),要使用 io.BytesIO
類(lèi)來(lái)代替。比如:
>>> s = io.BytesIO()
>>> s.write(b'binary data')
>>> s.getvalue()
b'binary data'
>>>
當(dāng)你想模擬一個(gè)普通的文件的時(shí)候 StringIO
和 BytesIO
類(lèi)是很有用的。 比如,在單元測(cè)試中,你可以使用 StringIO
來(lái)創(chuàng)建一個(gè)包含測(cè)試數(shù)據(jù)的類(lèi)文件對(duì)象, 這個(gè)對(duì)象可以被傳給某個(gè)參數(shù)為普通文件對(duì)象的函數(shù)。
需要注意的是, StringIO
和 BytesIO
實(shí)例并沒(méi)有正確的整數(shù)類(lèi)型的文件描述符。 因此,它們不能在那些需要使用真實(shí)的系統(tǒng)級(jí)文件如文件,管道或者是套接字的程序中使用。
你想讀寫(xiě)一個(gè) gzip 或 bz2 格式的壓縮文件。
gzip
和 bz2
模塊可以很容易的處理這些文件。 兩個(gè)模塊都為 open()
函數(shù)提供了另外的實(shí)現(xiàn)來(lái)解決這個(gè)問(wèn)題。 比如,為了以文本形式讀取壓縮文件,可以這樣做:
# gzip compression
import gzip
with gzip.open('somefile.gz', 'rt') as f:
text = f.read()
# bz2 compression
import bz2
with bz2.open('somefile.bz2', 'rt') as f:
text = f.read()
類(lèi)似的,為了寫(xiě)入壓縮數(shù)據(jù),可以這樣做:
# gzip compression
import gzip
with gzip.open('somefile.gz', 'wt') as f:
f.write(text)
# bz2 compression
import bz2
with bz2.open('somefile.bz2', 'wt') as f:
f.write(text)
如上,所有的 I/O 操作都使用文本模式并執(zhí)行 Unicode 的編碼/解碼。 類(lèi)似的,如果你想操作二進(jìn)制數(shù)據(jù),使用 rb
或者 wb
文件模式即可。
大部分情況下讀寫(xiě)壓縮數(shù)據(jù)都是很簡(jiǎn)單的。但是要注意的是選擇一個(gè)正確的文件模式是非常重要的。 如果你不指定模式,那么默認(rèn)的就是二進(jìn)制模式,如果這時(shí)候程序想要接受的是文本數(shù)據(jù),那么就會(huì)出錯(cuò)。 gzip.open()
和 bz2.open()
接受跟內(nèi)置的 open()
函數(shù)一樣的參數(shù), 包括 encoding
,errors
,newline
等等。
當(dāng)寫(xiě)入壓縮數(shù)據(jù)時(shí),可以使用compresslevel
這個(gè)可選的關(guān)鍵字參數(shù)來(lái)指定一個(gè)壓縮級(jí)別。比如:
with gzip.open('somefile.gz', 'wt', compresslevel=5) as f:
f.write(text)
默認(rèn)的等級(jí)是9,也是最高的壓縮等級(jí)。等級(jí)越低性能越好,但是數(shù)據(jù)壓縮程度也越低。
最后一點(diǎn), gzip.open()
和 bz2.open()
還有一個(gè)很少被知道的特性, 它們可以作用在一個(gè)已存在并以二進(jìn)制模式打開(kāi)的文件上。比如,下面代碼是可行的:
import gzip
f = open('somefile.gz', 'rb')
with gzip.open(f, 'rt') as g:
text = g.read()
這樣就允許gzip
和bz2
模塊可以工作在許多類(lèi)文件對(duì)象上,比如套接字,管道和內(nèi)存中文件等。
你想在一個(gè)固定長(zhǎng)度記錄或者數(shù)據(jù)塊的集合上迭代,而不是在一個(gè)文件中一行一行的迭代。
通過(guò)下面這個(gè)小技巧使用 iter
和 functools.partial()
函數(shù):
from functools import partial
RECORD_SIZE = 32
with open('somefile.data', 'rb') as f:
records = iter(partial(f.read, RECORD_SIZE), b'')
for r in records:
...
這個(gè)例子中的records
對(duì)象是一個(gè)可迭代對(duì)象,它會(huì)不斷的產(chǎn)生固定大小的數(shù)據(jù)塊,直到文件末尾。 要注意的是如果總記錄大小不是塊大小的整數(shù)倍的話(huà),最后一個(gè)返回元素的字節(jié)數(shù)會(huì)比期望值少。
iter()
函數(shù)有一個(gè)鮮為人知的特性就是,如果你給它傳遞一個(gè)可調(diào)用對(duì)象和一個(gè)標(biāo)記值,它會(huì)創(chuàng)建一個(gè)迭代器。 這個(gè)迭代器會(huì)一直調(diào)用傳入的可調(diào)用對(duì)象直到它返回標(biāo)記值為止,這時(shí)候迭代終止。
在例子中, functools.partial
用來(lái)創(chuàng)建一個(gè)每次被調(diào)用時(shí)從文件中讀取固定數(shù)目字節(jié)的可調(diào)用對(duì)象。 標(biāo)記值b''
就是當(dāng)?shù)竭_(dá)文件結(jié)尾時(shí)的返回值。
最后再提一點(diǎn),上面的例子中的文件時(shí)以二進(jìn)制模式打開(kāi)的。 如果是讀取固定大小的記錄,這通常是最普遍的情況。 而對(duì)于文本文件,一行一行的讀取(默認(rèn)的迭代行為)更普遍點(diǎn)。
你想直接讀取二進(jìn)制數(shù)據(jù)到一個(gè)可變緩沖區(qū)中,而不需要做任何的中間復(fù)制操作。 或者你想原地修改數(shù)據(jù)并將它寫(xiě)回到一個(gè)文件中去。
為了讀取數(shù)據(jù)到一個(gè)可變數(shù)組中,使用文件對(duì)象的 readinto()
方法。比如:
import os.path
def read_into_buffer(filename):
buf = bytearray(os.path.getsize(filename))
with open(filename, 'rb') as f:
f.readinto(buf)
return buf
下面是一個(gè)演示這個(gè)函數(shù)使用方法的例子:
>>> # Write a sample file
>>> with open('sample.bin', 'wb') as f:
... f.write(b'Hello World')
...
>>> buf = read_into_buffer('sample.bin')
>>> buf
bytearray(b'Hello World')
>>> buf[0:5] = b'Hallo'
>>> buf
bytearray(b'Hallo World')
>>> with open('newsample.bin', 'wb') as f:
... f.write(buf)
...
11
>>>
文件對(duì)象的 readinto()
方法能被用來(lái)為預(yù)先分配內(nèi)存的數(shù)組填充數(shù)據(jù),甚至包括由 array
模塊或numpy
庫(kù)創(chuàng)建的數(shù)組。 和普通 read()
方法不同的是, readinto()
填充已存在的緩沖區(qū)而不是為新對(duì)象重新分配內(nèi)存再返回它們。 因此,你可以使用它來(lái)避免大量的內(nèi)存分配操作。 比如,如果你讀取一個(gè)由相同大小的記錄組成的二進(jìn)制文件時(shí),你可以像下面這樣寫(xiě):
record_size = 32 # Size of each record (adjust value)
buf = bytearray(record_size)
with open('somefile', 'rb') as f:
while True:
n = f.readinto(buf)
if n < record_size:
break
# Use the contents of buf
...
另外有一個(gè)有趣特性就是 memoryview
, 它可以通過(guò)零復(fù)制的方式對(duì)已存在的緩沖區(qū)執(zhí)行切片操作,甚至還能修改它的內(nèi)容。比如:
>>> buf
bytearray(b'Hello World')
>>> m1 = memoryview(buf)
>>> m2 = m1[-5:]
>>> m2
<memory at 0x100681390>
>>> m2[:] = b'WORLD'
>>> buf
bytearray(b'Hello WORLD')
>>>
使用 f.readinto()
時(shí)需要注意的是,你必須檢查它的返回值,也就是實(shí)際讀取的字節(jié)數(shù)。
如果字節(jié)數(shù)小于緩沖區(qū)大小,表明數(shù)據(jù)被截?cái)嗷蛘弑黄茐牧?比如你期望每次讀取指定數(shù)量的字節(jié))。
最后,留心觀察其他函數(shù)庫(kù)和模塊中和 into
相關(guān)的函數(shù)(比如 recv_into()
, pack_into()
等)。 Python 的很多其他部分已經(jīng)能支持直接的 I/O 或數(shù)據(jù)訪問(wèn)操作,這些操作可被用來(lái)填充或修改數(shù)組和緩沖區(qū)內(nèi)容。
關(guān)于解析二進(jìn)制結(jié)構(gòu)和memoryviews
使用方法的更高級(jí)例子,請(qǐng)參考6.12小節(jié)。
你想內(nèi)存映射一個(gè)二進(jìn)制文件到一個(gè)可變字節(jié)數(shù)組中,目的可能是為了隨機(jī)訪問(wèn)它的內(nèi)容或者是原地做些修改。
使用 mmap
模塊來(lái)內(nèi)存映射文件。 下面是一個(gè)工具函數(shù),向你演示了如何打開(kāi)一個(gè)文件并以一種便捷方式內(nèi)存映射這個(gè)文件。
import os
import mmap
def memory_map(filename, access=mmap.ACCESS_WRITE):
size = os.path.getsize(filename)
fd = os.open(filename, os.O_RDWR)
return mmap.mmap(fd, size, access=access)
為了使用這個(gè)函數(shù),你需要有一個(gè)已創(chuàng)建并且內(nèi)容不為空的文件。 下面是一個(gè)例子,教你怎樣初始創(chuàng)建一個(gè)文件并將其內(nèi)容擴(kuò)充到指定大?。?/p>
>>> size = 1000000
>>> with open('data', 'wb') as f:
... f.seek(size-1)
... f.write(b'\x00')
...
>>>
下面是一個(gè)利用 memory_map()
函數(shù)類(lèi)內(nèi)存映射文件內(nèi)容的例子:
>>> m = memory_map('data')
>>> len(m)
1000000
>>> m[0:10]
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> m[0]
0
>>> # Reassign a slice
>>> m[0:11] = b'Hello World'
>>> m.close()
>>> # Verify that changes were made
>>> with open('data', 'rb') as f:
... print(f.read(11))
...
b'Hello World'
>>>
mmap()
返回的 mmap
對(duì)象同樣也可以作為一個(gè)上下文管理器來(lái)使用, 這時(shí)候底層的文件會(huì)被自動(dòng)關(guān)閉。比如:
>>> with memory_map('data') as m:
... print(len(m))
... print(m[0:10])
...
1000000
b'Hello World'
>>> m.closed
True
>>>
默認(rèn)情況下, memeory_map()
函數(shù)打開(kāi)的文件同時(shí)支持讀和寫(xiě)操作。 任何的修改內(nèi)容都會(huì)復(fù)制回原來(lái)的文件中。 如果需要只讀的訪問(wèn)模式,可以給參數(shù) access
賦值為 mmap.ACCESS_READ
。比如:
m = memory_map(filename, mmap.ACCESS_READ)
如果你想在本地修改數(shù)據(jù),但是又不想將修改寫(xiě)回到原始文件中,可以使用 mmap.ACCESS_COPY
:
m = memory_map(filename, mmap.ACCESS_COPY)
為了隨機(jī)訪問(wèn)文件的內(nèi)容,使用 mmap
將文件映射到內(nèi)存中是一個(gè)高效和優(yōu)雅的方法。 例如,你無(wú)需打開(kāi)一個(gè)文件并執(zhí)行大量的 seek()
, read()
, write()
調(diào)用, 只需要簡(jiǎn)單的映射文件并使用切片操作訪問(wèn)數(shù)據(jù)即可。
一般來(lái)講, mmap()
所暴露的內(nèi)存看上去就是一個(gè)二進(jìn)制數(shù)組對(duì)象。 但是,你可以使用一個(gè)內(nèi)存視圖來(lái)解析其中的數(shù)據(jù)。比如:
>>> m = memory_map('data')
>>> # Memoryview of unsigned integers
>>> v = memoryview(m).cast('I')
>>> v[0] = 7
>>> m[0:4]
b'\x07\x00\x00\x00'
>>> m[0:4] = b'\x07\x01\x00\x00'
>>> v[0]
263
>>>
需要強(qiáng)調(diào)的一點(diǎn)是,內(nèi)存映射一個(gè)文件并不會(huì)導(dǎo)致整個(gè)文件被讀取到內(nèi)存中。 也就是說(shuō),文件并沒(méi)有被復(fù)制到內(nèi)存緩存或數(shù)組中。相反,操作系統(tǒng)僅僅為文件內(nèi)容保留了一段虛擬內(nèi)存。 當(dāng)你訪問(wèn)文件的不同區(qū)域時(shí),這些區(qū)域的內(nèi)容才根據(jù)需要被讀取并映射到內(nèi)存區(qū)域中。 而那些從沒(méi)被訪問(wèn)到的部分還是留在磁盤(pán)上。所有這些過(guò)程是透明的,在幕后完成!
如果多個(gè) Python 解釋器內(nèi)存映射同一個(gè)文件,得到的 mmap
對(duì)象能夠被用來(lái)在解釋器直接交換數(shù)據(jù)。 也就是說(shuō),所有解釋器都能同時(shí)讀寫(xiě)數(shù)據(jù),并且其中一個(gè)解釋器所做的修改會(huì)自動(dòng)呈現(xiàn)在其他解釋器中。 很明顯,這里需要考慮同步的問(wèn)題。但是這種方法有時(shí)候可以用來(lái)在管道或套接字間傳遞數(shù)據(jù)。
這一小節(jié)中函數(shù)盡量寫(xiě)得很通用,同時(shí)適用于 Unix 和 Windows 平臺(tái)。 要注意的是使用 mmap()
函數(shù)時(shí)會(huì)在底層有一些平臺(tái)的差異性。 另外,還有一些選項(xiàng)可以用來(lái)創(chuàng)建匿名的內(nèi)存映射區(qū)域。 如果你對(duì)這個(gè)感興趣,確保你仔細(xì)研讀了 Python 文檔中這方面的內(nèi)容。
你需要使用路徑名來(lái)獲取文件名,目錄名,絕對(duì)路徑等等。
使用 os.path
模塊中的函數(shù)來(lái)操作路徑名。 下面是一個(gè)交互式例子來(lái)演示一些關(guān)鍵的特性:
>>> import os
>>> path = '/Users/beazley/Data/data.csv'
>>> # Get the last component of the path
>>> os.path.basename(path)
'data.csv'
>>> # Get the directory name
>>> os.path.dirname(path)
'/Users/beazley/Data'
>>> # Join path components together
>>> os.path.join('tmp', 'data', os.path.basename(path))
'tmp/data/data.csv'
>>> # Expand the user's home directory
>>> path = '~/Data/data.csv'
>>> os.path.expanduser(path)
'/Users/beazley/Data/data.csv'
>>> # Split the file extension
>>> os.path.splitext(path)
('~/Data/data', '.csv')
>>>
對(duì)于任何的文件名的操作,你都應(yīng)該使用 os.path
模塊,而不是使用標(biāo)準(zhǔn)字符串操作來(lái)構(gòu)造自己的代碼。 特別是為了可移植性考慮的時(shí)候更應(yīng)如此, 因?yàn)?os.path
模塊知道 Unix 和 Windows 系統(tǒng)之間的差異并且能夠可靠地處理類(lèi)似 Data/data.csv
和 Data\data.csv
這樣的文件名。 其次,你真的不應(yīng)該浪費(fèi)時(shí)間去重復(fù)造輪子。通常最好是直接使用已經(jīng)為你準(zhǔn)備好的功能。
要注意的是 os.path
還有更多的功能在這里并沒(méi)有列舉出來(lái)。 可以查閱官方文檔來(lái)獲取更多與文件測(cè)試,符號(hào)鏈接等相關(guān)的函數(shù)說(shuō)明。
你想測(cè)試一個(gè)文件或目錄是否存在。
使用 os.path
模塊來(lái)測(cè)試一個(gè)文件或目錄是否存在。比如:
>>> import os
>>> os.path.exists('/etc/passwd')
True
>>> os.path.exists('/tmp/spam')
False
>>>
你還能進(jìn)一步測(cè)試這個(gè)文件時(shí)什么類(lèi)型的。 在下面這些測(cè)試中,如果測(cè)試的文件不存在的時(shí)候,結(jié)果都會(huì)返回 False:
>>> # Is a regular file
>>> os.path.isfile('/etc/passwd')
True
>>> # Is a directory
>>> os.path.isdir('/etc/passwd')
False
>>> # Is a symbolic link
>>> os.path.islink('/usr/local/bin/python3')
True
>>> # Get the file linked to
>>> os.path.realpath('/usr/local/bin/python3')
'/usr/local/bin/python3.3'
>>>
如果你還想獲取元數(shù)據(jù)(比如文件大小或者是修改日期),也可以使用 os.path
模塊來(lái)解決:
>>> os.path.getsize('/etc/passwd')
3669
>>> os.path.getmtime('/etc/passwd')
1272478234.0
>>> import time
>>> time.ctime(os.path.getmtime('/etc/passwd'))
'Wed Apr 28 13:10:34 2010'
>>>
使用 os.path
來(lái)進(jìn)行文件測(cè)試是很簡(jiǎn)單的。 在寫(xiě)這些腳本時(shí),可能唯一需要注意的就是你需要考慮文件權(quán)限的問(wèn)題,特別是在獲取元數(shù)據(jù)時(shí)候。比如:
>>> os.path.getsize('/Users/guido/Desktop/foo.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.3/genericpath.py", line 49, in getsize
return os.stat(filename).st_size
PermissionError: [Errno 13] Permission denied: '/Users/guido/Desktop/foo.txt'
>>>
你想獲取文件系統(tǒng)中某個(gè)目錄下的所有文件列表。
使用 os.listdir()
函數(shù)來(lái)獲取某個(gè)目錄中的文件列表:
import os
names = os.listdir('somedir')
結(jié)果會(huì)返回目錄中所有文件列表,包括所有文件,子目錄,符號(hào)鏈接等等。 如果你需要通過(guò)某種方式過(guò)濾數(shù)據(jù),可以考慮結(jié)合os.path
庫(kù)中的一些函數(shù)來(lái)使用列表推導(dǎo)。比如:
import os.path
# Get all regular files
names = [name for name in os.listdir('somedir')
if os.path.isfile(os.path.join('somedir', name))]
# Get all dirs
dirnames = [name for name in os.listdir('somedir')
if os.path.isdir(os.path.join('somedir', name))]
字符串的startswith()
和 endswith()
方法對(duì)于過(guò)濾一個(gè)目錄的內(nèi)容也是很有用的。比如:
pyfiles = [name for name in os.listdir('somedir')
if name.endswith('.py')]
對(duì)于文件名的匹配,你可能會(huì)考慮使用 glob
或 fnmatch
模塊。比如:
import glob
pyfiles = glob.glob('somedir/*.py')
from fnmatch import fnmatch
pyfiles = [name for name in os.listdir('somedir')
if fnmatch(name, '*.py')]
獲取目錄中的列表是很容易的,但是其返回結(jié)果只是目錄中實(shí)體名列表而已。 如果你還想獲取其他的元信息,比如文件大小,修改時(shí)間等等, 你或許還需要使用到os.path
模塊中的函數(shù)或著 os.stat()
函數(shù)來(lái)收集數(shù)據(jù)。比如:
# Example of getting a directory listing
import os
import os.path
import glob
pyfiles = glob.glob('*.py')
# Get file sizes and modification dates
name_sz_date = [(name, os.path.getsize(name), os.path.getmtime(name))
for name in pyfiles]
for name, size, mtime in name_sz_date:
print(name, size, mtime)
# Alternative: Get file metadata
file_metadata = [(name, os.stat(name)) for name in pyfiles]
for name, meta in file_metadata:
print(name, meta.st_size, meta.st_mtime)
最后還有一點(diǎn)要注意的就是,有時(shí)候在處理文件名編碼問(wèn)題時(shí)候可能會(huì)出現(xiàn)一些問(wèn)題。 通常來(lái)講,函數(shù) os.listdir()
返回的實(shí)體列表會(huì)根據(jù)系統(tǒng)默認(rèn)的文件名編碼來(lái)解碼。 但是有時(shí)候也會(huì)碰到一些不能正常解碼的文件名。 關(guān)于文件名的處理問(wèn)題,在5.14和5.15小節(jié)有更詳細(xì)的講解。
你想使用原始文件名執(zhí)行文件的 I/O 操作,也就是說(shuō)文件名并沒(méi)有經(jīng)過(guò)系統(tǒng)默認(rèn)編碼去解碼或編碼過(guò)。
默認(rèn)情況下,所有的文件名都會(huì)根據(jù) sys.getfilesystemencoding()
返回的文本編碼來(lái)編碼或解碼。比如:
>>> sys.getfilesystemencoding()
'utf-8'
>>>
如果因?yàn)槟撤N原因你想忽略這種編碼,可以使用一個(gè)原始字節(jié)字符串來(lái)指定一個(gè)文件名即可。比如:
>>> # Wrte a file using a unicode filename
>>> with open('jalape\xf1o.txt', 'w') as f:
... f.write('Spicy!')
...
6
>>> # Directory listing (decoded)
>>> import os
>>> os.listdir('.')
['jalape?o.txt']
>>> # Directory listing (raw)
>>> os.listdir(b'.') # Note: byte string
[b'jalapen\xcc\x83o.txt']
>>> # Open file with raw filename
>>> with open(b'jalapen\xcc\x83o.txt') as f:
... print(f.read())
...
Spicy!
>>>
正如你所見(jiàn),在最后兩個(gè)操作中,當(dāng)你給文件相關(guān)函數(shù)如 open()
和 os.listdir()
傳遞字節(jié)字符串時(shí),文件名的處理方式會(huì)稍有不同。
通常來(lái)講,你不需要擔(dān)心文件名的編碼和解碼,普通的文件名操作應(yīng)該就沒(méi)問(wèn)題了。 但是,有些操作系統(tǒng)允許用戶(hù)通過(guò)偶然或惡意方式去創(chuàng)建名字不符合默認(rèn)編碼的文件。 這些文件名可能會(huì)神秘地中斷那些需要處理大量文件的 Python 程序。
讀取目錄并通過(guò)原始未解碼方式處理文件名可以有效的避免這樣的問(wèn)題, 盡管這樣會(huì)帶來(lái)一定的編程難度。
關(guān)于打印不可解碼的文件名,請(qǐng)參考5.15小節(jié)。
你的程序獲取了一個(gè)目錄中的文件名列表,但是當(dāng)它試著去打印文件名的時(shí)候程序崩潰, 出現(xiàn)了 UnicodeEncodeError
異常和一條奇怪的消息—— surrogates not allowed
。
當(dāng)打印未知的文件名時(shí),使用下面的方法可以避免這樣的錯(cuò)誤:
def bad_filename(filename):
return repr(filename)[1:-1]
try:
print(filename)
except UnicodeEncodeError:
print(bad_filename(filename))
這一小節(jié)討論的是在編寫(xiě)必須處理文件系統(tǒng)的程序時(shí)一個(gè)不太常見(jiàn)但又很棘手的問(wèn)題。 默認(rèn)情況下,Python 假定所有文件名都已經(jīng)根據(jù) sys.getfilesystemencoding()
的值編碼過(guò)了。 但是,有一些文件系統(tǒng)并沒(méi)有強(qiáng)制要求這樣做,因此允許創(chuàng)建文件名沒(méi)有正確編碼的文件。 這種情況不太常見(jiàn),但是總會(huì)有些用戶(hù)冒險(xiǎn)這樣做或者是無(wú)意之中這樣做了( 可能是在一個(gè)有缺陷的代碼中給 open()
函數(shù)傳遞了一個(gè)不合規(guī)范的文件名)。
當(dāng)執(zhí)行類(lèi)似 os.listdir()
這樣的函數(shù)時(shí),這些不合規(guī)范的文件名就會(huì)讓 Python 陷入困境。 一方面,它不能僅僅只是丟棄這些不合格的名字。而另一方面,它又不能將這些文件名轉(zhuǎn)換為正確的文本字符串。 Python 對(duì)這個(gè)問(wèn)題的解決方案是從文件名中獲取未解碼的字節(jié)值比如 \xhh
并將它映射成 Unicode 字符 \udchh
表示的所謂的”代理編碼”。 下面一個(gè)例子演示了當(dāng)一個(gè)不合格目錄列表中含有一個(gè)文件名為 b?d.txt(使用 Latin-1 而不是 UTF-8 編碼)時(shí)的樣子:
>>> import os
>>> files = os.listdir('.')
>>> files
['spam.py', 'b\udce4d.txt', 'foo.txt']
>>>
如果你有代碼需要操作文件名或者將文件名傳遞給 open()
這樣的函數(shù),一切都能正常工作。 只有當(dāng)你想要輸出文件名時(shí)才會(huì)碰到些麻煩(比如打印輸出到屏幕或日志文件等)。 特別的,當(dāng)你想打印上面的文件名列表時(shí),你的程序就會(huì)崩潰:
>>> for name in files:
... print(name)
...
spam.py
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce4' in
position 1: surrogates not allowed
>>>
程序崩潰的原因就是字符 \udce4
是一個(gè)非法的Unicode字符。 它其實(shí)是一個(gè)被稱(chēng)為代理字符對(duì)的雙字符組合的后半部分。 由于缺少了前半部分,因此它是個(gè)非法的 Unicode。 所以,唯一能成功輸出的方法就是當(dāng)遇到不合法文件名時(shí)采取相應(yīng)的補(bǔ)救措施。 比如可以將上述代碼修改如下:
>>> for name in files:
... try:
... print(name)
... except UnicodeEncodeError:
... print(bad_filename(name))
...
spam.py
b\udce4d.txt
foo.txt
>>>
在 bad_filename()
函數(shù)中怎樣處置取決于你自己。 另外一個(gè)選擇就是通過(guò)某種方式重新編碼,示例如下:
def bad_filename(filename):
temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape')
return temp.decode('latin-1')
譯者注:
surrogateescape:
這種是 Python 在絕大部分面向 OS 的 API 中所使用的錯(cuò)誤處理器,
它能以一種優(yōu)雅的方式處理由操作系統(tǒng)提供的數(shù)據(jù)的編碼問(wèn)題。
在解碼出錯(cuò)時(shí)會(huì)將出錯(cuò)字節(jié)存儲(chǔ)到一個(gè)很少被使用到的 Unicode 編碼范圍內(nèi)。
在編碼時(shí)將那些隱藏值又還原回原先解碼失敗的字節(jié)序列。
它不僅對(duì)于 OS API 非常有用,也能很容易的處理其他情況下的編碼錯(cuò)誤。
使用這個(gè)版本產(chǎn)生的輸出如下:
>>> for name in files:
... try:
... print(name)
... except UnicodeEncodeError:
... print(bad_filename(name))
...
spam.py
b?d.txt
foo.txt
>>>
這一小節(jié)主題可能會(huì)被大部分讀者所忽略。但是如果你在編寫(xiě)依賴(lài)文件名和文件系統(tǒng)的關(guān)鍵任務(wù)程序時(shí), 就必須得考慮到這個(gè)。否則你可能會(huì)在某個(gè)周末被叫到辦公室去調(diào)試一些令人費(fèi)解的錯(cuò)誤。
你想在不關(guān)閉一個(gè)已打開(kāi)的文件前提下增加或改變它的 Unicode 編碼。
如果你想給一個(gè)以二進(jìn)制模式打開(kāi)的文件添加 Unicode 編碼/解碼方式, 可以使用 io.TextIOWrapper()
對(duì)象包裝它。比如:
import urllib.request
import io
u = urllib.request.urlopen('http://www.python.org')
f = io.TextIOWrapper(u, encoding='utf-8')
text = f.read()
如果你想修改一個(gè)已經(jīng)打開(kāi)的文本模式的文件的編碼方式,可以先使用 detach()
方法移除掉已存在的文本編碼層, 并使用新的編碼方式代替。下面是一個(gè)在 sys.stdout
上修改編碼方式的例子:
>>> import sys
>>> sys.stdout.encoding
'UTF-8'
>>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='latin-1')
>>> sys.stdout.encoding
'latin-1'
>>>
這樣做可能會(huì)中斷你的終端,這里僅僅是為了演示而已。
I/O 系統(tǒng)由一系列的層次構(gòu)建而成。你可以試著運(yùn)行下面這個(gè)操作一個(gè)文本文件的例子來(lái)查看這種層次:
>>> f = open('sample.txt','w')
>>> f
<_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'>
>>> f.buffer
<_io.BufferedWriter name='sample.txt'>
>>> f.buffer.raw
<_io.FileIO name='sample.txt' mode='wb'>
>>>
在這個(gè)例子中,io.TextIOWrapper
是一個(gè)編碼和解碼 Unicode 的文本處理層, io.BufferedWriter
是一個(gè)處理二進(jìn)制數(shù)據(jù)的帶緩沖的 I/O 層, io.FileIO
是一個(gè)表示操作系統(tǒng)底層文件描述符的原始文件。 增加或改變文本編碼會(huì)涉及增加或改變最上面的 io.TextIOWrapper
層。
一般來(lái)講,像上面例子這樣通過(guò)訪問(wèn)屬性值來(lái)直接操作不同的層是很不安全的。 例如,如果你試著使用下面這樣的技術(shù)改變編碼看看會(huì)發(fā)生什么:
>>> f
<_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'>
>>> f = io.TextIOWrapper(f.buffer, encoding='latin-1')
>>> f
<_io.TextIOWrapper name='sample.txt' encoding='latin-1'>
>>> f.write('Hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
>>>
結(jié)果出錯(cuò)了,因?yàn)?f 的原始值已經(jīng)被破壞了并關(guān)閉了底層的文件。
detach()
方法會(huì)斷開(kāi)文件的最頂層并返回第二層,之后最頂層就沒(méi)什么用了。例如:
>>> f = open('sample.txt', 'w')
>>> f
<_io.TextIOWrapper name='sample.txt' mode='w' encoding='UTF-8'>
>>> b = f.detach()
>>> b
<_io.BufferedWriter name='sample.txt'>
>>> f.write('hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: underlying buffer has been detached
>>>
一旦斷開(kāi)最頂層后,你就可以給返回結(jié)果添加一個(gè)新的最頂層。比如:
>>> f = io.TextIOWrapper(b, encoding='latin-1')
>>> f
<_io.TextIOWrapper name='sample.txt' encoding='latin-1'>
>>>
盡管已經(jīng)向你演示了改變編碼的方法, 但是你還可以利用這種技術(shù)來(lái)改變文件行處理、錯(cuò)誤機(jī)制以及文件處理的其他方面。例如:
>>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='ascii',
... errors='xmlcharrefreplace')
>>> print('Jalape\u00f1o')
Jalapeño
>>>
注意下最后輸出中的非 ASCII 字符 ?
是如何被ñ
取代的。
你想在文本模式打開(kāi)的文件中寫(xiě)入原始的字節(jié)數(shù)據(jù)。
將字節(jié)數(shù)據(jù)直接寫(xiě)入文件的緩沖區(qū)即可,例如:
>>> import sys
>>> sys.stdout.write(b'Hello\n')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: must be str, not bytes
>>> sys.stdout.buffer.write(b'Hello\n')
Hello
5
>>>
類(lèi)似的,能夠通過(guò)讀取文本文件的 buffer
屬性來(lái)讀取二進(jìn)制數(shù)據(jù)。
I/O 系統(tǒng)以層級(jí)結(jié)構(gòu)的形式構(gòu)建而成。 文本文件是通過(guò)在一個(gè)擁有緩沖的二進(jìn)制模式文件上增加一個(gè) Unicode 編碼/解碼層來(lái)創(chuàng)建。 buffer
屬性指向?qū)?yīng)的底層文件。如果你直接訪問(wèn)它的話(huà)就會(huì)繞過(guò)文本編碼/解碼層。
本小節(jié)例子展示的 sys.stdout
可能看起來(lái)有點(diǎn)特殊。 默認(rèn)情況下,sys.stdout
總是以文本模式打開(kāi)的。 但是如果你在寫(xiě)一個(gè)需要打印二進(jìn)制數(shù)據(jù)到標(biāo)準(zhǔn)輸出的腳本的話(huà),你可以使用上面演示的技術(shù)來(lái)繞過(guò)文本編碼層。
你有一個(gè)對(duì)應(yīng)于操作系統(tǒng)上一個(gè)已打開(kāi)的 I/O 通道(比如文件、管道、套接字等)的整型文件描述符, 你想將它包裝成一個(gè)更高層的 Python 文件對(duì)象。
一個(gè)文件描述符和一個(gè)打開(kāi)的普通文件是不一樣的。 文件描述符僅僅是一個(gè)由操作系統(tǒng)指定的整數(shù),用來(lái)指代某個(gè)系統(tǒng)的 I/O 通道。 如果你碰巧有這么一個(gè)文件描述符,你可以通過(guò)使用 open()
函數(shù)來(lái)將其包裝為一個(gè) Python 的文件對(duì)象。 你僅僅只需要使用這個(gè)整數(shù)值的文件描述符作為第一個(gè)參數(shù)來(lái)代替文件名即可。例如:
# Open a low-level file descriptor
import os
fd = os.open('somefile.txt', os.O_WRONLY | os.O_CREAT)
# Turn into a proper file
f = open(fd, 'wt')
f.write('hello world\n')
f.close()
當(dāng)高層的文件對(duì)象被關(guān)閉或者破壞的時(shí)候,底層的文件描述符也會(huì)被關(guān)閉。 如果這個(gè)并不是你想要的結(jié)果,你可以給open()
函數(shù)傳遞一個(gè)可選的 colsefd=False
。比如:
# Create a file object, but don't close underlying fd when done
f = open(fd, 'wt', closefd=False)
...
在 Unix 系統(tǒng)中,這種包裝文件描述符的技術(shù)可以很方便的將一個(gè)類(lèi)文件接口作用于一個(gè)以不同方式打開(kāi)的 I/O 通道上, 如管道、套接字等。舉例來(lái)講,下面是一個(gè)操作管道的例子:
from socket import socket, AF_INET, SOCK_STREAM
def echo_client(client_sock, addr):
print('Got connection from', addr)
# Make text-mode file wrappers for socket reading/writing
client_in = open(client_sock.fileno(), 'rt', encoding='latin-1',
closefd=False)
client_out = open(client_sock.fileno(), 'wt', encoding='latin-1',
closefd=False)
# Echo lines back to the client using file I/O
for line in client_in:
client_out.write(line)
client_out.flush()
client_sock.close()
def echo_server(address):
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(address)
sock.listen(1)
while True:
client, addr = sock.accept()
echo_client(client, addr)
需要重點(diǎn)強(qiáng)調(diào)的一點(diǎn)是,上面的例子僅僅是為了演示內(nèi)置的 open()
函數(shù)的一個(gè)特性,并且也只適用于基于 Unix 的系統(tǒng)。 如果你想將一個(gè)類(lèi)文件接口作用在一個(gè)套接字并希望你的代碼可以跨平臺(tái),請(qǐng)使用套接字對(duì)象的makefile()
方法。 但是如果不考慮可移植性的話(huà),那上面的解決方案會(huì)比使用 makefile()
性能更好一點(diǎn)。
你也可以使用這種技術(shù)來(lái)構(gòu)造一個(gè)別名,允許以不同于第一次打開(kāi)文件的方式使用它。 例如,下面演示如何創(chuàng)建一個(gè)文件對(duì)象,它允許你輸出二進(jìn)制數(shù)據(jù)到標(biāo)準(zhǔn)輸出(通常以文本模式打開(kāi)):
import sys
# Create a binary-mode file for stdout
bstdout = open(sys.stdout.fileno(), 'wb', closefd=False)
bstdout.write(b'Hello World\n')
bstdout.flush()
盡管可以將一個(gè)已存在的文件描述符包裝成一個(gè)正常的文件對(duì)象, 但是要注意的是并不是所有的文件模式都被支持,并且某些類(lèi)型的文件描述符可能會(huì)有副作用 (特別是涉及到錯(cuò)誤處理、文件結(jié)尾條件等等的時(shí)候)。 在不同的操作系統(tǒng)上這種行為也是不一樣,特別的,上面的例子都不能在非 Unix 系統(tǒng)上運(yùn)行。 我說(shuō)了這么多,意思就是讓你充分測(cè)試自己的實(shí)現(xiàn)代碼,確保它能按照期望工作。
你需要在程序執(zhí)行時(shí)創(chuàng)建一個(gè)臨時(shí)文件或目錄,并希望使用完之后可以自動(dòng)銷(xiāo)毀掉。
tempfile
模塊中有很多的函數(shù)可以完成這任務(wù)。 為了創(chuàng)建一個(gè)匿名的臨時(shí)文件,可以使用tempfile.TemporaryFile
:
from tempfile import TemporaryFile
with TemporaryFile('w+t') as f:
# Read/write to the file
f.write('Hello World\n')
f.write('Testing\n')
# Seek back to beginning and read the data
f.seek(0)
data = f.read()
# Temporary file is destroyed
或者,如果你喜歡,你還可以像這樣使用臨時(shí)文件:
f = TemporaryFile('w+t')
# Use the temporary file
...
f.close()
# File is destroyed
TemporaryFile()
的第一個(gè)參數(shù)是文件模式,通常來(lái)講文本模式使用 w+t
,二進(jìn)制模式使用w+b
。 這個(gè)模式同時(shí)支持讀和寫(xiě)操作,在這里是很有用的,因?yàn)楫?dāng)你關(guān)閉文件去改變模式的時(shí)候,文件實(shí)際上已經(jīng)不存在了。 TemporaryFile()
另外還支持跟內(nèi)置的 open()