1. 程式人生 > 實用技巧 >11 檔案和異常

11 檔案和異常

實際開發中常常會遇到對資料進行持久化操作的場景,而實現資料持久化最直接簡單的方式就是將資料儲存到檔案中。說到“檔案”這個詞,可能需要先科普一下關於檔案系統的知識,但是這裡我們並不浪費筆墨介紹這個概念,請大家自行通過維基百科進行了解。

在Python中實現檔案的讀寫操作其實非常簡單,通過Python內建的open函式,我們可以指定檔名、操作模式、編碼資訊等來獲得操作檔案的物件,接下來就可以對檔案進行讀寫操作了。這裡所說的操作模式是指要開啟什麼樣的檔案(字元檔案還是二進位制檔案)以及做什麼樣的操作(讀、寫還是追加),具體的如下表所示。

操作模式具體含義
'r' 讀取 (預設)
'w' 寫入(會先截斷之前的內容)
'x' 寫入,如果檔案已經存在會產生異常
'a' 追加,將內容寫入到已有檔案的末尾
'b' 二進位制模式
't' 文字模式(預設)
'+' 更新(既可以讀又可以寫)

讀寫文字檔案

讀取文字檔案時,需要在使用open函式時指定好帶路徑的檔名(可以使用相對路徑或絕對路徑)並將檔案模式設定為'r'(如果不指定,預設值也是'r'),然後通過encoding引數指定編碼(如果不指定,預設值是None,那麼在讀取檔案時使用的是作業系統預設的編碼),如果不能保證儲存檔案時使用的編碼方式與encoding引數指定的編碼方式是一致的,那麼就可能因無法解碼字元而導致讀取失敗。下面的例子演示瞭如何讀取一個純文字檔案。

def main():
    f = open('致橡樹.txt', 'r', encoding='utf-8')
    print(f.read())
    f.close()


if __name__ == '__main__':
    main()

請注意上面的程式碼,如果open函式指定的檔案並不存在或者無法開啟,那麼將引發異常狀況導致程式崩潰。為了讓程式碼有一定的健壯性和容錯性,我們可以使用Python的異常機制對可能在執行時發生狀況的程式碼進行適當的處理,如下所示。

def main():
    f = None
    try:
        f = open("
C:\\Users\\Lenovo\\Desktop\\致橡樹.txt", "r", encoding="utf-8") print(f.read()) except FileNotFoundError: print("無法開啟指定的檔案!") except LookupError: print("指定了未知的編碼") except UnicodeDecodeError: print("讀取檔案時解碼錯誤!") finally: if f: f.close()

if __name__ == '__main__':
main()

在Python中,我們可以將那些在執行時可能會出現狀況的程式碼放在try程式碼塊中,在try程式碼塊的後面可以跟上一個或多個except來捕獲可能出現的異常狀況。例如在上面讀取檔案的過程中,檔案找不到會引發FileNotFoundError,指定了未知的編碼會引發LookupError,而如果讀取檔案時無法按指定方式解碼會引發UnicodeDecodeError,我們在try後面跟上了三個except分別處理這三種不同的異常狀況。最後我們使用finally程式碼塊來關閉開啟的檔案,釋放掉程式中獲取的外部資源,由於finally塊的程式碼不論程式正常還是異常都會執行到(甚至是呼叫了sys模組的exit函式退出Python環境,finally塊都會被執行,因為exit函式實質上是引發了SystemExit異常),因此我們通常把finally塊稱為“總是執行程式碼塊”,它最適合用來做釋放外部資源的操作。如果不願意在finally程式碼塊中關閉檔案物件釋放資源,也可以使用上下文語法,通過with關鍵字指定檔案物件的上下文環境並在離開上下文環境時自動釋放檔案資源,程式碼如下所示。

def main():
    try:
        with open("致橡樹.txt", "r", encoding="utf-8") as f:
            print(f.read())
    except FileNotFoundError:
        print("無法開啟指定的檔案!")
    except LookupError:
        print("指定了未知的編碼")
    except UnicodeDecodeError:
        print("讀取檔案時解碼錯誤!")


if __name__ == '__main__':
    main()

除了使用檔案物件的read方法讀取檔案之外,還可以使用for-in迴圈逐行讀取或者用readlines方法將檔案按行讀取到一個列表容器中,程式碼如下所示。

import time


def main():
    # 一次性讀取整個檔案內容
    with open("致橡樹.txt", "r", encoding="utf-8") as f:
        print(f.readlines())
    # 通過for-in迴圈朱行讀取
    with open("致橡樹.txt", mode="r", encoding="utf-8") as f:
        for line in f:
            print(line, end="")
            time.sleep(0.5)
    print()

    # 讀取檔案按行讀取到列表中
    with open("致橡樹.txt", encoding="utf-8") as f:
        lines = f.readlines()
    print(lines)


if __name__ == '__main__':
    main()

要將文字資訊寫入檔案也非常簡單,在使用open函式時指定好檔名並將檔案模式設定為'w'即可。注意如果需要對檔案內容進行追加式寫入,應該將模式設定為'a'。如果要寫入的檔案不存在會自動建立檔案而不是引發異常。下面的例子演示瞭如何將1-9999之間的素數分別寫入三個檔案中(1-99之間的素數儲存在a.txt中,100-999之間的素數儲存在b.txt中,1000-9999之間的素數儲存在c.txt中)。

from math import sqrt


def is_prime(n):
    """判斷素數的函式"""
    assert n > 0
    for factor in range(2, int(sqrt(n)) + 1):
        if n % factor == 0:
            return False
    return True if n != 1 else False


def main():
    filenames = ("a.txt", "b.txt", "c.txt")
    fs_list = []
    try:
        for filename in filenames:
            fs_list.append(open(filename, "w", encoding="utf-8"))
        for number in range(1, 10000):
            if is_prime(number):
                if number < 100:
                    fs_list[0].write(str(number) + "\n")
                elif number < 1000:
                    fs_list[1].write(str(number) + "\n")
                else:
                    fs_list[2].write(str(number) + "\n")
    except IOError as ex:
        print(ex)
        print("寫檔案時發生錯誤!")
    finally:
        for fs in fs_list:
            fs.close()
    print("操作完成!")


if __name__ == '__main__':
    main()

讀寫二進位制檔案

知道了如何讀寫文字檔案要讀寫二進位制檔案也就很簡單了,下面的程式碼實現了複製圖片檔案的功能。

def main():
    try:
        with open("xxx.png", "rb") as fs1:
            data = fs1.read()
            print(type(data))

        with open("aaa.png", "wb") as fs2:
            fs2.write(data)

    except FileNotFoundError as e:
        print("指定的檔案無法開啟")

    except IOError as e:
        print("讀寫檔案時出現錯誤")
    print("程式執行結束")


if __name__ == '__main__':
    main()

讀寫JSON檔案

通過上面的講解,我們已經知道如何將文字資料和二進位制資料儲存到檔案中,那麼這裡還有一個問題,如果希望把一個列表或者一個字典中的資料儲存到檔案中又該怎麼做呢?答案是將資料以JSON格式進行儲存。JSON是“JavaScript Object Notation”的縮寫,它本來是JavaScript語言中建立物件的一種字面量語法,現在已經被廣泛的應用於跨平臺跨語言的資料交換,原因很簡單,因為JSON也是純文字,任何系統任何程式語言處理純文字都是沒有問題的。目前JSON基本上已經取代了XML作為異構系統間交換資料的事實標準。關於JSON的知識,更多的可以參考JSON的官方網站,從這個網站也可以瞭解到每種語言處理JSON資料格式可以使用的工具或三方庫,下面是一個JSON的簡單例子。

import json


def main():
    mydict = {
        "name": "駱昊",
        'age': 38,
        'qq': 957658,
        'friends': ['王大錘', '白元芳'],
        'cars': [
            {'brand': 'BYD', 'max_speed': 180},
            {'brand': 'Audi', 'max_speed': 280},
            {'brand': 'Benz', 'max_speed': 320}
        ]
    }
    try:
        with open("data.json", "w", encoding="utf-8") as fs:
            json.dump(mydict, fs)
    except IOError as e:
        print(e)
    print("儲存資料完成!")


if __name__ == '__main__':
    main()

json模組主要有四個比較重要的函式,分別是:

  • dump - 將Python物件按照JSON格式序列化到檔案中
  • dumps - 將Python物件處理成JSON格式的字串
  • load - 將檔案中的JSON資料反序列化成物件
  • loads - 將字串的內容反序列化成Python物件

這裡出現了兩個概念,一個叫序列化,一個叫反序列化。自由的百科全書維基百科上對這兩個概念是這樣解釋的:“序列化(serialization)在電腦科學的資料處理中,是指將資料結構或物件狀態轉換為可以儲存或傳輸的形式,這樣在需要的時候能夠恢復到原先的狀態,而且通過序列化的資料重新獲取位元組時,可以利用這些位元組來產生原始物件的副本(拷貝)。與這個過程相反的動作,即從一系列位元組中提取資料結構的操作,就是反序列化(deserialization)”。

目前絕大多數網路資料服務(或稱之為網路API)都是基於HTTP協議提供JSON格式的資料,關於HTTP協議的相關知識,可以看看阮一峰老師的《HTTP協議入門》,如果想了解國內的網路資料服務,可以看看聚合資料阿凡達資料等網站,國外的可以看看{API}Search網站。下面的例子演示瞭如何使用requests模組(封裝得足夠好的第三方網路訪問模組)訪問網路API獲取國內新聞,如何通過json模組解析JSON資料並顯示新聞標題,這個例子使用了天行資料提供的國內新聞資料介面,其中的APIKey需要自己到該網站申請。

import requests
import json


def main():
    resp = requests.get("http://api.tianapi.com/guonei/?key=APIKey&num=10")
    data_model = json.loads(resp.text)
    for news in data_model["newslist"]:
        print(news["title"])


if __name__ == '__main__':
    main()

在Python中要實現序列化和反序列化除了使用json模組之外,還可以使用pickle和shelve模組,但是這兩個模組是使用特有的序列化協議來序列化資料,因此序列化後的資料只能被Python識別。關於這兩個模組的相關知識可以自己看看網路上的資料。另外,如果要了解更多的關於Python異常機制的知識,可以看看segmentfault上面的文章《總結:Python中的異常處理》,這篇文章不僅介紹了Python中異常機制的使用,還總結了一系列的最佳實踐,很值得一讀。