檔案處理
在基礎篇我們介紹了 Python 程式語言的功能,例如資料型態、邏輯判斷、函數以及物件導向等,接下來我們就用這些學到的語法來做點比較有趣的事,首先先從檔案的處理開始。
讀取檔案
在 Python 讀取檔案非常簡單,只要使用內建的 open()
函數就可以了。翻一下手冊會發現這個函數有好幾個參數:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
這麼多參數只有第一個 file
參數是必填的,第二個參數 mode
是指定檔案的開啟模式,預設值 r
代表使用讀取 模式,表示只能讀不能寫入。另外比較常見的模式還有「寫入模式」的 w
以及「附加模式」的 a
。我們先來試試看如何讀取一個檔案:
f = open("hello-world.txt")
這裡我只給了檔案的名字,如果像上面這樣寫就是找在同一個目錄底下的 hello-world.txt
檔案。如果不在同一層目錄,也可使用絕對路徑像是 /tmp/demo/hello-world.txt
或是相對路徑 ../hello-world.txt.py
的方式,只要能找的到就好,函數的第二個參數沒寫,所以就是預設的讀取模式。執行 open()
函數會回傳一個檔案物件,待會可 以用這個物件進行操作檔案,但執行之後卻發現錯誤訊息:
Traceback (most recent call last):
File "/demo.py", line 1, in <module>
f = open("hello-world.txt")
^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'hello-world.txt'
錯誤訊息很明顯告訴我們沒有這個檔案,這滿合理的。要解決這個問題,我們可以手動建立這個檔案,或是使用前面章節學過的 try...except...
來處理這個問題。如果在執行 open()
函數的時候,檔案模式設定成 a
或 w
模式的 話,檔案不存在的時候不會出錯,Python 會自動幫我們建立一個。這裡我就不另外建立檔案,直接使用這個練習檔案(檔名 demo.py
)本身當做讀取的範例,也就是要寫程式讀取自己的意思:
f = open("demo.py")
open()
函數回傳的檔案物件有好幾種方法可以把檔案內容讀取出來,首先,因為檔案物件本身就是一個可迭代物件,所以透過 for
迴圈把內容一行一行印出來:
f = open("demo.py")
# 這是註解
# 這行也是註解
for line in f:
print(line, end="")
執行之後應該就會在畫面上看到這個檔案的內容。各位應該會發現我故意在檔案裡面加了兩行註解,照理說程式在執行的時候註解會被當做沒看到,但為什麼這裡執行還是會被印出來?的確,Python 程式在編譯以及執行的時候會無視註解,但我們這裡是把檔案「讀」出來,並不是執行這個檔案。同樣是一行一行讀取,我們也可使用這個檔案物件身上的 .readlines()
方法:
>>> f = open("demo.py")
>>> f.readlines()
['f = open("demo.py")\n',
'\n',
'# 這是註解\n',
'# 這行也是註解\n',
'\n',
'for line in f:\n',
' print(line, end="")\n']
這裡我稍微手動整理輸出的結果讓大家看的清楚一點。檔案物件的 .readlines()
方法會把檔案的內容一行一行拆開並且組裝成一個串列,至於拿到串列之後要怎麼呈現,就看大家自己的創意了。一樣是檔案物件的方法,有個更簡單也更常見 .read()
方法:
f = open("demo.py")
# 這是註解
# 這行也是註解
print(f.read())
方法名稱很直覺,這會把整個檔案的內容讀出來。
游標的位置?
不管是用 .readlines()
或是 .read()
方法,當檔案讀取完畢之後,檔案物件的「游標(cursor)」會停在檔案的最後一行,我這裡說的游標不是指電腦滑鼠的游標,這比較像是進度條上的指針,當逐行讀取檔案的時候,指針會隨著讀取的進度移動位置,讀到哪裡就移動到哪裡。當讀取完畢之後,這個指針會停在檔案的最後一行。這時如果再繼續呼叫 .readlines()
或 .read()
方法的話,就會發現讀不到任何東西了。
舉例來說,我有個暢銷書的檔案叫做 best_selling_books.txt
,內容如下:
為你自己學 Git
金魚都能懂的 CSS 必學屬性
0 陷阱!0 誤解!8 天重新認識 JavaScript!
金魚都能懂的 CSS 選取器
為你自己學 Ruby on Rails
重新認識 Vue.js
打造高速網站從網站指標開始
為你自己學 Python
前端測試指南:策略與實踐
直接進 REPL 操作看看:
>>> books = open("best_selling_books.txt")
>>> books.read()
'為你自己學 Git\n金魚都能懂的 CSS 必學屬性\n...略...為你自己學 Python\n前端測試指南:策略與實踐\n'
# 再次讀取
>>> books.read()
''
如果想要再次讀取檔案的內容,可以使用 .seek()
方法來把磁頭或游標移動到檔案的開頭:
>>> books.seek(0)
>>> books.read()
'為你自己學 Git\n金魚都能懂的 CSS 必學屬性\n...略...為你自己學 Python\n前端測試指南:策略與實踐\n'
用完記得關!
在檔案的操作過程中,檔案用完之後要養成把開啟的檔案物件關掉的習慣,雖然就算不主動關掉,Python 本身的資源回收機制也會幫我們放掉,但不保證一定都會幫我們做這件事。Python 同時能夠開啟的檔案是有上限的,所以如果沒有把開啟的檔案關掉的話,不知道什麼時候會出現系統資源被吃光光的問題。
關閉檔案的方法很簡單,只要呼叫檔案物件的 .close()
方法就可以了:
f = open("demo.py")
# ...
# 這裡可以做一些你想做的事
# ...
# 做完之後就關掉
f.close()
但像上面這樣 的寫法,萬一在「做一些你想做的事」的過程中發生問題,.close()
方法就不會執行。為了確保檔案一定會被關掉,我們可以在前面章節學過的 try...except...finally...
:
f = open("demo.py")
try:
f.read()
# 這裡可以做一些你想做的事
except:
print("出事了!")
finally:
f.close()
不管前面的 try
區塊是順順的做完或是做到一半爆炸了,finally
區塊裡面的程式碼一定會被執行然後把檔案給關掉。
使用 with 關鍵字
要做到把檔案關閉這件事,在 Python 還有個更簡單的方法,就是使用 with
關鍵字:
with open("demo.py") as f:
print(f.read())
使用 with
關鍵字的時候,就算忘了呼叫 .close()
方法,Python 也會自動幫我們處理關檔的事情,不管在 with
區塊裡面有沒有發生例外,Python 都會自動幫我們在 with
區塊結束的時候關掉這個檔案物件,這樣就不用擔心忘記關掉檔案的問題了。但這個 with
怎麼這麼聰明知道什麼時候會幫我們把檔案關起來?其實這是 Python 的「情境管理員(Context Manager)」的做的好事。
當使用 with
關鍵字的時候,Python 幫我們建立並進入一個「情境(Context)」,在進到這個情境的時候 Python 會自動幫我們呼叫物件身上的魔術方法 __enter__()
,並且在離開情境的時候自動幫我們呼叫這顆物件身上的 __exit__()
方法。來做個簡單的實驗:
class Cat:
def __enter__(self):
print("我來了!")
def __exit__(self, exc_type, exc_val, exc_tb):
print("我走了!")
這裡我寫了一個很簡單的 Cat
類別,沒什麼了不起的功能,就只定義了 __enter__()
和 __exit__()
這兩個方法。其中 __exit__()
方法的參數看起來好像比較複雜,其實那些參數就是當在 with
區塊裡如果發生例外的時候,__exit__()
函數會收到例外的類型、內容以及更多錯誤的相關資訊(Traceback),如果執行過程沒什麼問題的話,這些參數都會是 None
。不過這裡可以先不管這些參數,接著我把它丟給 with
關鍵字:
kitty = Cat()
with kitty:
print("Meow~")
執行就會發現結果是:
我來了!
Meow~
我走了!
這就是 with
關鍵字運作的原理。回來看檔案物件,就這麼剛好,檔案物件身上有實作了 __enter__()
跟 __exit__()
這兩個方法,在 __enter__()
裡讀檔,而在 __exit__()
則是把檔案關掉,在 CPython 的原始碼裡有這樣一段定義:
static PyObject *
iobase_enter(PyObject *self, PyObject *args)
{
if (iobase_check_closed(self))
return NULL;
return Py_NewRef(self);
}
static PyObject *
iobase_exit(PyObject *self, PyObject *args)
{
return PyObject_CallMethodNoArgs(self, &_Py_ID(close));
}
這個 iobse_enter()
跟 iobase_exit()
就是 __enter__()
跟 __exit__()
。如果用 Python 來模擬的話,寫起來大概會像這樣:
class my_open_file:
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, mode=self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
然後再搭配 with
使用:
with my_open_file('heroes.csv', 'r') as f:
# 在這裡做該做的事..
print(f)
所以搭配 with
關鍵字進入以及離開情境的時候,Python 就會幫我們搞定這些開檔關檔的事了。
寫入檔案
寫入檔案跟讀取檔案的手法差不多,只是檔案模式要改成 w
或是 a
:
f = open("data.txt", "w")
跟讀取模式不同,在寫入模式下,如果檔案不存在會自動幫我們建一個。要注意的是,如果是使用 w
模式而且那個檔案本來就存在的話,就算什麼事都還沒做,光是用 w
模式開檔 Python 就會把檔案的內容清空。如果不想要清空檔案,可設定成 a
的附加模式,待會寫入的內容會附加到檔案的最後面。
寫入的方法也很直覺,直接使用檔案物件的 .write()
方法:
f.write("重要的內容!")
f.close()
.write()
方法的參數規定只能是字串,如果要寫入其他型態的資料,要先轉換成字串型態再寫入。差不多這樣就行了,最後別忘了在寫完之後把檔案關掉。
緩衝區
當我們在執行 .write()
方法的時候,Python 並沒有馬上幫我們把資料寫進檔案,這是因為效能考量的緣故。資料是先被放到緩衝區(Buffer),等到緩衝區滿了或是我們主動呼叫 .flush()
方法清空緩衝區,又或是檔案物件被關掉的時候,才會真的把內容寫入檔案並存到硬碟裡。所以如果在寫入的過程中程式突然中斷,就會像「還沒收功就罵髒話」一樣,資料可能就會這樣消失了。所以在寫入檔案的時候,也要記得在寫完之後把檔案關掉。
import os
f = open("important_data.txt", "w")
f.write("很重要的資料!")
os._exit(1) # 在這裡離開!
這裡我刻意使用 os._exit()
函數結束程式,官方手冊上有特別提到這個函數在離開的時候並不會處理緩衝區的資料,原本還在緩衝區的資料就來不及寫入檔案而消失了。所以在寫入檔案的時候,也要記得在寫完之後把檔案關掉,或是必要的時候,也可以主動呼叫 .flush()
方法把緩衝區的資料寫到檔案裡。