當你用 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)
與字節共舞
有時,你可能需要以字節的形式,來處理文件。你只需在模式參數中,追加 b 即可,文件對象所提供的所有的方法,都一樣用,不同的是,這些方法的輸入和輸出,不再是字符串 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
好了,現在我知道了,當要在文件最后,追加內容的時候,我應該使用 a 模式,而非 w 模式。然而其時此刻,我已經陷入了一個誤區,就是所有我感覺要不斷的在文件后追加新的內容的時候,我都會用 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() 則只會不斷的在要寫入的文件后面,增加新的內容而已。
所以,換成用 w 模式 open() 文件,清空原始數據,然后不斷的用 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')
讀寫模式,支持讀取並修改文件內容,注意,這種模式下寫入的內容,是追加在文件末尾的。
后記
現在,我可以說,這是一篇我翻譯的文章。
翻譯,有三種手段。第一種最簡單,用 Chrome 瀏覽器右鍵翻譯,直接出來結果,這種類似的方式,稱之為【機翻】。第二種,在【機翻】的基礎上,再進行改錯、優化,修正一些【機翻】錯誤或不到位的地方。第三種,就是基於原文,以其整篇文章的框架、脈絡、核心內容為基礎,進行二次創作,增、刪、改 部分內容,以達到譯者想要的效果。比如這篇文章,我就刪除了部分過於簡單、直白的小白內容,像文件的相對路徑,也刪除了過於復雜和高階的內容,像自定義 Context Manager;增加了我在用 a 模式寫文件時的一些誤解和感悟,和 r+ 讀寫文件模式 等等;同時也修改、調整了原文的小部分內容,使之更合理和自然,能夠對初學者更友好。
我們經常說,翻譯有三重境界:信、達、雅。我是這樣理解的:信 是譯文要准確,不能有根本錯誤;達 是能夠讓讀者很容易的理解意思,簡單易懂,要做到足夠的本地化;雅 在前面的基礎之上,還能做到優雅、美妙,有藝術創作的成分。我自認為我這篇譯文,基本做到了信和達,雅的話,我覺得我那個小標題【與字節共舞】(原文:Working With Bytes)還勉強能算得上。
如果,你在讀這篇文章的時候,沒有感覺是在讀一篇有些別扭的文字,相反,讀起來行文流暢、通俗易懂,那么我的目的就達到了。我就是要讓人感覺不出,這是一篇翻譯的文章,以檢驗我在初級英文上的翻譯水准和創作能力【見笑見笑】。
