跳至主要内容

檔案處理

檔案處理

在基礎篇我們介紹了 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() 函數的時候,檔案模式設定成 aw 模式的話,檔案不存在的時候不會出錯,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 的原始碼裡有這樣一段定義:

檔案:Modules/_io/iobase.c
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() 方法把緩衝區的資料寫到檔案裡。

工商服務

想學 Python 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!