當你用 Python 寫程式時,不論是簡單的指令碼,還是複雜的大型專案,其中最常見的操作就是讀寫檔案。不管是簡單的文字檔案、繁雜的日誌檔案,還是分析圖片等媒體檔案中的位元組資料,都需要用到 Python 中的檔案讀寫。
本文包含以下內容
- 檔案的構成部分
- Python 讀寫檔案的基本操作
- 在一些場景下讀寫檔案的技巧
這篇文章主要是面向 Python 的初級和中級開發者的,當然高階開發者也可能從中有所收穫 : )
檔案由哪些部分構成的?
在我們正式進入 Python 檔案讀寫之前,首要的一件事,是要搞明白,到底什麼是檔案,以及作業系統如何識別不同檔案的。
從根本上講,檔案實際上就是一組連續的位元組儲存下來的資料。這些資料,基於某些規範,組織成了不同的檔案型別,可以是一個簡單的文字檔案,異或是複雜的可執行程式檔案。但其實最終,不管它們原來是何種檔案型別,最終都會被計算機翻譯成 1
和 0
這種二機制的表示,以交給 CPU
進行資料處理。
在現代的大部分的檔案系統中,檔案由以下3個部分構成:
- 檔案頭: 檔案的元資料【檔名、大小、型別 等等】
- 檔案資料: 由檔案的建立者或編輯者,編輯的內容【比如:文字、圖片、音訊、視訊 內容等等】
- 檔案結束: 由特殊的字元來標記出來,這是檔案的結束了
檔案所表示的到底是什麼資料,具體由其 型別
所決定,通常情況下,體現在副檔名上【當然這主要在 windows
中較為常見,linux
中則對副檔名不是那麼的在意】。例如,如果一個檔案的副檔名是 .gif
,那麼通常情況下,它可能是一個動圖【極端情況下,它可能不是動圖,而是一個病毒或惡意指令碼程式】。檔案的副檔名型別有成百上千個,在本文中,你只需要操作 .txt
文字檔案。
檔案的路徑
當你訪問一個檔案的時候,檔案的路徑是必需的。檔案的路徑就是一個字串,代表了它所在檔案系統中的位置,它由以下3個部分組成:
- 檔案目錄: 檔案所處的目錄名稱,在
windows
系統中,多個目錄由\
分隔,在unix
系統中,由/
分隔 - 檔名: 副檔名如
.txt
前面的名稱,如果沒有副檔名,則整個都是檔名 - 副檔名: 最後一個
.
和後面的字元,組成擴充套件
注意換行符的不同
在處理檔案資料時,我們經常遇到的一個問題,就是 換行符
的不同。美國標準協會規定了換行符是由 \r\n
組成,這在 windows
系統上,是通行的換行符標準,而在 unix
系統上,像各種 linux
發行版 和 mac
,換行符是 \n
,這就給我們程式設計師在判斷和處理換行符時,帶來了麻煩,尤其是當你寫出的程式,需要相容 windows
和 unix
的時候。
讓我們來看下面這個例子,這是一個在 windows
上建立的,描述狗的品種的檔案:dog_breeds.txt
Pug\r\n
Jack Russell Terrier\r\n
English Springer Spaniel\r\n
German Shepherd\r\n
Staffordshire Bull Terrier\r\n
Cavalier King Charles Spaniel\r\n
Golden Retriever\r\n
West Highland White Terrier\r\n
Boxer\r\n
Border Terrier\r\n
它的換行符,明顯是 \r\n
,那麼在 unix
系統上,它將顯示成這樣:
Pug\r
\n
Jack Russell Terrier\r
\n
English Springer Spaniel\r
\n
German Shepherd\r
\n
Staffordshire Bull Terrier\r
\n
Cavalier King Charles Spaniel\r
\n
Golden Retriever\r
\n
West Highland White Terrier\r
\n
Boxer\r
\n
Border Terrier\r
\n
當你在 unix
系統上,執行你寫的 Python 程式的時候,你以為的換行符 \n
就不是你以為的了,每一行內容後面,都會多一個 \r
,這讓你的程式處理每行文字的時候,都要多一些相容性處理。
字元編碼
你極有可能遇到的另一個問題,是字元編碼問題。字元編碼實際上是計算機把二機制的位元組資料,轉換成人類可以看明白的字元的過程。字元編碼後,通常由一個整型數字來代表一個字元,像最常見的 ascii
和 unicode
字元編碼方式。
ascii
是 unicode
的子集,也就是說,它們共用相同的字符集,只不過 unicode
所能表示的字元數量,要比 ascii
多的多。值得注意的是,當你用一個錯誤的編碼方式,解析一個檔案內容的時候,通常會得到意想不到的後果。比如,一個檔案的內容是 utf-8
編碼的,而你用 ascii
的編碼方式去解析讀取此檔案內容,那麼,你大概率會得到一個滿是亂碼的文字內容。
檔案的開啟和關閉
當你想在 Python 中處理檔案的時候,首要的事情,就是用 open()
開啟檔案。open()
是 Python 的內建函式,它需要一個必要引數來指定檔案路徑,然後返回檔案物件:
file = open('dog_breeds.txt')
當你學會開啟檔案之後,你下一個要知道的是,如何關閉它。
給你一個忠告,在每次 open()
處理完檔案後,你一定要記得關閉它。雖然,當你寫的應用程式或指令碼,在執行完畢後,會自動的關閉檔案,回收資源,但你並不確定在某些意外情況下,這一定會執行。這就有可能導致資源洩漏。確保你寫的程式,有著合理的結構,清晰的邏輯,優雅的程式碼 和 不再使用的資源的釋放,是一個新時代IT農民工必備的優秀品質【手動狗頭】。
當你在處理檔案的時候,有2種方式,能夠確保你的檔案一定會被關閉,即使在出現異常的時候。
第一種方式,是使用 try-finally
異常處理:
reader = open('dog_breeds.txt')
try:
# Further file processing goes here
finally:
reader.close()
第二種方式,是使用 with statement
語句:
with open('dog_breeds.txt') as reader:
# Further file processing goes here
with
語句的形式,可以確保你的程式碼,在執行到離開 with
結構的時候,自動的執行關閉操作,即使在 with
程式碼塊中出現了異常。我極度的推薦這種寫法,因為這會讓你的程式碼很簡潔,並且在意想不到的異常處理上,也無需多做考慮。
通常情況下,你會用到 open()
的第2個引數 mode
,它用字串來表示,你想要用什麼方式,來開啟檔案。預設值是 r
代表用 read-only
只讀的方式,開啟檔案:
with open('dog_breeds.txt', 'r') as reader:
# Further file processing goes here
除了 r
之外,還有一些 mode
引數值,這裡只簡要的列出一些常用的:
Character | Meaning |
---|---|
'r' | 只讀的方式開啟檔案 (預設方式) |
'w' | 只寫的方式開啟檔案, 並且在檔案開啟時,會清空原來的檔案內容 |
'rb' or 'wb' | 二進位制的方式開啟檔案 (讀寫位元組資料) |
現在讓我們回過頭,來談一談 open()
之後,返回的檔案物件:
“an object exposing a file-oriented API (with methods such as read() or write()) to an underlying resource.”
檔案物件分為3類:
- Text files
- Buffered binary files
- Raw binary files
Text File Types
文字檔案是你最常遇到和處理的,當你用 open()
開啟文字檔案時,它會返回一個 TextIOWrapper
檔案物件:
>>> file = open('dog_breeds.txt')
>>> type(file)
<class '_io.TextIOWrapper'>
Buffered Binary File Types
Buffered binary file type 用來以二進位制的形式操作檔案的讀寫。當用 rb
的方式 open()
檔案後,它會返回 BufferedReader
或 BufferedWriter
檔案物件:
>>> file = open('dog_breeds.txt', 'rb')
>>> type(file)
<class '_io.BufferedReader'>
>>> file = open('dog_breeds.txt', 'wb')
>>> type(file)
<class '_io.BufferedWriter'>
Raw File Types
Raw file type 的官方定義是:
“generally used as a low-level building-block for binary and text streams.”
說實話,它並不常用,下面是一個示例:
>>> file = open('dog_breeds.txt', 'rb', buffering=0)
>>> type(file)
<class '_io.FileIO'>
你可以看到,當你用 rb
的方式 open()
檔案,並且 buffering=0
時,返回的是 FileIO
檔案物件。
檔案的讀和寫
下面,終於進入正題了。
當你開啟一個檔案的時候,實際上,你是想 讀 或是 寫 檔案。首先,讓我們先來看讀檔案,下面是一些 open()
返回的檔案物件,可以呼叫的方法:
Method | What It Does |
---|---|
.read(size=-1) | This reads from the file based on the number of size bytes. If no argument is passed or None or -1 is passed, then the entire file is read. |
.readline(size=-1) | This reads at most size number of characters from the line. This continues to the end of the line and then wraps back around. If no argument is passed or None or -1 is passed, then the entire line (or rest of the line) is read. |
.readlines() | This reads the remaining lines from the file object and returns them as a list. |
以上文提到的 dog_breeds.txt
文字檔案作為讀取目標,下面來演示如何用 read()
讀取整個檔案內容:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read & print the entire file
>>> print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
下面的例子,通過 readline()
每次只讀取一行內容:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> print(reader.readline())
>>> print(reader.readline())
Pug
Jack Russell Terrier
下面是通過 readlines()
讀取檔案的全部內容,並返回一個 list
列表物件:
>>> f = open('dog_breeds.txt')
>>> f.readlines() # Returns a list object
['Pug\n', 'Jack Russell Terrier\n', 'English Springer Spaniel\n', 'German Shepherd\n', 'Staffordshire Bull Terrier\n', 'Cavalier King Charles Spaniel\n', 'Golden Retriever\n', 'West Highland White Terrier\n', 'Boxer\n', 'Border Terrier\n']
以迴圈的方式,讀取檔案中的每一行
其實,最常見的操作,是以迴圈迭代的方式,一行行的讀取檔案內容,直至檔案結尾。
下面是一個初學者經常會寫出來的典型範例【包括幾天前的我自己 】:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read and print the entire file line by line
>>> line = reader.readline()
>>> while line != '': # The EOF char is an empty string
>>> print(line, end='')
>>> line = reader.readline()
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
而另一種寫法,則是用 readlines()
來實現的,說實話,這比上面的那種,要好不少:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> for line in reader.readlines():
>>> print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
需要注意的是,readlines()
返回的是一個 list
列表物件,它裡面的每個元素,就代表著文字檔案的每一行內容。
然而,上面的2種寫法,都可以用下面這樣,直接迴圈迭代檔案物件自身的方式,更簡單的實現:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read and print the entire file line by line
>>> for line in reader:
>>> print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
這最後一種實現方式,更具 Python 風格,更高效,所以這是我推薦給你的最佳實現。
寫檔案
現在,讓我們來看看如何寫檔案。就像讀取檔案一樣,寫檔案也有一些好用的方法供我們使用:
Method | What It Does |
---|---|
.write(string) | This writes the string to the file. |
.writelines(seq) | This writes the sequence to the file. No line endings are appended to each sequence item. It’s up to you to add the appropriate line ending(s). |
下面是一個分別使用 write()
和 writelines()
寫檔案的示例:
# 先從原始檔案中讀取狗的品種
with open('dog_breeds.txt', 'r') as reader:
# Note: readlines doesn't trim the line endings
dog_breeds = reader.readlines()
# 以 w 的模式,開啟要寫入的新檔案
with open('dog_breeds_reversed.txt', 'w') as writer:
# 實現方式一
# writer.writelines(reversed(dog_breeds))
# 實現方式二,將讀取到的狗的品種,寫入新檔案,並且用了 reversed() 函式,將原文的順序進行了反轉
for breed in reversed(dog_breeds):
writer.write(breed)
與位元組共舞
有時,你可能需要以位元組的形式,來處理檔案。你只需在模式引數中,追加 r
即可,檔案物件所提供的所有的方法,都一樣用,不同的是,這些方法的輸入和輸出,不再是字串 str
物件,而是位元組 bytes
物件。
這是一個簡單的示例:
>>> with open('dog_breeds.txt', 'rb') as reader:
>>> print(reader.readline())
b'Pug\n'
使用 b
模式處理文字檔案,並沒什麼特別的花樣,讓我們來看看,處理圖片,會不會比較有意思一點,像下面這樣一條狗狗的 jack_russell.png
圖片:
你可以寫 Python 程式碼,讀取這張圖片,然後檢查它的內容。如果一個 png
圖片是正兒八經的,那麼它的檔案頭部內容,是8個位元組,分別由以下部分組成:
Value | Interpretation |
---|---|
0x89 | 其實就是一個魔術數字,代表這是一個PNG圖片的開頭 |
0x50 0x4E 0x47 | 以 ASCII 碼錶示的【PNG】這3個字母 |
0x0D 0x0A | DOS 風格的換行符 \r\n |
0x1A | DOS 風格的 EOF 字元 |
0x0A | Unix 風格的換行符 \n |
如果,你用下面的程式碼,讀取這張圖片的話,你會發現,它確實是個正兒八經的 png
圖片,因為它檔案頭部的8個位元組,同上表一致:
>>> with open('jack_russell.png', 'rb') as byte_reader:
>>> print(byte_reader.read(1))
>>> print(byte_reader.read(3))
>>> print(byte_reader.read(2))
>>> print(byte_reader.read(1))
>>> print(byte_reader.read(1))
b'\x89'
b'PNG'
b'\r\n'
b'\x1a'
b'\n'
一些小技巧和我的新的領悟
現在,你掌握了檔案讀寫的基本操作,這些完全夠你用的了,正所謂這20%的技能,就能覆蓋80%的使用場景。下面說一下上文沒有提到的,但是使用時,也經常會用到的一些技巧,和我對於某些方面的新的領悟。
在要寫入的檔案後,追加內容
有時,你需要在要寫入的檔案後,追加內容,而不是像之前的 w
模式,先把原檔案清空了再寫入。此時,可以用 a
模式:
with open('dog_breeds.txt', 'a') as a_writer:
a_writer.write('\nBeagle')
當你再次用 Python 程式碼讀取,或是直接開啟這個文字檔案的時候,你會發現原始內容還在,只是在最後追加了 Beagle
:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
Beagle
好了,現在我知道了,當要在檔案最後,追加內容的時候,我應該使用 w
模式,而非 a
模式。然而其時此刻,我已經陷入了一個誤區,就是所有我感覺要不斷的在檔案後追加新的內容的時候,我都會用 a
模式,而這在一些場景下面,是不合時宜的。
比如,我下面要做這樣一件事,讀取 dog_breeds.txt
狗的品種,計算每一行字元的長度,把 品種
和 品種的字元長度
寫入新的檔案。因為是每讀取一行,就每寫入一行,這裡我通常會順其自然的想到用 a
模式,追加寫入的新的檔案中:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> with open('new_dog_breeds.txt', 'a') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因為讀取到的每一行,是包含換行符的,所以,這裡要先把最後面的換行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')
這個程式,只執行一次,固然沒有問題,但是,如果我現在修改了 dog_breeds.txt
原始檔案,在裡面增加了一些狗的品種,想再次執行這個程式,生成新的結果的時候,我必須先把之前儲存結果的 new_dog_breeds.txt
檔案內容清空,再去執行。否則,第二次執行的結果,會追加在 new_dog_breeds.txt
檔案原有的內容後面,導致老的內容重複了,這不是我想要的。
其實,我正是我對 w
和 a
的誤解。要解決這個不大,但確實是有點小麻煩的問題,其實,我們只需把上面程式碼中,第二行開啟寫入檔案時的 a
模式,換成 w
模式即可:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> with open('new_dog_breeds.txt', 'w') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因為讀取到的每一行,是包含換行符的,所以,這裡要先把最後面的換行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')
這樣,不管我的程式執行多少遍,寫入的新檔案中,都只會有原始檔案中所有狗的品種和長度,而再也不會出現上述問題。
之所以我犯了這個看起來無關緊要的錯誤,就是因為我先前對於 w
的誤解:我以為 w
是在寫之前,要清空原始內容,準確的說,是在呼叫 writer.write()
的時候,會清空原始內容,其實並不是;其實 w
模式是在 open()
的時候清空的,而 writer.write()
則並不會清空,不斷的 write()
則只會不斷的在要寫入的檔案後面,增加新的內容而已。
你看,這就是我的新的領悟,還是小有所獲的吧。如果你也像我之前一樣,我想你也有了同樣的頓悟。
讀檔案和寫檔案,程式碼放在一行
就像上面的程式碼,其實的一邊讀,一邊寫,每讀取一行,處理後就寫入一行,那麼這2個檔案的 open()
操作,可以放到一行,使得程式碼結構更清爽一點:
>>> with open('dog_breeds.txt', 'r') as reader, with open('new_dog_breeds.txt', 'w') as writer:
>>> for line in reader:
>>> dog = line.rstrip('\n') # 因為讀取到的每一行,是包含換行符的,所以,這裡要先把最後面的換行符清除掉
>>> writer.write(dog + ' ' + len(dog) + '\n')
讀取和寫入同一個檔案
r
模式,是隻讀模式,w
和 a
模式,是隻寫模式。有時,我們想從一個檔案中讀取內容,進行計算或其他處理後,再寫入同一個檔案中,對於這種場景,我們可以用 r+
模式,即讀寫模式:
>>> with open('dog_breeds.txt', 'r+') as file:
>>> for line in file:
>>> dog = line.rstrip('\n') # 因為讀取到的每一行,是包含換行符的,所以,這裡要先把最後面的換行符清除掉
>>> file.write(dog + ' ' + len(dog) + '\n')
讀寫模式,支援讀取並修改檔案內容,注意,這種模式下寫入的內容,是追加在檔案末尾的。
後記
現在,我可以說,這是一篇我翻譯的文章。
翻譯,有3種手段。第一種最簡單,用 Chrome 瀏覽器右鍵翻譯,直接出來結果,這種類似的方式,稱之為【機翻】。第二種,在【機翻】的基礎上,再進行改錯、優化,修正一些【機翻】錯誤或不到位的地方。第三種,就是基於原文,以其整篇文章的框架、脈絡、核心內容為基礎,進行二次創作,增、刪、改 部分內容,以達到譯者想要的效果。比如這篇文章,我就刪除了部分過於簡單、直白的小白內容,像檔案的相對路徑,也刪除了過於複雜和高階的內容,像自定義 Context Manager
;增加了我在用 a
模式寫檔案時的一些誤解和感悟,和 r+
讀寫檔案模式;同時也修改、調整了原文的小部分內容,使之更合理和自然,能夠對初學者更友好。
我們經常說,翻譯有3重境界:信、達、雅。我是這樣理解的:信 是譯文要準確,不能有根本錯誤;達 是能夠讓讀者很容易的理解意思,簡單易懂,要做到足夠的本地化;雅 在前面的基礎之上,還能做到優雅、美妙,有藝術創作的成分。我自認為我這篇譯文,基本做到了信和達,雅的話,我覺得我那個小標題【與位元組共舞】(原文:Working With Bytes)還勉強能算得上。
如果,你在讀這篇文章的時候,沒有感覺是在讀一篇有些彆扭的文字,相反,讀起來行文流暢、通俗易懂,那麼我的目的就達到了。我就是要讓人感覺不出,這是一篇翻譯的文章,以檢驗我在初級英文上的翻譯水準和創作能力【見笑見笑】。
原文出處:https://realpython.com/read-write-files-python