文件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: # 文件结束
break
yield 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 = 0
with open(source_file, 'rb') as s:
with open(target_file, 'wb') as t:
while True:
block = s.read(chunk)
if not block: # 文件结束
break
total += 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 re
def 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:
# 修改 次数+1
words[word] += 1
else:
# 新建 次数从1开始
words[word] = 1
except 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 = 0
with open(source_path, 'rb') as source_file, open(destination_path,'wb') as destination_file:
# total 进度最大值
# unit 进度数据单位
# unit_scale 单位缩放 500kb -> 2.5mb
with tqdm(total=file_size, unit="B", unit_scale=True, desc="当前进度") as progess_bar:
while True:
chunk = source_file.read(chunk_size)
if not chunk:
break
destination_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 shutil
def 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 .MP3
name, ext = os.path.splitext(file)
if ext.lower() not in file_extensions:
# 跳过的文件数量
stats['skipped'] += 1
continue
# 符合过滤器 就需要复制出来 创建绝对路径
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'] += 1
print(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 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]
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 marshal
with open('marshal.mar', mode='rb') as file:
data = marshal.load( file)
print(data, type(data))
(4)shelve
唯一一个文件IO操作中,对抽象数据进行存储的高级模块
- 如果需要将抽象数据保存到文件中,优先推荐使用该模块
- shelve模块的操作方式,是所有序列化模块中最简洁、最清晰
将抽象数据,序列化到文件中
import shelve
user_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_dict
db['user_set'] = user_set
db['user_arr'] = user_arr
将文件中的数据,反序列化到代码中
import shelve
with shelve.open("my_shevel") as db:
db['user_arr'] = [66,666,6666]
print(db['user_dict'])
print(db['user_set'])
print(db['user_arr'])