一 引入
應用程序運行過程中產生的數據最先都是存放於內存中的,若想永久保存下來,必須要保存於硬盤中。應用程序若想操作硬件必須通過操作系統,而文件就是操作系統提供給應用程序來操作硬盤的虛擬概念,用戶或應用程序對文件的操作,就是向操作系統發起調用,然后由操作系統完成對硬盤的具體操作。

二 文件操作的基本流程
2.1 基本流程

有了文件的概念,我們無需再去考慮操作硬盤的細節,只需要關注操作文件的流程:
# 1. 打開文件,由應用程序向操作系統發起系統調用open(...),操作系統打開該文件,對應一塊硬盤空間,並返回一個文件對象賦值給一個變量f
f=open('a.txt','r',encoding='utf-8') #默認打開模式就為r
# 2. 調用文件對象下的讀/寫方法,會被操作系統轉換為讀/寫硬盤的操作
data=f.read()
# 3. 向操作系統發起關閉文件的請求,回收系統資源
f.close()

2.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)

2.3 指定操作文本文件的字符編碼
f = open(...)是由操作系統打開文件,如果打開的是文本文件,會涉及到字符編碼問題,如果沒有為open指定編碼,那么打開文本文件的默認編碼很明顯是操作系統說了算了,操作系統會用自己的默認編碼去打開文件,在windows下是gbk,在linux下是utf-8。
這就用到了上節課講的字符編碼的知識:若要保證不亂碼,文件以什么方式存的,就要以什么方式打開。
f = open('a.txt','r',encoding='utf-8')

三 文件的操作模式
3.1 控制文件讀寫操作的模式
r(默認的):只讀
w:只寫
a:只追加寫
3.1.1 案例一:r 模式的使用
# r只讀模式: 在文件不存在時則報錯,文件存在文件內指針直接跳到文件開頭
with open('a.txt',mode='r',encoding='utf-8') as f:
res=f.read() # 會將文件的內容由硬盤全部讀入內存,賦值給res
# 小練習:實現用戶認證功能
inp_name=input('請輸入你的名字: ').strip()
inp_pwd=input('請輸入你的密碼: ').strip()
with open(r'db.txt',mode='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('賬號名或者密碼錯誤')
3.1.2 案例二:w 模式的使用
# w只寫模式: 在文件不存在時會創建空文檔,文件存在會清空文件,文件指針跑到文件開頭
with open('b.txt',mode='w',encoding='utf-8') as f:
f.write('你好\n')
f.write('我好\n')
f.write('大家好\n')
f.write('111\n222\n333\n')
#強調:
# 1 在文件不關閉的情況下,連續的寫入,后寫的內容一定跟在前寫內容的后面
# 2 如果重新以w模式打開文件,則會清空文件內容
3.1.3 案例三:a 模式的使用
# a只追加寫模式: 在文件不存在時會創建空文檔,文件存在會將文件指針直接移動到文件末尾
with open('c.txt',mode='a',encoding='utf-8') as f:
f.write('44444\n')
f.write('55555\n')
#強調 w 模式與 a 模式的異同:
# 1 相同點:在打開的文件不關閉的情況下,連續的寫入,新寫的內容總會跟在前寫的內容之后
# 2 不同點:以 a 模式重新打開文件,不會清空原文件內容,會將文件指針直接移動到文件末尾,新寫的內容永遠寫在最后
# 小練習:實現注冊功能:
name=input('username>>>: ').strip()
pwd=input('password>>>: ').strip()
with open('db1.txt',mode='a',encoding='utf-8') as f:
info='%s:%s\n' %(name,pwd)
f.write(info)
3.1.4 案例四:+ 模式的使用(了解)
# r+ w+ a+ :可讀可寫
#在平時工作中,我們只單純使用r/w/a,要么只讀,要么只寫,一般不用可讀可寫的模式

3.2 控制文件讀寫內容的模式
大前提: tb模式均不能單獨使用,必須與r/w/a之一結合使用
t(默認的):文本模式
1. 讀寫文件都是以字符串為單位的
2. 只能針對文本文件
3. 必須指定encoding參數
b:二進制模式:
1.讀寫文件都是以bytes/二進制為單位的
2. 可以針對所有文件
3. 一定不能指定encoding參數
3.2.1 案例一:t 模式的使用
# t 模式:如果我們指定的文件打開模式為r/w/a,其實默認就是rt/wt/at
with open('a.txt',mode='rt',encoding='utf-8') as f:
res=f.read()
print(type(res)) # 輸出結果為:<class 'str'>
with open('a.txt',mode='wt',encoding='utf-8') as f:
s='abc'
f.write(s) # 寫入的也必須是字符串類型
#強調:t 模式只能用於操作文本文件,無論讀寫,都應該以字符串為單位,而存取硬盤本質都是二進制的形式,當指定 t 模式時,內部幫我們做了編碼與解碼
3.2.2 案例二: b 模式的使用
# b: 讀寫都是以二進制位單位
with open('1.mp4',mode='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模式
# 小練習: 編寫拷貝工具
src_file=input('源文件路徑: ').strip()
dst_file=input('目標文件路徑: ').strip()
with open(r'%s' %src_file,mode='rb') as read_f,open(r'%s' %dst_file,mode='wb') as write_f:
for line in read_f:
# print(line)
write_f.write(line)

四 操作文件的方法
4.1 重點
# 讀操作
f.read() # 讀取所有內容,執行完該操作后,文件指針會移動到文件末尾
f.readline() # 讀取一行內容,光標移動到第二行首部
f.readlines() # 讀取每一行內容,存放於列表中
# 強調:
# f.read()與f.readlines()都是將內容一次性讀入內容,如果內容過大會導致內存溢出,若還想將內容全讀入內存,則必須分多次讀入,有兩種實現方式:
# 方式一
with open('a.txt',mode='rt',encoding='utf-8') as f:
for line in f:
print(line) # 同一時刻只讀入一行內容到內存中
# 方式二
with open('1.mp4',mode='rb') as f:
while True:
data=f.read(1024) # 同一時刻只讀入1024個Bytes到內存中
if len(data) == 0:
break
print(data)
# 寫操作
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模式

4.2 了解
f.readable() # 文件是否可讀
f.writable() # 文件是否可讀
f.closed # 文件是否關閉
f.encoding # 如果文件打開模式為b,則沒有該屬性
f.flush() # 立刻將文件內容從內存刷到硬盤
f.name

五 主動控制文件內指針移動
#大前提:文件內指針的移動都是Bytes為單位的,唯一例外的是t模式下的read(n),n以字符為單位
with open('a.txt',mode='rt',encoding='utf-8') as f:
data=f.read(3) # 讀取3個字符
with open('a.txt',mode='rb') as f:
data=f.read(3) # 讀取3個Bytes
# 之前文件內指針的移動都是由讀/寫操作而被動觸發的,若想讀取文件某一特定位置的數據,則則需要用f.seek方法主動控制文件內指針的移動,詳細用法如下:
# f.seek(指針移動的字節數,模式控制):
# 模式控制:
# 0: 默認的模式,該模式代表指針移動的字節數是以文件開頭為參照的
# 1: 該模式代表指針移動的字節數是以當前所在的位置為參照的
# 2: 該模式代表指針移動的字節數是以文件末尾的位置為參照的
# 強調:其中0模式可以在t或者b模式使用,而1跟2模式只能在b模式下用

5.1 案例一: 0模式詳解
# a.txt用utf-8編碼,內容如下(abc各占1個字節,中文“你好”各占3個字節)
abc你好
# 0模式的使用
with open('a.txt',mode='rt',encoding='utf-8') as f:
f.seek(3,0) # 參照文件開頭移動了3個字節
print(f.tell()) # 查看當前文件指針距離文件開頭的位置,輸出結果為3
print(f.read()) # 從第3個字節的位置讀到文件末尾,輸出結果為:你好
# 注意:由於在t模式下,會將讀取的內容自動解碼,所以必須保證讀取的內容是一個完整中文數據,否則解碼失敗
with open('a.txt',mode='rb') as f:
f.seek(6,0)
print(f.read().decode('utf-8')) #輸出結果為: 好

5.2 案例二: 1模式詳解
# 1模式的使用
with open('a.txt',mode='rb') as f:
f.seek(3,1) # 從當前位置往后移動3個字節,而此時的當前位置就是文件開頭
print(f.tell()) # 輸出結果為:3
f.seek(4,1) # 從當前位置往后移動4個字節,而此時的當前位置為3
print(f.tell()) # 輸出結果為:7

5.3 案例三: 2模式詳解
# a.txt用utf-8編碼,內容如下(abc各占1個字節,中文“你好”各占3個字節)
abc你好
# 2模式的使用
with open('a.txt',mode='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',mode='rb') as f:
f.seek(0,2)
while True:
line=f.readline()
if len(line) == 0:
# 沒有內容
time.sleep(0.5)
else:
print(line.decode('utf-8'),end='')

六 文件的修改
# 文件a.txt內容如下
張一蛋 山東 179 49 12344234523
李二蛋 河北 163 57 13913453521
王全蛋 山西 153 62 18651433422
# 執行操作
with open('a.txt',mode='r+t',encoding='utf-8') as f:
f.seek(9)
f.write('<婦女主任>')
# 文件修改后的內容如下
張一蛋<婦女主任> 179 49 12344234523
李二蛋 河北 163 57 13913453521
王全蛋 山西 153 62 18651433422
# 強調:
# 1、硬盤空間是無法修改的,硬盤中數據的更新都是用新內容覆蓋舊內容
# 2、內存中的數據是可以修改的

文件對應的是硬盤空間,硬盤不能修改對應着文件本質也不能修改,
那我們看到文件的內容可以修改,是如何實現的呢?
大致的思路是將硬盤中文件內容讀入內存,然后在內存中修改完畢后再覆蓋回硬盤
具體的實現方式分為兩種:
6.1 文件修改方式一
# 實現思路:將文件內容發一次性全部讀入內存,然后在內存中修改完畢后再覆蓋寫回原文件
# 優點: 在文件修改過程中同一份數據只有一份
# 缺點: 會過多地占用內存
with open('db.txt',mode='rt',encoding='utf-8') as f:
data=f.read()
with open('db.txt',mode='wt',encoding='utf-8') as f:
f.write(data.replace('kevin','SB'))
6.1 文件修改方式二
# 實現思路:以讀的方式打開原文件,以寫的方式打開一個臨時文件,一行行讀取原文件內容,修改完后寫入臨時文件...,刪掉原文件,將臨時文件重命名原文件名
# 優點: 不會占用過多的內存
# 缺點: 在文件修改過程中同一份數據存了兩份
import os
with open('db.txt',mode='rt',encoding='utf-8') as read_f,\
open('.db.txt.swap',mode='wt',encoding='utf-8') as wrife_f:
for line in read_f:
wrife_f.write(line.replace('SB','kevin'))
os.remove('db.txt')
os.rename('.db.txt.swap','db.txt')
