本文爲譯文,原文連接 read-write-files-python 本人博客: 編程禪師html
使用Python作的最多見的任務是讀取和寫入文件。不管是寫入簡單的文本文件,讀取複雜的服務器日誌,仍是分析原始的字節數據。全部這些狀況都須要讀取或寫入文件。python
在本教程中,你將學習:程序員
本教程主要面向初學者到中級的Python開發者,可是這裏有一些提示,更高級的程序員也能夠從中獲益。shell
在咱們開始研究如何使用Python中的文件以前,瞭解文件到底是什麼以及現代操做系統如何處理它們的某些方面是很是重要的。編程
從本質上講,文件是用於存儲數據的連續字節集。這些數據以特定格式組織,能夠是任何像文本文件同樣簡單的數據,也能夠像程序可執行文件同樣複雜。最後,這些字節文件被翻譯成二進制文件1
,0
以便計算機更容易處理。json
大多數現代文件系統上的文件由三個主要部分組成:bash
數據表示的內容取決於所使用的格式規範,一般由擴展名錶示。例如,擴展名爲.gif
的文件最可能符合圖形交換格式規範。有數百個(若是不是數千個)文件擴展名。對於本教程,你將只處理.txt
或.csv
文件擴展名。服務器
在操做系統上訪問文件時,須要文件路徑。文件路徑是表示文件位置的字符串。它分爲三個主要部分:app
/
(Unix)或反斜槓\
(Windows)分隔.
),用於表示文件類型這是一個簡單的例子。假設你有一個位於文件結構中的文件,以下所示:函數
/
│
├── path/
| │
│ ├── to/
│ │ └── cats.gif
│ │
│ └── dog_breeds.txt
|
└── animals.csv
複製代碼
假設您要訪問該cats.gif
文件,而且你當前的位置與文件夾中path
平級。要訪問該文件,你須要瀏覽該path
文件夾,而後查看to
文件夾,最後到達該cats.gif
文件。文件夾路徑是path/to/
。文件名是cats
。文件擴展名是.gif
。因此完整的道路是path/to/cats.gif
。
如今假設你當前的位置或當前工做目錄(cwd)位於咱們的示例文件夾結構的to
文件夾中。能夠經過文件名和擴展名簡單地引用文件cats.gif
,而不是引用cats.gif
完整路徑path/to/cats.gif
。
/
│
├── path/
| │
| ├── to/ ← Your current working directory (cwd) is here
| │ └── cats.gif ← Accessing this file
| │
| └── dog_breeds.txt
|
└── animals.csv
複製代碼
但對於dog_breeds.txt
如何進行訪問呢?若是不使用完整路徑,你將如何訪問?你可使用特殊字符雙點(..
)來向前移動一個目錄。這意味着能夠在to
目錄使用../dog_breeds.txt
引用dog_breeds.txt
文件。
/
│
├── path/ ← Referencing this parent folder
| │
| ├── to/ ← Current working directory (cwd)
| │ └── cats.gif
| │
| └── dog_breeds.txt ← Accessing this file
|
└── animals.csv
複製代碼
雙點(..
)能夠鏈接在一塊兒以遍歷當前目錄以前的多個目錄。例如,在to
文件夾中要訪問animals.csv
,你將使用../../animals.csv
。
處理文件數據時常常遇到的一個問題是新行或行結尾的表示。行結尾起源於莫爾斯電碼時代,使用一個特定的符號被用來表示傳輸的結束或一行的結尾。
後來,國際標準化組織(ISO)和美國標準協會(ASA)對電傳打字機進行了標準化。ASA標準規定行尾應使用回車(序列CR
或\r
)和換行(LF
或\n
)字符(CR+LF
或\r\n
)。然而,ISO標準容許CR+LF
字符或僅LF
字符。
Windows使用CR+LF
字符表示新行,而Unix和較新的Mac版本僅使用LF
字符。當你處理來源於不一樣操做系統上的文件時,這可能會致使一些複雜狀況。這是一個簡單的例子。假設咱們檢查在Windows系統上建立的文件dog_breeds.txt
:
Pug\r\n
Jack Russel 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設備上以不一樣方式解釋:
Pug\r
\n
Jack Russel 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
複製代碼
這可能會使每行重複出現問題,你可能須要考慮這樣的狀況。
你可能面臨的另外一個常見問題是字節數據的編碼。編碼是從字節數據到人類可讀字符的轉換。一般經過指定編碼的格式來完成。兩種最多見的編碼是ASCII和UNICODE格式。ASCII只能存儲128個字符,而Unicode最多可包含1,114,112個字符。
ASCII其實是Unicode(UTF-8)的子集,這意味着ASCII和Unicode共享相同的數值字符值。重要的是要注意,使用不正確的字符編碼解析文件可能會致使字符轉換失敗和出錯。例如,若是文件是使用UTF-8編碼建立的,而且你嘗試使用ASCII編碼對其進行解析,則若是存在超出這128個值的字符,則會引起錯誤。
當你想使用文件時,首先要作的就是打開它。該操做經過調用 open() 內置函數完成的。open()
有一個必需的參數,它是文件的路徑。open()
有一個返回,是這個文件的文件對象:
file = open('dog_breeds.txt')
複製代碼
打開文件後,接下來要學習的是如何關閉它。
**警告:**你應始終確保正確關閉打開的文件。
重要的是要記住,關閉文件是你的責任。在大多數狀況下,在應用程序或腳本終止時,文件最終將被關閉。可是,沒法保證明際上將會發生什麼。這可能致使沒必要要的行爲,包括資源泄漏。這也是Python(Pythonic)中的最佳實踐,以確保你的代碼以明肯定義的方式運行並減小任何不須要的行爲。
當你操做文件時,有兩種方法能夠確保文件正確關閉,即便遇到錯誤也是如此。關閉文件的第一種方法是使用try-finally
塊:
reader = open('dog_breeds.txt')
try:
# Further file processing goes here
finally:
reader.close()
複製代碼
若是你不熟悉try-finally
塊的內容,請查看Python Exceptions:An Introduction。
關閉文件的第二種方法是使用如下with
語句:
with open('dog_breeds.txt') as reader:
# Further file processing goes here
複製代碼
使用 with
語句,一旦離開了with
塊或甚至在錯誤的狀況下,系統也會自動關閉文件。我強烈建議你儘量使用with
語句,由於它的代碼更加清晰並使你更容易處理任何意外錯誤。
最有可能的是,你也想要使用第二個位置參數mode
。此參數是一個字符串,其中包含多個字符以表示你要如何打開文件。默認值和最多見的是'r'
,表示以只讀模式將文件做爲文本文件打開:
with open('dog_breeds.txt', 'r') as reader:
# Further file processing goes here
複製代碼
其餘模式請看在線文檔,但最經常使用的模式以下:
模式 | 含義 |
---|---|
'r' | 只讀模式打開(默認) |
‘w’ | 寫入模式打開,會覆蓋文件 |
'rb' 或 'wb' |
以二進制模式打開(使用字節數據讀/寫) |
讓咱們回過頭來談談文件對象。文件對象是:
「將面向文件的API(使用
read()
orwrite()
等方法)暴露給底層資源的對象。」(來源)
有三種不一樣類型的文件對象:
這些中每一種文件類型的都在io
模塊中定義。這裏簡要介紹了這三種類型。
文本文件是你將遇到的最多見的文件。如下是一些如何打開這些文件的示例:
open('abc.txt')
open('abc.txt', 'r')
open('abc.txt', 'w')
複製代碼
對於此類型的文件,open()
將返回一個TextIOWrapper
文件對象:
>>> file = open('dog_breeds.txt')
>>> type(file)
<class '_io.TextIOWrapper'>
複製代碼
這是open()
默認返回的文件對象。
緩衝二進制文件類型用於讀取和寫入二進制文件。如下是一些如何打開這些文件的示例:
open('abc.txt', 'rb')
open('abc.txt', 'wb')
複製代碼
對於此類型的文件,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'>
複製代碼
原始文件類型是:
「一般用做二進制和文本流的低級構建塊。」(來源)
所以一般不使用它。
如下是如何打開這些文件的示例:
open('abc.txt', 'rb', buffering=0)
複製代碼
對於此類型的文件,open()
將返回一個FileIO
文件對象:
>>> file = open('dog_breeds.txt', 'rb', buffering=0)
>>> type(file)
<class '_io.FileIO'>
複製代碼
打開文件後,你將須要讀取或寫入文件。首先,讓咱們來閱讀一個文件。能夠在文件對象上調用多種方法:
方法 | 描述 |
---|---|
.read(size=-1) |
這將根據size 字節數從文件中讀取。若是沒有傳遞參數或None 或-1 ,那麼整個文件被讀取。 |
.readline(size=-1) |
這將從該行讀取最多size 數量的字符。直到到行結尾,而後到下一行。若是沒有參數被傳遞或None 或-1 ,則整行(或行剩餘的部分)被讀出。 |
使用上面使用過的 dog_breeds.txt
文件,咱們來看一些如何使用這些方法的示例。如下是如何使用 .read()
命令打開和讀取整個文件的示例:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read & print the entire file
>>> print(reader.read())
Pug
Jack Russel Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
複製代碼
這是一個如何使用.readline()
在一行中每次讀取5個字節的示例:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> # Read & print the first 5 characters of the line 5 times
>>> print(reader.readline(5))
>>> # Notice that line is greater than the 5 chars and continues
>>> # down the line, reading 5 chars each time until the end of the
>>> # line and then "wraps" around
>>> print(reader.readline(5))
>>> print(reader.readline(5))
>>> print(reader.readline(5))
>>> print(reader.readline(5))
Pug
Jack
Russe
l Ter
rier
複製代碼
譯者注:第一次調用reader.readline(5) 實際打印出 Pug\r\n,所以能夠看到有輸出一個換行
如下是使用.readlines()
將整個文件做爲列表讀取的示例:
>>> f = open('dog_breeds.txt')
>>> f.readlines() # Returns a list object
['Pug\n', 'Jack Russel 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']
複製代碼
上面的例子也能夠經過使用list()
從文件對象建立列表來完成:
>>> f = open('dog_breeds.txt')
>>> list(f)
['Pug\n', 'Jack Russel 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']
複製代碼
讀取文件時常見的事情是迭代每一行。如下是如何使用.readline()
執行該迭代的示例:
>>> 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 Russel Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
複製代碼
迭代文件中每一行的另外一種方法是使用.readlines()
文件對象。請記住,.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
複製代碼
可是,經過迭代文件對象自己能夠進一步簡化上述示例:
>>> 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 Russel Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
複製代碼
最後的方法更Pythonic,能夠更快,更高效。所以,建議你改用它。
**注意:**上面的一些示例包含
print('some text', end='')
。這end=''
是爲了防止Python爲正在打印的文本添加額外的換行符,並僅打印從文件中讀取的內容。
如今讓咱們深刻研究文件。與讀取文件同樣,文件對象有多種方法可用於寫入文件:
方法 | 描述 |
---|---|
.write(string) | 將字符串寫入文件。 |
.writelines(seq) | 將序列寫入文件。不會給每一個序列項附加結尾符。這會由你來添加適當的結尾符。 |
如下是使用.write()
和的簡單示例.writelines()
:
with open('dog_breeds.txt', 'r') as reader:
# Note: readlines doesn't trim the line endings
dog_breeds = reader.readlines()
with open('dog_breeds_reversed.txt', 'w') as writer:
# Alternatively you could use
# writer.writelines(reversed(dog_breeds))
# Write the dog breeds to the file in reversed order
for breed in reversed(dog_breeds):
writer.write(breed)
複製代碼
有時,你可能須要使用字節字符串處理文件。能夠經過在mode
參數中添加'b'
字符來完成。適用於文件對象的全部相同方法。可是,每一個方法都指望並返回一個bytes
對象:
>>> with open(`dog_breeds.txt`, 'rb') as reader:
>>> print(reader.readline())
b'Pug\n'
複製代碼
使用b
標誌打開文本文件並不那麼有趣。假設咱們有一張Jack Russell Terrier(jack_russell.png
)的可愛圖片:
你能夠在Python中打開該文件並檢查內容!因爲.png
文件格式定義的那樣,文件的標題是8個字節,以下所示:
值 | 描述 |
---|---|
0x89 | 一個「魔術」數字,表示這是一個PNG 的開始 |
0x50 0x4E 0x47 | PNG 的ASCII |
0x0D 0x0A | DOS樣式行結束 \r\n |
0x1A | DOS風格的EOF字符 |
0x0A | 一個Unix風格的行結尾 \n |
當打開文件並單獨讀取這些字節時,能夠看到這確實是一個.png
頭文件:
>>> 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'
複製代碼
讓咱們把知識點整理一下,看看如何讀取和寫入文件的完整示例。下面是一個dos2unix
相似的工具,將其轉換一個文件,將它的的行結束\r\n
轉爲\n
。
該工具分爲三個主要部分。第一個是str2unix()
將字符串從\\r\\n
行結尾轉換爲\\n
。第二個是dos2unix()
將包含\r\n
字符的字符串轉換爲\n
。dos2unix()
調用str2unix()
。最後,有__main__
塊,只有當文件做爲腳本執行時纔會調用。
""" A simple script and library to convert files or strings from dos like line endings with Unix like line endings. """
import argparse
import os
def str2unix(input_str: str) -> str:
r"""\ Converts the string from \r\n line endings to \n Parameters ---------- input_str The string whose line endings will be converted Returns ------- The converted string """
r_str = input_str.replace('\r\n', '\n')
return r_str
def dos2unix(source_file: str, dest_file: str):
"""\ Coverts a file that contains Dos like line endings into Unix like Parameters ---------- source_file The path to the source file to be converted dest_file The path to the converted file for output """
# NOTE: Could add file existence checking and file overwriting
# protection
with open(source_file, 'r') as reader:
dos_content = reader.read()
unix_content = str2unix(dos_content)
with open(dest_file, 'w') as writer:
writer.write(unix_content)
if __name__ == "__main__":
# Create our Argument parser and set its description
parser = argparse.ArgumentParser(
description="Script that converts a DOS like file to an Unix like file",
)
# Add the arguments:
# - source_file: the source file we want to convert
# - dest_file: the destination where the output should go
# Note: the use of the argument type of argparse.FileType could
# streamline some things
parser.add_argument(
'source_file',
help='The location of the source '
)
parser.add_argument(
'--dest_file',
help='Location of dest file (default: source_file appended with `_unix`',
default=None
)
# Parse the args (argparse automatically grabs the values from
# sys.argv)
args = parser.parse_args()
s_file = args.source_file
d_file = args.dest_file
# If the destination file wasn't passed, then assume we want to
# create a new file based on the old one
if d_file is None:
file_path, file_extension = os.path.splitext(s_file)
d_file = f'{file_path}_unix{file_extension}'
dos2unix(s_file, d_file)
複製代碼
如今你已經掌握了讀取和寫入文件的基礎知識,這裏有一些提示和技巧能夠幫助你提升技能。
__file__
該__file__
屬性是模塊的特殊屬性,相似於__name__
。它是:
「若是是從文件加載的,它就爲加載模塊的文件的路徑名,」(來源)
注意:
__file__
返回相對於調用初始Python腳本的路徑。若是須要完整的系統路徑,可使用os.getcwd()
獲取執行代碼的當前工做目錄。
這是一個真實的例子。在我過去的一份工做中,我對硬件設備進行了屢次測試。每一個測試都是使用Python腳本編寫的,測試腳本文件名用做標題。而後將執行這些腳本並使用__file__
特殊屬性打印其狀態。這是一個示例文件夾結構:
project/
|
├── tests/
| ├── test_commanding.py
| ├── test_power.py
| ├── test_wireHousing.py
| └── test_leds.py
|
└── main.py
複製代碼
運行main.py
產生如下內容:
>>> python main.py
tests/test_commanding.py Started:
tests/test_commanding.py Passed!
tests/test_power.py Started:
tests/test_power.py Passed!
tests/test_wireHousing.py Started:
tests/test_wireHousing.py Failed!
tests/test_leds.py Started:
tests/test_leds.py Passed!
複製代碼
有時,你可能但願追加到文件或在已有文件的末尾開始寫入。這能夠經過在參數mode
中追加'a'
字符來完成:
with open('dog_breeds.txt', 'a') as a_writer:
a_writer.write('\nBeagle')
複製代碼
當對dog_breeds.txt
再次檢查時,你將看到文件的開頭未更改,Beagle
如今已添加到文件的末尾:
>>> with open('dog_breeds.txt', 'r') as reader:
>>> print(reader.read())
Pug
Jack Russel Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
Beagle
複製代碼
有時你可能想要讀取文件並同時寫入另外一個文件。若是你使用在學習如何寫入文件時顯示的示例,它實際上能夠合併到如下內容中:
d_path = 'dog_breeds.txt'
d_r_path = 'dog_breeds_reversed.txt'
with open(d_path, 'r') as reader, open(d_r_path, 'w') as writer:
dog_breeds = reader.readlines()
writer.writelines(reversed(dog_breeds))
複製代碼
有時候,你可能須要經過將文件對象放在自定義類中來更好地控制文件對象。執行此操做時,除非添加一些魔術方法,不然沒法再使用with
語句:經過添加__enter__
和__exit__
,你將建立所謂的上下文管理器。
__enter__()
調用with
語句時調用。__exit__()
從with
語句塊退出時被調用。
這是一個可用於製做自定義類的模板:
class my_file_reader():
def __init__(self, file_path):
self.__path = file_path
self.__file_object = None
def __enter__(self):
self.__file_object = open(self.__path)
return self
def __exit__(self, type, val, tb):
self.__file_object.close()
# Additional methods implemented below
複製代碼
如今你已經擁有了帶有上下文管理器的自定義類,你能夠與使用內置open()
那樣使用它:
with my_file_reader('dog_breeds.txt') as reader:
# Perform custom class operations
pass
複製代碼
這是一個很好的例子。還記得咱們有可愛的Jack Russell形象嗎?也許你想打開其餘.png
文件,但不想每次都解析頭文件。這是一個如何作到這一點的例子。此示例還使用自定義迭代器。若是你不熟悉它們,請查看Python迭代器:
class PngReader():
# Every .png file contains this in the header. Use it to verify
# the file is indeed a .png.
_expected_magic = b'\x89PNG\r\n\x1a\n'
def __init__(self, file_path):
# Ensure the file has the right extension
if not file_path.endswith('.png'):
raise NameError("File must be a '.png' extension")
self.__path = file_path
self.__file_object = None
def __enter__(self):
self.__file_object = open(self.__path, 'rb')
magic = self.__file_object.read(8)
if magic != self._expected_magic:
raise TypeError("The File is not a properly formatted .png file!")
return self
def __exit__(self, type, val, tb):
self.__file_object.close()
def __iter__(self):
# This and __next__() are used to create a custom iterator
# See https://dbader.org/blog/python-iterators
return self
def __next__(self):
# Read the file in "Chunks"
# See https://en.wikipedia.org/wiki/Portable_Network_Graphics#%22Chunks%22_within_the_file
initial_data = self.__file_object.read(4)
# The file hasn't been opened or reached EOF. This means we
# can't go any further so stop the iteration by raising the
# StopIteration.
if self.__file_object is None or initial_data == b'':
raise StopIteration
else:
# Each chunk has a len, type, data (based on len) and crc
# Grab these values and return them as a tuple
chunk_len = int.from_bytes(initial_data, byteorder='big')
chunk_type = self.__file_object.read(4)
chunk_data = self.__file_object.read(chunk_len)
chunk_crc = self.__file_object.read(4)
return chunk_len, chunk_type, chunk_data, chunk_crc
複製代碼
你如今能夠打開.png
文件,並使用自定義上下文管理器正確解析它們:
>>> with PngReader('jack_russell.png') as reader:
>>> for l, t, d, c in reader:
>>> print(f"{l:05}, {t}, {c}")
00013, b'IHDR', b'v\x121k'
00001, b'sRGB', b'\xae\xce\x1c\xe9'
00009, b'pHYs', b'(<]\x19'
00345, b'iTXt', b"L\xc2'Y"
16384, b'IDAT', b'i\x99\x0c('
16384, b'IDAT', b'\xb3\xfa\x9a$'
16384, b'IDAT', b'\xff\xbf\xd1\n'
16384, b'IDAT', b'\xc3\x9c\xb1}'
16384, b'IDAT', b'\xe3\x02\xba\x91'
16384, b'IDAT', b'\xa0\xa99='
16384, b'IDAT', b'\xf4\x8b.\x92'
16384, b'IDAT', b'\x17i\xfc\xde'
16384, b'IDAT', b'\x8fb\x0e\xe4'
16384, b'IDAT', b')3={'
01040, b'IDAT', b'\xd6\xb8\xc1\x9f'
00000, b'IEND', b'\xaeB`\x82'
複製代碼
在處理文件時可能會遇到常見狀況。大多數狀況可使用其餘模塊處理。您可能須要使用的兩種常見文件類型是.csv
和.json
。Real Python已經彙總了一些關於如何處理這些內容的精彩文章:
此外,還有內置庫,可使用它們來幫助你:
.plist
文件還有更多的東西。此外,PyPI還有更多第三方工具可用。一些流行的是如下:
你如今知道如何使用Python處理文件,包括一些高級技術。使用Python中的文件如今比以往任什麼時候候都更容易,當你開始這樣作時,這是一種有益的感受。
在本教程中,你已經瞭解到:
關注公衆號 <代碼與藝術>,學習更多國外精品技術文章。