在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Python/ 文件與 IO
類與對象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時間
測試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

文件與 IO

所有程序都要處理輸入和輸出。 這一章將涵蓋處理不同類型的文件,包括文本和二進(jìn)制文件,文件編碼和其他相關(guān)的內(nèi)容。 對文件名和目錄的操作也會涉及到。

讀寫文本數(shù)據(jù)

問題

你需要讀寫各種不同編碼的文本數(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
        ...

類似的,為了寫入一個文本文件,使用帶有 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)容,使用模式為atopen()函數(shù)。

文件的讀寫操作默認(rèn)使用系統(tǒng)編碼,可以通過調(diào)用 sys.getdefaultencoding() 來得到。 在大多數(shù)機(jī)器上面都是 utf-8 編碼。如果你已經(jīng)知道你要讀寫的文本是其他編碼方式, 那么可以通過傳遞一個可選的 encoding 參數(shù)給 open()函數(shù)。如下所示:

with open('somefile.txt', 'rt', encoding='latin-1') as f:
    ...

Python 支持非常多的文本編碼。幾個常見的編碼是 ascii, latin-1, utf-8 和 utf-16。 在 web 應(yīng)用程序中通常都使用的是 UTF-8。 ascii 對應(yīng)從 U+0000 到 U+007F 范圍內(nèi)的7位字符。 latin-1 是字節(jié)0-255到 U+0000 至 U+00FF 范圍內(nèi) Unicode字 符的直接映射。 當(dāng)讀取一個未知編碼的文本時使用 latin-1 編碼永遠(yuǎn)不會產(chǎn)生解碼錯誤。 使用 latin-1 編碼讀取一個文件的時候也許不能產(chǎn)生完全正確的文本解碼數(shù)據(jù), 但是它也能從中提取出足夠多的有用數(shù)據(jù)。同時,如果你之后將數(shù)據(jù)回寫回去,原先的數(shù)據(jù)還是會保留的。

討論

讀寫文本文件一般來講是比較簡單的。但是也幾點(diǎn)是需要注意的。 首先,在例子程序中的 with 語句給被使用到的文件創(chuàng)建了一個上下文環(huán)境, 但 with控制塊結(jié)束時,文件會自動關(guān)閉。你也可以不使用 with 語句,但是這時候你就必須記得手動關(guān)閉文件:

f = open('somefile.txt', 'rt')
data = f.read()
f.close()

另外一個問題是關(guān)于換行符的識別問題,在 Unix 和 Windows 中是不一樣的(分別是 n 和 rn)。 默認(rèn)情況下,Python 會以統(tǒng)一模式處理換行符。 這種模式下,在讀取文本的時候,Python 可以識別所有的普通換行符并將其轉(zhuǎn)換為單個\n 字符。 類似的,在輸出時會將換行符\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:
    ...

為了說明兩者之間的差異,下面我在 Unix 機(jī)器上面讀取一個 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'
>>>

最后一個問題就是文本文件中可能出現(xiàn)的編碼錯誤。 但你讀取或者寫入一個文本文件時,你可能會遇到一個編碼或者解碼錯誤。比如:

>>> 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)這個錯誤,通常表示你讀取文本時指定的編碼不正確。 你最好仔細(xì)閱讀說明并確認(rèn)你的文件編碼是正確的(比如使用 UTF-8 而不是 Latin-1 編碼或其他)。 如果編碼錯誤還是存在的話,你可以給 open()函數(shù)傳遞一個可選的errors參數(shù)來處理這些錯誤。 下面是一些處理常見錯誤的方法:

>>> # 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ù)來處理編碼錯誤,可能會讓你的生活變得很糟糕。 對于文本處理的首要原則是確保你總是使用的是正確編碼。當(dāng)模棱兩可的時候,就使用默認(rèn)的設(shè)置(通常都是 UTF-8)。

打印輸出至文件中

問題

你想將 print() 函數(shù)的輸出重定向到一個文件中去。

解決方案

print() 函數(shù)中指定 file 關(guān)鍵字參數(shù),像下面這樣:

with open('d:/work/test.txt', 'wt') as f:
    print('Hello World!', file=f)

討論

關(guān)于輸出重定向到文件中就這些了。但是有一點(diǎn)要注意的就是文件必須是以文本模式打開。 如果文件是二進(jìn)制模式的話,打印就會出錯。

使用其他分隔符或行終止符打印

問題

你想使用 print() 函數(shù)輸出數(shù)據(jù),但是想改變默認(rèn)的分隔符或者行尾符。

解決方案

可以使用在 print() 函數(shù)中使用 sepend 關(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)你想使用非空格分隔符來輸出數(shù)據(jù)的時候,給 print() 函數(shù)傳遞一個 seq 參數(shù)是最簡單的方案。 有時候你會看到一些程序員會使用 str.join() 來完成同樣的事情。比如:

>>> print(','.join('ACME','50','91.5'))
ACME,50,91.5
>>>

str.join() 的問題在于它僅僅適用于字符串。這意味著你通常需要執(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)然可以不用那么麻煩,僅僅只需要像下面這樣寫:

>>> print(*row, sep=',')
ACME,50,91.5
>>>

讀寫字節(jié)數(shù)據(jù)

問題

你想讀寫二進(jìn)制文件,比如圖片,聲音文件等等。

解決方案

使用模式為 rbwbopen() 函數(shù)來讀取或?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ù)據(jù)都是字節(jié)字符串格式的,而不是文本字符串。 類似的,在寫入的時候,必須保證參數(shù)是以字節(jié)形式對外暴露數(shù)據(jù)的對象(比如字節(jié)字符串,字節(jié)數(shù)組對象等)。

討論

在讀取二進(jìn)制數(shù)據(jù)的時候,字節(jié)字符串和文本字符串的語義差異可能會導(dǎo)致一個潛在的陷阱。 特別需要注意的是,索引和迭代動作返回的是字節(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 還有一個鮮為人知的特性就是數(shù)組和C結(jié)構(gòu)體類型能直接被寫入,而不需要中間轉(zhuǎn)換為自己對象。比如:

import array
nums = array.array('i', [1, 2, 3, 4])
with open('data.bin','wb') as f:
    f.write(nums)

這個適用于任何實(shí)現(xiàn)了被稱之為”緩沖接口”的對象,這種對象會直接暴露其底層的內(nèi)存緩沖區(qū)給能處理它的操作。 二進(jìn)制數(shù)據(jù)的寫入就是這類操作之一。

很多對象還允許通過使用文件對象的 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ù)的時候需要格外小心,因?yàn)樗ǔ>哂衅脚_相關(guān)性,并且可能會依賴字長和字節(jié)順序(高位優(yōu)先和低位優(yōu)先)。 可以查看5.9小節(jié)中另外一個讀取二進(jìn)制數(shù)據(jù)到可修改緩沖區(qū)的例子。

文件不存在才能寫入

問題

你想像一個文件中寫入數(shù)據(jù),但是前提必須是這個文件在文件系統(tǒng)上不存在。 也就是不允許覆蓋已存在的文件內(nèi)容。

解決方案

可以在 open() 函數(shù)中使用 x 模式來代替 w 模式的方法來解決這個問題。比如:

>>> 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 來代替 xt

討論

這一小節(jié)演示了在寫文件時通常會遇到的一個問題的完美解決方案(不小心覆蓋一個已存在的文件)。 一個替代方案是先測試這個文件是否存在,像下面這樣:

>>> 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!
>>>

顯而易見,使用 x 文件模式更加簡單。要注意的是 x 模式是一個 Python3 對 open()函數(shù)特有的擴(kuò)展。 在 Python 的舊版本或者是 Python 實(shí)現(xiàn)的底層 C 函數(shù)庫中都是沒有這個模式的。

字符串的 I/O 操作

問題

你想使用操作類文件對象的程序來操作文本或二進(jìn)制字符串。

解決方案

使用 io.StringIO()io.BytesIO() 類來創(chuàng)建類文件對象操作字符串?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 類來代替。比如:

>>> s = io.BytesIO()
>>> s.write(b'binary data')
>>> s.getvalue()
b'binary data'
>>>

討論

當(dāng)你想模擬一個普通的文件的時候 StringIOBytesIO 類是很有用的。 比如,在單元測試中,你可以使用 StringIO 來創(chuàng)建一個包含測試數(shù)據(jù)的類文件對象, 這個對象可以被傳給某個參數(shù)為普通文件對象的函數(shù)。

需要注意的是, StringIOBytesIO實(shí)例并沒有正確的整數(shù)類型的文件描述符。 因此,它們不能在那些需要使用真實(shí)的系統(tǒng)級文件如文件,管道或者是套接字的程序中使用。

讀寫壓縮文件

問題

你想讀寫一個 gzip 或 bz2 格式的壓縮文件。

解決方案

gzipbz2 模塊可以很容易的處理這些文件。 兩個模塊都為 open() 函數(shù)提供了另外的實(shí)現(xià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()

類似的,為了寫入壓縮數(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 的編碼/解碼。 類似的,如果你想操作二進(jìn)制數(shù)據(jù),使用 rb 或者 wb 文件模式即可。

討論

大部分情況下讀寫壓縮數(shù)據(jù)都是很簡單的。但是要注意的是選擇一個正確的文件模式是非常重要的。 如果你不指定模式,那么默認(rèn)的就是二進(jìn)制模式,如果這時候程序想要接受的是文本數(shù)據(jù),那么就會出錯。 gzip.open()bz2.open() 接受跟內(nèi)置的 open()函數(shù)一樣的參數(shù), 包括 encoding,errors,newline 等等。

當(dāng)寫入壓縮數(shù)據(jù)時,可以使用compresslevel 這個可選的關(guān)鍵字參數(shù)來指定一個壓縮級別。比如:

with gzip.open('somefile.gz', 'wt', compresslevel=5) as f:
    f.write(text)

默認(rèn)的等級是9,也是最高的壓縮等級。等級越低性能越好,但是數(shù)據(jù)壓縮程度也越低。

最后一點(diǎn), gzip.open()bz2.open()還有一個很少被知道的特性, 它們可以作用在一個已存在并以二進(jìn)制模式打開的文件上。比如,下面代碼是可行的:

import gzip
f = open('somefile.gz', 'rb')
with gzip.open(f, 'rt') as g:
    text = g.read()

這樣就允許gzipbz2模塊可以工作在許多類文件對象上,比如套接字,管道和內(nèi)存中文件等。

固定大小記錄的文件迭代

問題

你想在一個固定長度記錄或者數(shù)據(jù)塊的集合上迭代,而不是在一個文件中一行一行的迭代。

解決方案

通過下面這個小技巧使用 iterfunctools.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:
        ...

這個例子中的records 對象是一個可迭代對象,它會不斷的產(chǎn)生固定大小的數(shù)據(jù)塊,直到文件末尾。 要注意的是如果總記錄大小不是塊大小的整數(shù)倍的話,最后一個返回元素的字節(jié)數(shù)會比期望值少。

討論

iter()函數(shù)有一個鮮為人知的特性就是,如果你給它傳遞一個可調(diào)用對象和一個標(biāo)記值,它會創(chuàng)建一個迭代器。 這個迭代器會一直調(diào)用傳入的可調(diào)用對象直到它返回標(biāo)記值為止,這時候迭代終止。

在例子中, functools.partial用來創(chuàng)建一個每次被調(diào)用時從文件中讀取固定數(shù)目字節(jié)的可調(diào)用對象。 標(biāo)記值b'' 就是當(dāng)?shù)竭_(dá)文件結(jié)尾時的返回值。

最后再提一點(diǎn),上面的例子中的文件時以二進(jìn)制模式打開的。 如果是讀取固定大小的記錄,這通常是最普遍的情況。 而對于文本文件,一行一行的讀取(默認(rèn)的迭代行為)更普遍點(diǎn)。

讀取二進(jìn)制數(shù)據(jù)到可變緩沖區(qū)中

問題

你想直接讀取二進(jìn)制數(shù)據(jù)到一個可變緩沖區(qū)中,而不需要做任何的中間復(fù)制操作。 或者你想原地修改數(shù)據(jù)并將它寫回到一個文件中去。

解決方案

為了讀取數(shù)據(jù)到一個可變數(shù)組中,使用文件對象的 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

下面是一個演示這個函數(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
>>>

討論

文件對象的 readinto() 方法能被用來為預(yù)先分配內(nèi)存的數(shù)組填充數(shù)據(jù),甚至包括由 array 模塊或numpy庫創(chuàng)建的數(shù)組。 和普通 read()方法不同的是, readinto()填充已存在的緩沖區(qū)而不是為新對象重新分配內(nèi)存再返回它們。 因此,你可以使用它來避免大量的內(nèi)存分配操作。 比如,如果你讀取一個由相同大小的記錄組成的二進(jìn)制文件時,你可以像下面這樣寫:

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
        ...

另外有一個有趣特性就是 memoryview , 它可以通過零復(fù)制的方式對已存在的緩沖區(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í)際讀取的字節(jié)數(shù)。

如果字節(jié)數(shù)小于緩沖區(qū)大小,表明數(shù)據(jù)被截斷或者被破壞了(比如你期望每次讀取指定數(shù)量的字節(jié))。

最后,留心觀察其他函數(shù)庫和模塊中和 into相關(guān)的函數(shù)(比如 recv_into()pack_into()等)。 Python 的很多其他部分已經(jīng)能支持直接的 I/O 或數(shù)據(jù)訪問操作,這些操作可被用來填充或修改數(shù)組和緩沖區(qū)內(nèi)容。

關(guān)于解析二進(jìn)制結(jié)構(gòu)和memoryviews使用方法的更高級例子,請參考6.12小節(jié)。

內(nèi)存映射的二進(jìn)制文件

問題

你想內(nèi)存映射一個二進(jìn)制文件到一個可變字節(jié)數(shù)組中,目的可能是為了隨機(jī)訪問它的內(nèi)容或者是原地做些修改。

解決方案

使用 mmap模塊來內(nèi)存映射文件。 下面是一個工具函數(shù),向你演示了如何打開一個文件并以一種便捷方式內(nèi)存映射這個文件。

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)

為了使用這個函數(shù),你需要有一個已創(chuàng)建并且內(nèi)容不為空的文件。 下面是一個例子,教你怎樣初始創(chuàng)建一個文件并將其內(nèi)容擴(kuò)充到指定大?。?/p>

>>> size = 1000000
>>> with open('data', 'wb') as f:
...     f.seek(size-1)
...     f.write(b'\x00')
...
>>>

下面是一個利用 memory_map() 函數(shù)類內(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 對象同樣也可以作為一個上下文管理器來使用, 這時候底層的文件會被自動關(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ù)打開的文件同時支持讀和寫操作。 任何的修改內(nèi)容都會復(fù)制回原來的文件中。 如果需要只讀的訪問模式,可以給參數(shù) access 賦值為 mmap.ACCESS_READ。比如:

m = memory_map(filename, mmap.ACCESS_READ)

如果你想在本地修改數(shù)據(jù),但是又不想將修改寫回到原始文件中,可以使用 mmap.ACCESS_COPY

m = memory_map(filename, mmap.ACCESS_COPY)

討論

為了隨機(jī)訪問文件的內(nèi)容,使用 mmap將文件映射到內(nèi)存中是一個高效和優(yōu)雅的方法。 例如,你無需打開一個文件并執(zhí)行大量的 seek() , read(), write()調(diào)用, 只需要簡單的映射文件并使用切片操作訪問數(shù)據(jù)即可。

一般來講, mmap()所暴露的內(nèi)存看上去就是一個二進(jìn)制數(shù)組對象。 但是,你可以使用一個內(nè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)存映射一個文件并不會導(dǎo)致整個文件被讀取到內(nèi)存中。 也就是說,文件并沒有被復(fù)制到內(nèi)存緩存或數(shù)組中。相反,操作系統(tǒng)僅僅為文件內(nèi)容保留了一段虛擬內(nèi)存。 當(dāng)你訪問文件的不同區(qū)域時,這些區(qū)域的內(nèi)容才根據(jù)需要被讀取并映射到內(nèi)存區(qū)域中。 而那些從沒被訪問到的部分還是留在磁盤上。所有這些過程是透明的,在幕后完成!

如果多個 Python 解釋器內(nèi)存映射同一個文件,得到的 mmap 對象能夠被用來在解釋器直接交換數(shù)據(jù)。 也就是說,所有解釋器都能同時讀寫數(shù)據(jù),并且其中一個解釋器所做的修改會自動呈現(xiàn)在其他解釋器中。 很明顯,這里需要考慮同步的問題。但是這種方法有時候可以用來在管道或套接字間傳遞數(shù)據(jù)。

這一小節(jié)中函數(shù)盡量寫得很通用,同時適用于 Unix 和 Windows 平臺。 要注意的是使用 mmap()函數(shù)時會在底層有一些平臺的差異性。 另外,還有一些選項(xiàng)可以用來創(chuàng)建匿名的內(nèi)存映射區(qū)域。 如果你對這個感興趣,確保你仔細(xì)研讀了 Python 文檔中這方面的內(nèi)容

文件路徑名的操作

問題

你需要使用路徑名來獲取文件名,目錄名,絕對路徑等等。

解決方案

使用 os.path 模塊中的函數(shù)來操作路徑名。 下面是一個交互式例子來演示一些關(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')
>>>

討論

對于任何的文件名的操作,你都應(yīng)該使用 os.path 模塊,而不是使用標(biāo)準(zhǔn)字符串操作來構(gòu)造自己的代碼。 特別是為了可移植性考慮的時候更應(yīng)如此, 因?yàn)?os.path 模塊知道 Unix 和 Windows 系統(tǒng)之間的差異并且能夠可靠地處理類似 Data/data.csvData\data.csv 這樣的文件名。 其次,你真的不應(yīng)該浪費(fèi)時間去重復(fù)造輪子。通常最好是直接使用已經(jīng)為你準(zhǔn)備好的功能。

要注意的是 os.path還有更多的功能在這里并沒有列舉出來。 可以查閱官方文檔來獲取更多與文件測試,符號鏈接等相關(guān)的函數(shù)說明。

測試文件是否存在

問題

你想測試一個文件或目錄是否存在。

解決方案

使用 os.path 模塊來測試一個文件或目錄是否存在。比如:

>>> import os
>>> os.path.exists('/etc/passwd')
True
>>> os.path.exists('/tmp/spam')
False
>>>

你還能進(jìn)一步測試這個文件時什么類型的。 在下面這些測試中,如果測試的文件不存在的時候,結(jié)果都會返回 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 模塊來解決:

>>> 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 來進(jìn)行文件測試是很簡單的。 在寫這些腳本時,可能唯一需要注意的就是你需要考慮文件權(quán)限的問題,特別是在獲取元數(shù)據(jù)時候。比如:

>>> 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)中某個目錄下的所有文件列表。

解決方案

使用 os.listdir() 函數(shù)來獲取某個目錄中的文件列表:

import os
names = os.listdir('somedir')

結(jié)果會返回目錄中所有文件列表,包括所有文件,子目錄,符號鏈接等等。 如果你需要通過某種方式過濾數(shù)據(jù),可以考慮結(jié)合os.path庫中的一些函數(shù)來使用列表推導(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()方法對于過濾一個目錄的內(nèi)容也是很有用的。比如:

pyfiles = [name for name in os.listdir('somedir')
            if name.endswith('.py')]

對于文件名的匹配,你可能會考慮使用 globfnmatch模塊。比如:

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í)體名列表而已。 如果你還想獲取其他的元信息,比如文件大小,修改時間等等, 你或許還需要使用到os.path 模塊中的函數(shù)或著 os.stat()函數(shù)來收集數(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)要注意的就是,有時候在處理文件名編碼問題時候可能會出現(xiàn)一些問題。 通常來講,函數(shù) os.listdir()返回的實(shí)體列表會根據(jù)系統(tǒng)默認(rèn)的文件名編碼來解碼。 但是有時候也會碰到一些不能正常解碼的文件名。 關(guān)于文件名的處理問題,在5.14和5.15小節(jié)有更詳細(xì)的講解。

忽略文件名編碼

問題

你想使用原始文件名執(zhí)行文件的 I/O 操作,也就是說文件名并沒有經(jīng)過系統(tǒng)默認(rèn)編碼去解碼或編碼過。

解決方案

默認(rèn)情況下,所有的文件名都會根據(jù) sys.getfilesystemencoding()返回的文本編碼來編碼或解碼。比如:

>>> sys.getfilesystemencoding()
'utf-8'
>>>

如果因?yàn)槟撤N原因你想忽略這種編碼,可以使用一個原始字節(jié)字符串來指定一個文件名即可。比如:

>>> # 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!
>>>

正如你所見,在最后兩個操作中,當(dāng)你給文件相關(guān)函數(shù)如 open()os.listdir()傳遞字節(jié)字符串時,文件名的處理方式會稍有不同。

討論

通常來講,你不需要擔(dān)心文件名的編碼和解碼,普通的文件名操作應(yīng)該就沒問題了。 但是,有些操作系統(tǒng)允許用戶通過偶然或惡意方式去創(chuàng)建名字不符合默認(rèn)編碼的文件。 這些文件名可能會神秘地中斷那些需要處理大量文件的 Python 程序。

讀取目錄并通過原始未解碼方式處理文件名可以有效的避免這樣的問題, 盡管這樣會帶來一定的編程難度。

關(guān)于打印不可解碼的文件名,請參考5.15小節(jié)。

打印不合法的文件名

問題

你的程序獲取了一個目錄中的文件名列表,但是當(dāng)它試著去打印文件名的時候程序崩潰, 出現(xiàn)了 UnicodeEncodeError 異常和一條奇怪的消息—— surrogates not allowed

解決方案

當(dāng)打印未知的文件名時,使用下面的方法可以避免這樣的錯誤:

def bad_filename(filename):
    return repr(filename)[1:-1]

try:
    print(filename)
except UnicodeEncodeError:
    print(bad_filename(filename))

討論

這一小節(jié)討論的是在編寫必須處理文件系統(tǒng)的程序時一個不太常見但又很棘手的問題。 默認(rèn)情況下,Python 假定所有文件名都已經(jīng)根據(jù) sys.getfilesystemencoding() 的值編碼過了。 但是,有一些文件系統(tǒng)并沒有強(qiáng)制要求這樣做,因此允許創(chuàng)建文件名沒有正確編碼的文件。 這種情況不太常見,但是總會有些用戶冒險這樣做或者是無意之中這樣做了( 可能是在一個有缺陷的代碼中給 open()函數(shù)傳遞了一個不合規(guī)范的文件名)。

當(dāng)執(zhí)行類似 os.listdir()這樣的函數(shù)時,這些不合規(guī)范的文件名就會讓 Python 陷入困境。 一方面,它不能僅僅只是丟棄這些不合格的名字。而另一方面,它又不能將這些文件名轉(zhuǎn)換為正確的文本字符串。 Python 對這個問題的解決方案是從文件名中獲取未解碼的字節(jié)值比如 \xhh 并將它映射成 Unicode 字符 \udchh 表示的所謂的”代理編碼”。 下面一個例子演示了當(dāng)一個不合格目錄列表中含有一個文件名為 b?d.txt(使用 Latin-1 而不是 UTF-8 編碼)時的樣子:

>>> import os
>>> files = os.listdir('.')
>>> files
['spam.py', 'b\udce4d.txt', 'foo.txt']
>>>

如果你有代碼需要操作文件名或者將文件名傳遞給 open() 這樣的函數(shù),一切都能正常工作。 只有當(dāng)你想要輸出文件名時才會碰到些麻煩(比如打印輸出到屏幕或日志文件等)。 特別的,當(dāng)你想打印上面的文件名列表時,你的程序就會崩潰:

>>> 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是一個非法的Unicode字符。 它其實(shí)是一個被稱為代理字符對的雙字符組合的后半部分。 由于缺少了前半部分,因此它是個非法的 Unicode。 所以,唯一能成功輸出的方法就是當(dāng)遇到不合法文件名時采取相應(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ù)中怎樣處置取決于你自己。 另外一個選擇就是通過某種方式重新編碼,示例如下:

def bad_filename(filename):
    temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape')
    return temp.decode('latin-1')

譯者注:

surrogateescape:
這種是 Python 在絕大部分面向 OS 的 API 中所使用的錯誤處理器,
它能以一種優(yōu)雅的方式處理由操作系統(tǒng)提供的數(shù)據(jù)的編碼問題。
在解碼出錯時會將出錯字節(jié)存儲到一個很少被使用到的 Unicode 編碼范圍內(nèi)。
在編碼時將那些隱藏值又還原回原先解碼失敗的字節(jié)序列。
它不僅對于 OS API 非常有用,也能很容易的處理其他情況下的編碼錯誤。

使用這個版本產(chǎn)生的輸出如下:

>>> for name in files:
...     try:
...         print(name)
...     except UnicodeEncodeError:
...         print(bad_filename(name))
...
spam.py
b?d.txt
foo.txt
>>>

這一小節(jié)主題可能會被大部分讀者所忽略。但是如果你在編寫依賴文件名和文件系統(tǒng)的關(guān)鍵任務(wù)程序時, 就必須得考慮到這個。否則你可能會在某個周末被叫到辦公室去調(diào)試一些令人費(fèi)解的錯誤。

增加或改變已打開文件的編碼

問題

你想在不關(guān)閉一個已打開的文件前提下增加或改變它的 Unicode 編碼。

解決方案

如果你想給一個以二進(jìn)制模式打開的文件添加 Unicode 編碼/解碼方式, 可以使用 io.TextIOWrapper()對象包裝它。比如:

import urllib.request
import io

u = urllib.request.urlopen('http://www.python.org')
f = io.TextIOWrapper(u, encoding='utf-8')
text = f.read()

如果你想修改一個已經(jīng)打開的文本模式的文件的編碼方式,可以先使用 detach() 方法移除掉已存在的文本編碼層, 并使用新的編碼方式代替。下面是一個在 sys.stdout 上修改編碼方式的例子:

>>> import sys
>>> sys.stdout.encoding
'UTF-8'
>>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='latin-1')
>>> sys.stdout.encoding
'latin-1'
>>>

這樣做可能會中斷你的終端,這里僅僅是為了演示而已。

討論

I/O 系統(tǒng)由一系列的層次構(gòu)建而成。你可以試著運(yùn)行下面這個操作一個文本文件的例子來查看這種層次:

>>> 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'>
>>>

在這個例子中,io.TextIOWrapper 是一個編碼和解碼 Unicode 的文本處理層, io.BufferedWriter 是一個處理二進(jìn)制數(shù)據(jù)的帶緩沖的 I/O 層, io.FileIO 是一個表示操作系統(tǒng)底層文件描述符的原始文件。 增加或改變文本編碼會涉及增加或改變最上面的 io.TextIOWrapper層。

一般來講,像上面例子這樣通過訪問屬性值來直接操作不同的層是很不安全的。 例如,如果你試著使用下面這樣的技術(shù)改變編碼看看會發(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é)果出錯了,因?yàn)?f 的原始值已經(jīng)被破壞了并關(guān)閉了底層的文件。

detach()方法會斷開文件的最頂層并返回第二層,之后最頂層就沒什么用了。例如:

>>> 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
>>>

一旦斷開最頂層后,你就可以給返回結(jié)果添加一個新的最頂層。比如:

>>> f = io.TextIOWrapper(b, encoding='latin-1')
>>> f
<_io.TextIOWrapper name='sample.txt' encoding='latin-1'>
>>>

盡管已經(jīng)向你演示了改變編碼的方法, 但是你還可以利用這種技術(shù)來改變文件行處理、錯誤機(jī)制以及文件處理的其他方面。例如:

>>> sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='ascii',
...                             errors='xmlcharrefreplace')
>>> print('Jalape\u00f1o')
Jalape&#241;o
>>>

注意下最后輸出中的非 ASCII 字符 ? 是如何被&#241;取代的。

將字節(jié)寫入文本文件

問題

你想在文本模式打開的文件中寫入原始的字節(jié)數(shù)據(jù)。

解決方案

將字節(jié)數(shù)據(jù)直接寫入文件的緩沖區(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
>>>

類似的,能夠通過讀取文本文件的 buffer屬性來讀取二進(jìn)制數(shù)據(jù)。

討論

I/O 系統(tǒng)以層級結(jié)構(gòu)的形式構(gòu)建而成。 文本文件是通過在一個擁有緩沖的二進(jìn)制模式文件上增加一個 Unicode 編碼/解碼層來創(chuàng)建。 buffer 屬性指向?qū)?yīng)的底層文件。如果你直接訪問它的話就會繞過文本編碼/解碼層。

本小節(jié)例子展示的 sys.stdout 可能看起來有點(diǎn)特殊。 默認(rèn)情況下,sys.stdout 總是以文本模式打開的。 但是如果你在寫一個需要打印二進(jìn)制數(shù)據(jù)到標(biāo)準(zhǔn)輸出的腳本的話,你可以使用上面演示的技術(shù)來繞過文本編碼層。

將文件描述符包裝成文件對象

問題

你有一個對應(yīng)于操作系統(tǒng)上一個已打開的 I/O 通道(比如文件、管道、套接字等)的整型文件描述符, 你想將它包裝成一個更高層的 Python 文件對象。

解決方案

一個文件描述符和一個打開的普通文件是不一樣的。 文件描述符僅僅是一個由操作系統(tǒng)指定的整數(shù),用來指代某個系統(tǒng)的 I/O 通道。 如果你碰巧有這么一個文件描述符,你可以通過使用 open()函數(shù)來將其包裝為一個 Python 的文件對象。 你僅僅只需要使用這個整數(shù)值的文件描述符作為第一個參數(shù)來代替文件名即可。例如:

# 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)高層的文件對象被關(guān)閉或者破壞的時候,底層的文件描述符也會被關(guān)閉。 如果這個并不是你想要的結(jié)果,你可以給open()函數(shù)傳遞一個可選的 colsefd=False 。比如:

# Create a file object, but don't close underlying fd when done
f = open(fd, 'wt', closefd=False)
...

討論

在 Unix 系統(tǒng)中,這種包裝文件描述符的技術(shù)可以很方便的將一個類文件接口作用于一個以不同方式打開的 I/O 通道上, 如管道、套接字等。舉例來講,下面是一個操作管道的例子:

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ù)的一個特性,并且也只適用于基于 Unix 的系統(tǒng)。 如果你想將一個類文件接口作用在一個套接字并希望你的代碼可以跨平臺,請使用套接字對象的makefile()方法。 但是如果不考慮可移植性的話,那上面的解決方案會比使用 makefile() 性能更好一點(diǎn)。

你也可以使用這種技術(shù)來構(gòu)造一個別名,允許以不同于第一次打開文件的方式使用它。 例如,下面演示如何創(chuàng)建一個文件對象,它允許你輸出二進(jìn)制數(shù)據(jù)到標(biāo)準(zhǔn)輸出(通常以文本模式打開):

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()

盡管可以將一個已存在的文件描述符包裝成一個正常的文件對象, 但是要注意的是并不是所有的文件模式都被支持,并且某些類型的文件描述符可能會有副作用 (特別是涉及到錯誤處理、文件結(jié)尾條件等等的時候)。 在不同的操作系統(tǒng)上這種行為也是不一樣,特別的,上面的例子都不能在非 Unix 系統(tǒng)上運(yùn)行。 我說了這么多,意思就是讓你充分測試自己的實(shí)現(xiàn)代碼,確保它能按照期望工作。

創(chuàng)建臨時文件和文件夾

問題

你需要在程序執(zhí)行時創(chuàng)建一個臨時文件或目錄,并希望使用完之后可以自動銷毀掉。

解決方案

tempfile 模塊中有很多的函數(shù)可以完成這任務(wù)。 為了創(chuàng)建一個匿名的臨時文件,可以使用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

或者,如果你喜歡,你還可以像這樣使用臨時文件:

f = TemporaryFile('w+t')
# Use the temporary file
...
f.close()
# File is destroyed

TemporaryFile() 的第一個參數(shù)是文件模式,通常來講文本模式使用 w+t ,二進(jìn)制模式使用w+b。 這個模式同時支持讀和寫操作,在這里是很有用的,因?yàn)楫?dāng)你關(guān)閉文件去改變模式的時候,文件實(shí)際上已經(jīng)不存在了。 TemporaryFile()另外還支持跟內(nèi)置的 open()

下一篇:字符串和文本