1. 程式人生 > 其它 >檔案操作與處理

檔案操作與處理

應用程式執行過程中產生的資料最先都是存放於記憶體中的,若想永久儲存下來,必須要保存於硬碟中。
應用程式若想操作硬體必須通過作業系統,而檔案就是作業系統提供給應用程式來操作硬碟的虛擬概念;使用者或應用程式對檔案的操作,就是向作業系統發起呼叫,然後由作業系統完成對硬碟的具體操作。

1.檔案操作的基本流程

1.1 基本流程

有了檔案的概念,我們無需再去考慮操作硬碟的細節,只需要關注操作檔案的流程:

# 1、 開啟檔案,由應用程式向作業系統發起系統呼叫open(...),作業系統開啟該檔案,對應一塊硬碟空間,並返回一個檔案物件賦值給一個變數f
f = open('a.txt', 'r', encoding='utf-8')  # 預設開啟模式就為r
 
# 2、 呼叫檔案物件下的讀/寫方法,會被作業系統轉換為讀/寫硬碟的操作
data = f.read()
 
# 3、 向作業系統發起關閉檔案的請求,回收系統資源
f.close()

1.2 資源回收與with上下文管理

開啟一個檔案包含兩部分資源:應用程式的變數f和作業系統開啟的檔案。在操作完畢一個檔案時,必須把與該檔案的這兩部分資源全部回收,回收方法為:

1、f.close()  # 回收作業系統開啟的檔案資源
2、del f  # 回收應用程式級的變數

其中del f一定要發生在f.close()之後,否則就會導致作業系統開啟的檔案無法關閉,白白佔用資源;
而Python自動的垃圾回收機制決定了我們無需考慮del f,這就要求我們,在操作完畢檔案後,一定要記住f.close(),雖然如此強調,但是大多數讀者還是會不由自主地忘記f.close(),考慮到這一點,Python提供了with關鍵字來幫我們管理上下文:

# 1、在執行完子程式碼塊後,with 會自動執行f.close()
with open('a.txt', 'w') as f:
    pass 
 
# 2、可以用with同時開啟多個檔案,用逗號分隔開即可
with open('a.txt', 'r') as read_f, open('b.txt', 'w') as write_f:  
    data = read_f.read()
    write_f.write(data)

1.3 指定操作文字檔案的字元編碼

f = open(...) 是由作業系統開啟檔案,如果開啟的是文字檔案,會涉及到字元編碼問題,如果沒有為open()指定編碼,那麼開啟文字檔案的預設編碼很明顯是作業系統說了算,作業系統會用自己的預設編碼去開啟檔案,在Windows下是GBK,在Linux下是utf-8。
這就要用到之前介紹的字元編碼的知識:若要保證不亂碼,檔案以什麼方式存的,就要以什麼方式開啟。

f = open('a.txt', 'r', encoding='utf-8')

2.檔案的操作模式

2.1 控制檔案讀寫操作的模式

  • r(預設的):只讀模式
  • w:重寫模式
  • a:只追加寫模式
2.1.1 案例一:r 模式的使用
# r只讀模式: 在檔案不存在時則報錯,檔案存在時檔案內指標直接跳到檔案開頭
 with open('a.txt', 'r', encoding='utf-8') as f:
     res = f.read()  # 會將檔案的內容由硬碟全部讀入記憶體,賦值給res
 
# 實現使用者認證功能
 inp_name = input('請輸入你的名字: ').strip()
 inp_pwd = input('請輸入你的密碼: ').strip()
 with open(r'db.txt', 'r', encoding='utf-8') as f:
     for line in f:
         # 把使用者輸入的名字與密碼與讀出內容做比對
         u, p = line.strip('\n').split(':')
         if inp_name == u and inp_pwd == p:
             print('登入成功!')
             break
     else:
         print('賬號名或者密碼錯誤')
2.1.2 案例二:w 模式的使用

w重寫模式: 檔案不存在時會自動建立空文件,檔案存在會清空檔案,檔案指標跑到檔案開頭

with open('b.txt', 'w', encoding='utf-8') as f:
    f.write('你好\n')
    f.write('我好\n') 
    f.write('大家好\n')
    f.write('111\n222\n333\n')

強調:

  • 在檔案不關閉的情況下,連續的寫入,後寫的內容一定跟在前寫內容的後面
  • 如果重新以w模式開啟檔案,則會清空檔案內容
2.1.3 案例三:a 模式的使用

a只追加寫模式: 在檔案不存在時會自動建立空文件,檔案存在會將檔案指標直接移動到檔案末尾

with open('c.txt', 'a', encoding='utf-8') as f:
     f.write('44444\n')
     f.write('55555\n')

w 模式與 a 模式的異同:

  • 相同點:在開啟的檔案不關閉的情況下,連續的寫入,新寫的內容總會跟在前寫的內容之後
  • 不同點:以 a 模式重新開啟檔案,不會清空原檔案內容,會將檔案指標直接移動到檔案末尾,新寫的內容永遠寫在最後
# 實現註冊功能:
 name = input('username>>>: ').strip()
 pwd = input('password>>>: ').strip()
 with open('db1.txt', 'a', encoding='utf-8') as f:
     info = '%s:%s\n' %(name, pwd)
     f.write(info)
2.1.4 案例四:+ 模式的使用(瞭解)
  • r+ w+ a+ :可讀可寫
  • 在平時工作中,我們只單純使用r/w/a,要麼只讀,要麼只寫,一般不用可讀可寫的模式

2.2 控制檔案讀寫內容的模式

大前提: tb模式均不能單獨使用,必須與r/w/a之一結合使用

t(預設的模式):文字模式 : r w a >>> rt wt at

  1. 讀寫檔案都是以字串為單位的
  2. 只能針對文字檔案進行操作
  3. 必須指定encoding引數

b:二進位制模式: rb wb ab

  1. 該模式可以操作任意型別的檔案
  2. 讀寫檔案都是以bytes(二進位制)為基本單位的
  3. 一定不能指定encoding引數
2.2.1 案例一:t 模式的使用

t 模式:如果我們指定的檔案開啟模式為r/w/a,其實預設就是rt/wt/at

with open('a.txt', 'rt', encoding='utf-8') as f:
     res = f.read() 
     print(type(res))  # 輸出結果為:<class 'str'>
        
with open('a.txt', 'wt', encoding='utf-8') as f:
     s = 'abc'
     f.write(s)  # 寫入的也必須是字串型別  

強調:t 模式只能用於操作文字檔案,無論讀寫,都應該以字串為單位,而存取硬碟本質都是二進位制的形式,當指定 t 模式時,內部幫我們做了編碼與解碼。

2.2.2 案例二:b 模式的使用

b: 讀寫都是以二進位制為單位

with open('1.mp4', 'rb') as f:
    data = f.read()
    print(type(data))  # 輸出結果為:<class 'bytes'>

with open('a.txt',mode='wb') as f:
	 msg = "你好"
    res = msg.encode('utf-8')  # res為bytes型別
    f.write(res)  # 在b模式下寫入檔案的只能是bytes型別

強調:b模式對比t模式

  1. 在操作純文字檔案方面t模式幫我們省去了編碼與解碼的環節,b模式則需要手動編碼與解碼,所以此時t模式更為方便。
  2. 針對非文字檔案(如圖片、視訊、音訊等)只能使用b模式。

小練習:編寫拷貝工具

file_adr = input("請輸入原始檔路徑:")
copy_file_adr = input("請輸入檔案拷貝路徑:")

with open(file_adr, 'rb') as f1, open(copy_file_adr, 'wb') as f2:
    for line in f1:
        f2.write(line)

3.操作檔案的方法

3.1 重點

讀操作

f.read()  # 讀取所有內容,執行完該操作後,檔案指標會移動到檔案末尾
f.readline()  # 讀取一行內容,游標移動到第二行首部
f.readlines()  # 讀取每一行內容,存放於列表中

強調:f.read()與f.readlines()都是將內容一次性讀入內容,如果內容過大會導致記憶體溢位,若還想將內容全讀入記憶體,則必須分多次讀入,有兩種實現方式:

方式一:

with open('a.txt', 'rt', encoding='utf-8') as f:
    for line in f:
        print(line)  # 同一時刻只讀入一行內容到記憶體中

方式二:

with open('1.mp4', 'rb') as f:
    while True:
        data = f.read(1024)  # 同一時刻只讀入1024個Bytes到記憶體中
        if len(data) == 0:
            break

寫操作

f.write('1111\n222\n')  # 針對文字模式的寫,需要自己寫換行符
f.write('1111\n222\n'.encode('utf-8'))  # 針對b模式的寫,需要自己寫換行符
f.writelines(['333\n', '444\n'])  # 檔案模式
f.writelines([bytes('333\n', encoding='utf-8'), '444\n'.encode('utf-8')])  # b模式

3.2 瞭解

f.readable()  # 檔案是否可讀
f.writable()  # 檔案是否可寫
f.closed  # 檔案是否關閉
f.encoding  # 如果檔案開啟模式為b,則沒有該屬性
f.flush()  # 立刻將檔案內容從記憶體刷到硬碟
f.name  # 檔案物件指代的檔案路徑

4.主動控制檔案內指標移動

大前提:檔案內指標的移動都是Bytes為單位的,唯一例外的是t模式下的read(n):n以字元為單位。

with open('a.txt', 'rt', encoding='utf-8') as f:
     data = f.read(3)  # 讀取3個字元
    
with open('a.txt', 'rb') as f:
     data = f.read(3) # 讀取3個Bytes

之前檔案內指標的移動都是由讀/寫操作而被動觸發的,若想讀取檔案某一特定位置的資料,則則需要用f.seek()方法主動控制檔案內指標的移動,詳細用法如下:

f.seek(指標移動的位元組數, 模式控制):

模式控制:

  • 0: 預設的模式,該模式代表指標移動的位元組數是以檔案開頭為參照的
  • 1: 該模式代表指標移動的位元組數是以當前所在的位置為參照的
  • 2: 該模式代表指標移動的位元組數是以檔案末尾的位置為參照的

強調:其中0模式可以在t或者b模式使用,而1跟2模式只能在b模式下用

4.1 案例一:0模式詳解

a.txt用utf-8編碼,內容如下:(abc各佔1個位元組,中文“你好”各佔3個位元組)

​ abc你好

0模式的使用:

with open('a.txt', 'rt', encoding='utf-8') as f:
    f.seek(3,0)     # 參照檔案開頭移動了3個位元組
    print(f.tell())  # 檢視當前檔案指標距離檔案開頭的位置,輸出結果為3
    print(f.read())  # 從第3個位元組的位置讀到檔案末尾,輸出結果為:你好

注意:由於在t模式下,會將讀取的內容自動解碼,所以必須保證讀取的內容是一個完整中文資料,否則解碼失敗

with open('a.txt', 'rb') as f:
    f.seek(6,0)
    print(f.read().decode('utf-8'))  # 輸出結果為: 好

4.2 案例二:1模式詳解

1模式的使用:

with open('a.txt', 'rb') as f:
    f.seek(3,1)  # 從當前位置往後移動3個位元組,而此時的當前位置就是檔案開頭
    print(f.tell())  # 輸出結果為:3
    f.seek(4,1)  # 從當前位置往後移動4個位元組,而此時的當前位置為3
    print(f.tell())  # 輸出結果為:7

4.3 案例三:2模式詳解

a.txt用utf-8編碼,內容如下:(abc各佔1個位元組,中文“你好”各佔3個位元組)

​ abc你好

2模式的使用:

with open('a.txt', 'rb') as f:
    f.seek(0,2)  # 參照檔案末尾移動0個位元組, 即直接跳到檔案末尾
    print(f.tell())  # 輸出結果為:9
    f.seek(-3,2)  # 參照檔案末尾往前移動了3個位元組
    print(f.read().decode('utf-8'))  # 輸出結果為:好

小練習:實現動態檢視最新一條日誌的效果

import time
with open('access.log', 'rb') as f:
    f.seek(0, 2)
    while True:
        line = f.readline()
        if len(line) == 0:  # 沒有內容
            time.sleep(1)
        else:
            print(line.decode('utf-8'), end='')

5.檔案的修改

檔案a.txt內容如下:

張一蛋     山東    179    49    12344234523
李二蛋     河北    163    57    13913453521
王全蛋     山西    153    62    18651433422

執行操作:

with open('a.txt', 'r+t', encoding='utf-8') as f:
    f.seek(9)
    f.write('<婦女主任>')

檔案修改後的內容如下:

張一蛋<婦女主任> 179    49    12344234523
李二蛋     河北    163    57    13913453521
王全蛋     山西    153    62    18651433422

強調:

  1. 硬碟空間是無法修改的,硬碟中資料的更新都是用新內容覆蓋舊內容。
  2. 記憶體中的資料是可以修改的。

檔案對應的是硬碟空間,硬碟不能修改對應著檔案本質也不能修改,那我們看到檔案的內容可以修改,是如何實現的呢?
大致的思路是將硬碟中檔案內容讀入記憶體,然後在記憶體中修改完畢後再覆蓋回硬碟。具體的實現方式分為兩種:

5.1 檔案修改方式一

實現思路:將檔案內容一次性全部讀入記憶體,然後在記憶體中修改完畢後再覆蓋寫回原檔案

  • 優點:在檔案修改過程中同一份資料只有一份
  • 缺點:會過多地佔用記憶體
with open('db.txt', 'rt', encoding='utf-8') as f:
    data = f.read()

with open('db.txt', 'wt', encoding='utf-8') as f:
    f.write(data.replace('kevin', 'NB'))

5.2 檔案修改方式二

實現思路:以讀的方式開啟原檔案,以寫的方式開啟一個臨時檔案,一行行讀取原檔案內容,修改完後寫入臨時檔案...,刪掉原檔案,將臨時檔案重新命名原檔名

  • 優點:不會佔用過多的記憶體
  • 缺點:在檔案修改過程中同一份資料存了兩份
import os

with open('db.txt', 'rt', encoding='utf-8') as read_f,\
        open('.db.txt.swap', 'wt', encoding='utf-8') as wrife_f:
    for line in read_f:
        wrife_f.write(line.replace('NB', 'kevin'))

os.remove('db.txt')
os.rename('.db.txt.swap', 'db.txt')