Python初学者笔记第二十期 -- (文件IO)
第29节课 文件IO
在编程中,文件 I/O(输入/输出)允许程序与外部文件进行数据交互。Python 提供了丰富且易用的文件 I/O 操作方法,能让开发者轻松实现文件的读取、写入和修改等操作。
IO交互方向
- 从硬盘文件 -> 读取数据 -> 内存(程序): 输入流 Input
- 内存(程序)-> 写入数据 -> 硬盘文件:输出流 Output
文件类型基础
任何操作系统中根据文件中内容的组成方式,通常将文件区分为两种类型:
- 字符类文件:内容数据底层通过字符组成;校验方式-使用记事本打开文件不会出现乱码!
- 任何编程语言编写的源代码、各种文本配置文件、记事本文档、CSV文件、JSON文件、MD格式文件等
- 字节类文件:内容数据底层通过字节/二进制组成;校验方式-使用记事本打开文件会出现乱码!
- 图片、音频、视频、可执行文件、压缩文件、ppt、word、excel等
说到底,在计算机中,所有的文件在硬盘上存储的时候,其实本质上都是字节文件
所谓的字符文件:字节文件 + 编码表 = 字符文件
- 字符输入流 字符输出流
- 字节输入流 字节输出流
1. 指定文件路径
在Python中操作文件前,首先需要正确指定文件路径。
(1)绝对路径
绝对路径是从文件系统的根目录开始的完整路径,能唯一确定文件的位置。在不同操作系统中,绝对路径的表示方式有所不同:
- Windows:使用反斜杠
\
作为路径分隔符。不过在 Python 字符串里,反斜杠是转义字符,所以要使用双反斜杠\\
或者在字符串前加r
来表示原始字符串,示例如下:
path1 = "C:\\Users\\HENG\\Desktop\\PyDay28"
path2 = "C:/Users/HENG/Desktop/PyDay28"
- Linux 和 macOS:使用正斜杠
/
作为路径分隔符。示例代码:
path3 = '/home/xixi/desktop/test.txt'
(2)相对路径
相对路径是相对于当前工作目录的路径。当前工作目录指的是程序运行时所在的目录。可以使用 os.getcwd()
函数获取当前工作目录。常见的相对路径表示方式有:
./
:表示当前目录。../
:表示上一级目录。
示例代码:
import os
# 查看当前文件所在的目录
print(os.getcwd())
# C:\Users\HENG\Desktop\PyDay28# 因为xixi.txt 与 Demo.py 处于同一个目录下的
f = open("xixi.txt", "r")
print(f.read())
# 因为datas目录 与 Demo.py 处于同一个目录下的
f = open("datas\\example.txt", "r")
print(f.read())# 因为xixi.txt 与 Demo.py 处于同一个目录下的
f = open("./xixi.txt", "r")
print(f.read())
# 因为datas目录 与 Demo.py 处于同一个目录下的
f = open("./datas\\example.txt", "r")
print(f.read())# Desktop\\YDJava\\README.md
f = open("../YDJava\\README.md", "r", encoding="UTF-8")
print(f.read())
(3)使用os.path模块处理路径
Python的os.path模块提供了许多实用函数,可以帮助我们以跨平台的方式处理文件路径:
import os.path
# 路径拼接
# haha\\xixi\\a.txt
full_path = os.path.join("D:\\","xixi","a.txt")
print(full_path)# 获取文件的绝对路径
abs_path = os.path.abspath("xixi.txt")
print(abs_path)# 获取文件所在的目录 一般传入绝对路径
dir_name = os.path.dirname(abs_path)
print(dir_name)
# 获取文件名
file_name = os.path.basename(abs_path)
print(file_name)# 获取文件名称 与 后缀名
name,ext = os.path.splitext(file_name)
print(name, ext)# 检查路径是否存在
print(os.path.exists(abs_path))# 是否是文件或者目录
print(os.path.isfile(abs_path))
print(os.path.isdir("./datas"))
2. 文件打开与关闭
文件操作的第一步是打开文件,最后一步是关闭文件。
(1)打开文件
在 Python 中,使用 open()
函数来打开文件,其基本语法如下:
file_object = open(file_path, mode, encoding=None, buffering=-1, errors=None)
主要参数说明:
file_path
:文件的路径,可以是绝对路径或相对路径。mode
:文件的打开模式,常见的模式有:'r'
:只读模式,文件必须存在(默认模式)。'w'
:写入模式,若文件不存在则创建,若存在则清空原有内容。'a'
:追加模式,若文件不存在则创建,若存在则在文件末尾追加内容。'x'
:独占创建模式,若文件已存在则失败。'rb'
:二进制只读模式。'wb'
:二进制写入模式。'ab'
:二进制追加模式。'r+'
:读写模式,文件必须存在。'w+'
:读写模式,若文件不存在则创建,若存在则清空原有内容。'a+'
:读写模式,若文件不存在则创建,若存在则在文件末尾追加内容。
encoding
:指定文件的编码方式(针对字符文件),常见的编码方式有:'utf-8'
:Unicode编码,支持多语言字符(推荐使用)。'gbk'
:中文编码,主要用于简体中文。'ascii'
:ASCII编码,仅支持英文字符。'latin-1'
:西欧语言编码。
在处理文本文件时,建议明确指定编码方式,避免出现编码错误。
buffering
:缓冲策略,-1表示使用默认缓冲策略,0表示无缓冲,1表示行缓冲,大于1表示缓冲区大小。【提高读写效率的】errors
:指定如何处理编码和解码错误,如’strict’(默认,抛出异常)、‘ignore’(忽略错误)、‘replace’(替换错误字符)等。
(2)关闭文件
文件使用完毕后,需要调用 close()
方法关闭文件,以释放系统资源。不关闭文件可能导致资源泄漏和数据丢失。示例代码如下:
file = open("./datas/example.txt", "w", encoding="utf-8")
content = file.read()
print(content)
file.close()
(3)使用with语句(上下文管理器)
为了避免忘记关闭文件或异常发生时文件未关闭的情况,推荐使用 with
语句(上下文管理器),它会在代码块执行完毕后自动关闭文件,即使发生异常也能确保文件被正确关闭:
with open("./datas/example.txt", 'r', encoding="utf-8") as file:content = file.read()print(content)
(4)同时操作多个文件
with
语句也支持同时打开多个文件:
with (open("./datas/example.txt", 'r', encoding="utf-8") as file1,open("./datas/copy.txt", "w", encoding="utf-8") as file2):content = file1.read()file2.write(content)print(content)
3. 文件读取
文件读取是文件操作中最常见的任务之一。Python提供了多种方法来读取文件内容,从整个文件一次性读取到逐行处理,满足不同的需求场景。
(1)读取整个文件内容
使用 read()
方法可以一次性读取整个文件的内容,适用于处理小型文件:
# 字符文件输入流
with open("datas/example.txt", "r", encoding="utf-8") as file:content = file.read()print(content)
# 字节文件输入流
with open("datas/example.txt", "rb") as file:content = file.read()print(content) # 打印的是字符串的字节形式
# 字节文件输入流
with open('datas/fig.png', 'rb') as file:content = file.read()print(content) # 打印的是字符串的字节形式
也可以通过指定参数来读取指定字节/字符数:
# 字符文件输入流
with open("datas/example.txt", "r", encoding="utf-8") as file:# 字符输入 10 -> 10个字符content = file.read(10) # 从开始读10个字符print(content)content = file.read(10) # 从刚才读取的位置继续读取10个字符print(content)content = file.read(10) # 从刚才读取的位置继续读取10个字符print(content)# 注意 换行也算字符!# 字节文件输入流
with open("datas/example.txt", "rb") as file:content = file.read(12)print(content)content = file.read(12)print(content)content = file.read(12)print(content)#windows中换行\r\n
(2)逐行读取文件内容
可以使用 readline()
方法逐行读取文件内容,适用于按行处理文件:
# 字符文件输入流
with open("datas/example.txt", "r", encoding="utf-8") as file:line = file.readline()while line:print(line)line = file.readline()
也可以使用 for
循环逐行读取文件内容,这种方式更简洁且内存效率更高,推荐使用:
# 字符文件输入流
with open("datas/example.txt", "r", encoding="utf-8") as file:for line in file:print(line)
(3)读取多行内容
使用 readlines()
方法可以将文件的每一行作为一个元素存储在列表中,适用于需要随机访问行的场景:
# 字符文件输入流
with open("datas/example.txt", "r", encoding="utf-8") as file:lines = file.readlines()print(len(lines))for i in range(len(lines)):print(lines[i])
(4)大文件处理技巧
处理大文件时,应避免一次性将整个文件读入内存,而应采用逐行或分块读取的方式:
# 返回一个文件当中的某一个分块
def read_in_chunks(file, chunk):with open(file, 'r', encoding='utf-8') as file:while True:block = file.read(chunk)if not block: # 文件结束breakyield block # 返回当前块for block in read_in_chunks("./datas/pride-and-prejudice.txt", 100):print("=" * 20)print(block)
4. 文件写入
文件写入是程序将数据持久化存储的重要方式。Python提供了多种方法来写入文件,包括覆盖写入、追加写入和按行写入等。
(1)写入文件
使用 write()
方法可以向文件中写入内容,示例代码如下:
# 覆盖写入
with open('./datas/copy.txt', 'w', encoding='utf-8') as file:file.write("Hello")
(2)追加写入文件
使用 a
模式打开文件,然后使用 write()
方法可以在文件末尾追加内容,示例代码如下:
# 追加写入
with open('./datas/copy.txt', 'a', encoding='utf-8') as file:file.write("\nHello")
(3)写入多行内容
使用 writelines()
方法可以一次性写入多行内容,但需要注意该方法不会自动添加换行符:
# 追加写入
with open('./datas/copy.txt', 'a', encoding='utf-8') as file:lines = ['Hello!\n', "World\n", "Nice\n"]file.writelines(lines)
(4)格式化写入
结合字符串格式化功能,可以更灵活地写入内容:
# 追加写入
with open('./datas/copy.txt', 'a', encoding='utf-8') as file:file.write('\n')for i in range(1,10):for j in range(1,i + 1):file.write(f'{i} × {j} = {i * j} \t')file.write("\n")
(5)文件写入的实际应用场景
简易日志记录器:
import datetime
def log_message(message):timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")log_item = f'[{timestamp}] {message}\r\n'with open("./datas/log.txt", 'a', encoding="utf-8") as file:file.write(log_item)log_message("原神启动!")
log_message("抽卡100次")
log_message("一个都没有!")
log_message("卸载 拜拜!")
5. 文件指针操作
文件指针是一个重要概念,它表示当前文件读写的位置。通过控制文件指针,我们可以实现随机访问文件的任意位置,而不必从头到尾顺序读取。
(1)seek() 方法
seek()
方法用于移动文件指针到文件中的指定位置,其语法如下:
file.seek(offset, whence=0)
参数说明:
offset
:偏移量,表示要移动的字节数。whence
:可选参数,指定偏移的起始位置,取值为:0
:从文件开头开始偏移(默认值)。1
:从当前位置开始偏移。(字节,无缓冲,可支持随机访问)2
:从文件末尾开始偏移。(字节,无缓冲,可支持随机访问)
示例代码如下:
with open('./datas/example.txt','r', encoding='utf-8') as file:file.seek(12)content = file.read(12)print(content)file.seek(30)content = file.read(5)print(content)
(2)tell() 方法
tell()
方法用于获取文件指针的当前位置(从文件开头算起的字节/字符数),示例代码如下:
with open('./datas/example.txt','r', encoding='utf-8') as file:print(file.tell())content = file.read(5)print(content)print(file.tell())
6. 二进制文件操作
除了文本文件,Python 还可以处理二进制文件,如图片、音频、视频等。在操作二进制文件时,需要使用二进制模式('rb'
、'wb'
、'ab'
等)。二进制模式下,Python不会对数据进行任何转换,而是按原始字节处理。
(1)读取二进制文件
读取二进制文件时,返回的是字节对象(bytes),而不是字符串:
with open('./datas/fig.png','rb') as file:data = file.read()print(type(data))print(len(data))print(data[:10])
(2)写入二进制文件
写入二进制文件时,必须提供字节对象,而不是字符串:
with open('./datas/out.png','wb') as file:with open('./datas/fig.png', 'rb') as f:file.write(f.read())
(3)分块处理大型二进制文件
处理大型二进制文件时,应避免一次性将整个文件读入内存:
def copy(source_file, target_file):chunk = 1024 * 1024 * 10 # 10MB为分块total = 0with open(source_file, 'rb') as s:with open(target_file, 'wb') as t:while True:block = s.read(chunk)if not block: # 文件结束breaktotal += len(block)t.write(block)print("写入了" , total)source_file = "F:\\系统镜像\\CentOS-7-x86_64-DVD-1810.iso"
target_file = "F:\\系统镜像\\CentOS-7-x86_64-DVD-1810-copy.iso"
copy(source_file, target_file)
7. 文件操作的异常处理
在进行文件操作时,可能会出现各种异常,如文件不存在、权限不足、磁盘空间不足等。为了保证程序的健壮性,需要对这些异常进行适当处理。
(1)常见的文件操作异常
FileNotFoundError
:尝试打开不存在的文件时抛出PermissionError
:没有足够权限访问文件时抛出IsADirectoryError
:尝试对目录执行文件操作时抛出FileExistsError
:尝试创建已存在的文件时抛出(使用’x’模式)UnicodeDecodeError
:文件编码与指定的编码不匹配时抛出IOError
:输入/输出操作失败时抛出(如磁盘已满)
(2)使用try-except处理异常
可以使用 try-except
语句来捕获和处理异常,示例代码如下:
try:with open("./datas/example.txt", 'r', encoding='utf-8') as file:# 如果出现异常 则导致程序中断content = file.read()print(1/0)print(content)
except FileNotFoundError:print("文件未找到!")
except PermissionError:print("文件权限异常!")
except UnicodeDecodeError:print("编码异常!")
# except ZeroDivisionError:
# print("除数不能为0")
except IOError as e:print(f"发生其他错误{e}")
except Exception as e:print(f"范围最大的异常问题{e}")print("之后执行的代码")
(3)使用finally确保资源释放
finally
子句可以确保无论是否发生异常,某些代码都会执行,通常用于资源清理:
最好把操作的文件对象设置为全局
file = None
try:file = open("./datas/example.txt", 'r', encoding='utf-8')# 如果出现异常 则导致程序中断content = file.read()print(1/0)print(content)
except FileNotFoundError:print("文件未找到!")
except PermissionError:print("文件权限异常!")
except UnicodeDecodeError:print("编码异常!")
# except ZeroDivisionError:
# print("除数不能为0")
except IOError as e:print(f"发生其他错误{e}")
except Exception as e:print(f"范围最大的异常问题{e}")
finally:# 关闭资源(文件 数据库 网络链接)# 判断文件对象的存在性 如果存在则关闭# 不存在 则不管 文件对象在创建时出现了异常print(type(file))if file is not None:file.close()print("之后执行的代码")
8. 案例解析
(1)统计文件中单词的数量
import redef count_words(file_path):file = None# 记录单词与其出现次数的字典# 键:单词# 值:次数words = dict()try:file = open(file_path, 'r', encoding='utf-8')# 读取每一行字符串for line in file:# 通过正则表达式 提取所有单词(连续的英文字母)words_in_line = re.findall(r'\b[a-zA-Z]+\b', line)# 将提取出来的所有单词 进行小写化words_in_line = [word.lower() for word in words_in_line]for word in words_in_line:# 判断当前单词是否存在于字典当中(键)if word in words:# 修改 次数+1words[word] += 1else:# 新建 次数从1开始words[word] = 1except Exception as e:print(e)finally:if file is not None:file.close()return words
if __name__ == "__main__":file_path = "./Demo.py"# 统计单词的个数 返回字典words = count_words(file_path)# 打印字典内容for key in words.keys():print(f'单词{key},出现{words[key]}次')
(2)复制文件
#做一个复制文件的函数 传入两个文件的路径 + try-expect-finally
(3)带进度的复制文件
# pip install tqdm
from tqdm import tqdm
import time
import os
# 模拟一个需要一定时间完成的任务
# total_items指的是总任务个数
# def long_running_task(total_items):
# for i in tqdm(range(total_items), desc="当前进度"):
# time.sleep(0.1)
# long_running_task(50)# source_path 源文件路径
# destination_path 目标文件路径
def copy_file(source_path, destination_path):# 分块大小chunk_size = 1024 * 1024 # => 1mb# 源文件大小file_size = os.path.getsize(source_path)# 当前拷贝大小copied_size = 0with open(source_path, 'rb') as source_file, open(destination_path,'wb') as destination_file:# total 进度最大值# unit 进度数据单位# unit_scale 单位缩放 500kb -> 2.5mbwith tqdm(total=file_size, unit="B", unit_scale=True, desc="当前进度") as progess_bar:while True:chunk = source_file.read(chunk_size)if not chunk:breakdestination_file.write(chunk)copied_size += len(chunk)progess_bar.update(len(chunk))print("复制完成")if __name__ == "__main__":s = "F:\\系统镜像\\CentOS-7-x86_64-DVD-1810.iso"d = "F:\\系统镜像\\CentOS-7-x86_64-DVD-1810-copy.iso"copy_file(s, d)
(4)文件备份工具
"""
工作空间目录A目录A目录B目录C文件A文件B文件C目录B目录C文件A文件B文件C
"""
# source_dir 源目录
# backup_dir 拷贝目录
# file_extensions 文件后缀名的过滤列表 ['.java', '.py', '.c']
import os
# 高级的文件操作的模块
import shutildef backup_files(source_dir, backup_dir, file_extensions):# 确保备份拷贝目录是存在的os.makedirs(backup_dir, exist_ok=True)# 统计信息 总共遍历了多少文件 拷贝了多少文件 跳过了多少文件stats = {'total': 0, 'back_up': 0, 'skipped': 0}for root, dirs, files in os.walk(source_dir):# 跳过拷贝目录本身(拷贝目录如果在源目录中)if os.path.abspath(root) == os.path.abspath(backup_dir):continue# 对当前root中的一级子文件进行遍历(文件名称)for file in files:stats['total'] += 1# 创建该文件全路径(以root为父目录)src_file = os.path.join(root, file)# 检查后缀 .mp3 .MP3name, ext = os.path.splitext(file)if ext.lower() not in file_extensions:# 跳过的文件数量stats['skipped'] += 1continue# 符合过滤器 就需要复制出来 创建绝对路径rel_path = os.path.relpath(root, source_dir)# 目标目录路径 保持原先的目录结构dst_dir = os.path.join(backup_dir, rel_path)os.makedirs(dst_dir, exist_ok=True)# 复制文件dst_file = os.path.join(dst_dir, file)shutil.copy2(src_file, dst_file)stats['back_up'] += 1print(f'已备份:{src_file} -> {dst_file}')print(f'备份完成!总共{stats['total']},备份{stats['back_up']},跳过{stats['skipped']}')if __name__ == '__main__':backup_files("E:\\工作空间", "./备份", ['.java', '.py', '.c'])
9. 序列化操作
序列化是将程序中的数据结构转换为可存储或传输的格式的过程,而反序列化则是将这种格式转换回原始数据结构。在Python中,序列化常用于数据持久化、网络传输和进程间通信等场景。
将代码中不属于字符、字节的数据,文件IO操作中称为抽象数据
Python中针对抽象数据提供了对应的模块,可以实现抽象数据和文件之间的IO操作
- 序列化:将完整数据,拆分标记后进行保存
- 反序列化:将拆分标记的数据进行组合
Python提供的内置模块:
- 将抽象数据序列化成字节文件:pickle(掌握)- 仅适用于Python
- 将抽象数据序列化成字符文件:json(掌握)- 跨语言通用
- 将抽象数据序列化成字节文件:marshal(了解)- 主要用于Python内部
- 将抽象数据进行数据字典保存:shelve(了解)- 提供类似字典的接口
(1)pickle
pickle模块是Python特有的序列化模块,可以将几乎任何Python对象序列化为字节流,适合用于Python程序间的数据交换和持久化存储。
将字典数据,序列化到文件中:
import pickle
user_dict = {"admin": {"username": "admin", "password": 123, "realname":"张三"},"manager": {"username": "manager", "password": 123, "realname":"李四"}
}
arr = [1,2,3,4,5,6,7,8,9,10]# 一般而言 建议将数据一次性封装好 再去进行序列化
with open("pickle.pickle", mode="wb") as file:pickle.dump(user_dict, file)pickle.dump(arr, file)
将文件中的数据,反序列化到代码中:
import pickle
with open("pickle.pickle", mode='rb') as file:while True:try:data = pickle.load(file)print(data, type(data))except EOFError:break
推荐方式如下:
import pickleuser_dict = {"admin": {"username": "admin", "password": 123, "realname": "张三"},"manager": {"username": "manager", "password": 123, "realname": "李四"}
}
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
tup = (1, 2, 3, 4, 5, 6)
set = {1, 2, 3, 4}
# 一次性将需要序列化的数据进行一个统一的封装
data = {"user_dict": user_dict,"arr": arr,"tup": tup,"set": set
}with open("pickle_upper.pickle", mode="wb") as file:pickle.dump(data, file)with open('pickle_upper.pickle', mode='rb') as file:data = pickle.load(file)print(data, type(data))print(data['user_dict'])print(data['arr'])print(data['tup'])print(data['set'])
(2)json
json模块是Python标准库中用于JSON数据编码和解码的模块,JSON是一种轻量级的数据交换格式,可以在不同编程语言之间传递数据(JSON支持的数据类型:字典、数组、数字、布尔类型、字符串(必须用""双引号))。
将代码中的抽象数据,序列化存储到字符文件中:
import json
user_dict = {"admin": {"username": "admin", "password": 123, "realname": "张三"},"manager": {"username": "manager", "password": 123, "realname": "李四"}
}arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
tup = (1, 2, 3, 4, 5, 6)data = {"user_dict": user_dict,"arr": arr,"tup": tup,
}
# serializable 可序列化
with open("json.txt", mode="w") as file:json.dump(data, file, indent=4)
将字符文件中的数据,反序列化到代码中:
import json
with open('json.txt', mode='r') as file:data = json.load(file)print(data, type(data))print(data['arr'])
(3)marshal
将代码中的抽象数据,序列化存储到字节文件中
import marshal
user_dict = {"admin": {"username": "admin", "password": 123, "realname": "张三"},"manager": {"username": "manager", "password": 123, "realname": "李四"}
}arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
tup = (1, 2, 3, 4, 5, 6)data = {"user_dict": user_dict,"arr": arr,"tup": tup,
}
with open('marshal.mar', mode='wb') as file:marshal.dump(data, file)
将字节文件中的数据,反-序列化到代码中
import marshalwith open('marshal.mar', mode='rb') as file:data = marshal.load( file)print(data, type(data))
(4)shelve
唯一一个文件IO操作中,对抽象数据进行存储的高级模块
- 如果需要将抽象数据保存到文件中,优先推荐使用该模块
- shelve模块的操作方式,是所有序列化模块中最简洁、最清晰
将抽象数据,序列化到文件中
import shelveuser_dict = {"admin": {"username": "admin", "password": 123, "realname": "张三"},"manager": {"username": "manager", "password": 123, "realname": "李四"}
}
user_set = {"admin": "张三", "manger": "李四"}
user_arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
.bak 数据备份
.dir 数据的索引
.dat 源数据的字节形式
"""
with shelve.open("my_shevel") as db:db['user_dict'] = user_dictdb['user_set'] = user_setdb['user_arr'] = user_arr
将文件中的数据,反序列化到代码中
import shelvewith shelve.open("my_shevel") as db:db['user_arr'] = [66,666,6666]print(db['user_dict'])print(db['user_set'])print(db['user_arr'])