文件读写
# 文件操作
在几乎所有的编程应用中,我们都需要与文件打交道——读取配置文件、保存用户数据、处理日志、分析数据集等等。文件操作是连接程序与永久性存储(如硬盘)的桥梁,是每个 Python 学习者都必须掌握的核心技能。
# 1. open() 函数
在对文件进行任何操作之前,你必须先“打开”它。Python 内置的 open()
函数负责这个任务,它会返回一个文件对象。这个对象非常强大,它不仅包含了文件的信息,还是一个迭代器和上下文管理器,这为后续的 for
循环和 with
语句提供了基础。
# 1.1 open()
语法详解
file_object = open(file, mode='r', encoding=None)
file
(必需): 文件的路径。mode
(可选): 一个字符串,用于指定文件的打开模式。默认为'r'
(只读文本模式)。encoding
(可选): 用于编解码文本文件的编码格式。这是处理文本文件时极其重要的参数。
# 1.2 理解文件路径
相对路径 (Relative Path): 从当前脚本所在的位置开始计算。
"data.txt"
: 表示与你的.py
脚本文件在同一个文件夹下的data.txt
文件。"data/log.txt"
: 表示在当前文件夹下的data
子文件夹中的log.txt
文件。
绝对路径 (Absolute Path): 从文件系统的根目录开始的完整路径,不受脚本位置影响。
- Windows:
"C:\\Users\\Alice\\Documents\\data.txt"
(注意\
需要转义成\\
) 或使用原始字符串r"C:\Users\Alice\Documents\data.txt"
。 - macOS/Linux:
"/home/alice/documents/data.txt"
。
- Windows:
现代化的路径处理 (推荐):
from pathlib import Path # Path 对象让路径操作更直观、更安全 data_folder = Path("data") file_path = data_folder / "log.txt" # 使用 / 运算符拼接路径 # with open(file_path, "r") as f: ...
1
2
3
4
5
# 1.3 深入理解文件模式 (mode
)
模式由 “操作” + “类型” 组合而成。
- 核心操作:
'r'
: 只读 (Read)。文件必须存在,否则FileNotFoundError
。'w'
: 写入 (Write)。清空原有内容。文件不存在则创建。'a'
: 追加 (Append)。在末尾写入。文件不存在则创建。
- 模式修改器:
'+'
: 更新。与r/w/a
组合,变为读写模式。'b'
: 二进制 (Binary)。用于处理图片、音频等非文本文件。't'
: 文本 (Text)。默认模式,通常省略。
常见组合 | 描述 |
---|---|
'r' | 只读文本文件 |
'w' | 覆盖写入文本文件 |
'a' | 追加写入文本文件 |
'rb' | 只读二进制文件 |
'wb' | 覆盖写入二进制文件 |
'r+' | 读写文本文件。指针在开头,写入会覆盖。 |
'a+' | 读写文本文件。指针在末尾,写入是追加。若需读取,要先 seek(0) 。 |
# 1.4 为何 encoding
至关重要?
计算机只认识 0
和 1
。encoding
就是一本“密码本”,它定义了如何将人类的文字(如 '你')转换成计算机能存储的字节(如 b'\xe4\xbd\xa0'
),以及如何反向转换。
- 不指定
encoding
的风险: Python 会使用操作系统的默认编码,在 Windows(可能是GBK
)和 macOS/Linux(可能是UTF-8
)之间移动文件时,极易产生乱码。 - 黄金法则: 处理文本文件时,永远显式指定
encoding='utf-8'
。UTF-8 是全球通用的标准,能表示所有语言的字符。
# 正确的做法
with open("chinese_text.txt", "w", encoding="utf-8") as f:
f.write("你好,世界!")
# 如果用错误的编码读取,会引发错误
try:
with open("chinese_text.txt", "r", encoding="gbk") as f:
print(f.read())
except UnicodeDecodeError as e:
print(f"编码错误!{e}")
# 输出: 编码错误!'gbk' codec can't decode byte 0xa0 in position 8: illegal multibyte sequence
2
3
4
5
6
7
8
9
10
11
# 2. with 语句 (上下文管理器)
with
语句是处理文件等需要“获取”和“释放”资源的最佳方式。它能确保无论代码块是正常结束还是中途发生错误,文件都能被自动、安全地关闭。
with
语句的原理:
它实际上是 try...finally
的一种简写。
# with 语句
with open("my_file.txt", "w") as f:
f.write("Hello")
# 等价的 try...finally 结构
f = open("my_file.txt", "w")
try:
f.write("Hello")
finally:
f.close() # 无论 try 中是否出错,finally 块总会执行
2
3
4
5
6
7
8
9
10
显然,with
语句更简洁、更安全。
# 3. 写入文件:write()
与 writelines()
# 3.1 write(string)
: 写入单个字符串
此方法将一个字符串写入文件,并返回实际写入的字符数。
关键点:
write()
不会自动添加换行符\n
。- 文件缓冲区: 出于效率考虑,Python 在写入时可能先将数据放在内存缓冲区。数据只有在缓冲区满、文件关闭或手动调用
f.flush()
时,才保证被写入硬盘。
with open("log.txt", "w", encoding="utf-8") as f:
f.write("Log started.\n")
f.write("Processing data...")
f.flush() # 强制将缓冲区内容写入磁盘
2
3
4
# 3.2 writelines(lines)
: 写入字符串列表
此方法接收一个字符串列表(或任何可迭代对象),并将其中每个字符串连续写入文件。
关键点: writelines()
也不会为每个字符串添加换行符。它仅仅是 for line in lines: f.write(line)
的一个高效替代。
lines_to_write = ["Event 1\n", "Event 2\n", "Event 3\n"]
with open("events.txt", "w", encoding="utf-8") as f:
f.writelines(lines_to_write)
2
3
# 4. 读取文件:三种核心方法
准备一个用于读取的示例文件 poem.txt
:
静夜思
李白
床前明月光,
疑是地上霜。
2
3
4
# 4.1 read(size=-1)
: 一次性读取
f.read()
: 读取整个文件,返回一个字符串。读完后指针在文件末尾,再次调用返回空字符串''
。f.read(size)
: 读取指定size
数量的字符。- 适用场景: 文件较小,或需要一次性处理全部内容时。对大文件要小心内存溢出。
with open("poem.txt", "r", encoding="utf-8") as f:
content = f.read()
print(f"再次读取: '{f.read()}'") # 输出: 再次读取: ''
2
3
# 4.2 readlines()
: 所有行存入列表
f.readlines()
: 读取所有行,返回一个列表,其中每个元素是文件的一行字符串。- 关键点: 每行末尾的换行符
\n
会被保留。 - 适用场景: 文件不大,且需要对所有行进行索引、排序等列表操作时。
- 处理换行符:
with open("poem.txt", "r", encoding="utf-8") as f: lines_with_newline = f.readlines() stripped_lines = [line.strip() for line in lines_with_newline] print(stripped_lines) # 输出: ['静夜思', '李白', '床前明月光,', '疑是地上霜。']
1
2
3
4
# 4.3 readline()
与迭代:逐行处理 (最高效)
f.readline()
: 每次只读取一行,返回一个字符串。读到文件末尾时返回空字符串''
。for line in f
: 这是处理文本文件最推荐、最高效的方式。它逐行读取,内存占用小,代码简洁。
# 使用 readline() 和 while 循环的底层模拟
with open("poem.txt", "r", encoding="utf-8") as f:
while True:
line = f.readline()
if not line: # 读到文件末尾,line 为 ''
break
print(line.strip())
2
3
4
5
6
7
# 5. 文件对象的属性
文件对象自身也包含一些有用的信息。
with open("poem.txt", "r+", encoding="utf-8") as f:
print(f"文件名: {f.name}") # poem.txt
print(f"打开模式: {f.mode}") # r+
print(f"编码格式: {f.encoding}") # utf-8
print(f"是否可读? {f.readable()}") # True
print(f"是否可写? {f.writable()}") # True
print(f"文件是否关闭? {f.closed}") # False
print(f"\nwith 块结束后,文件是否关闭? {f.closed}") # True
2
3
4
5
6
7
8
9
# 6. 文件指针:seek()
与 tell()
可以把文件指针想象成文本编辑器里的光标,它标记了下一次读写操作的起始位置。
tell()
: 返回指针当前位置的字节数。seek(offset, whence=0)
: 移动指针。offset
: 偏移的字节数。whence
:0
(默认): 从文件头开始。1
: 从当前位置开始。2
: 从文件尾开始。
- 重要: 在文本模式下,
seek()
只保证seek(0)
和seek(offset, 0)
的可靠性。任意的seek(offset, 1)
或seek(offset, 2)
行为可能未定义。要进行精确的字节级移动,必须使用二进制模式 ('b'
)。
with open("poem.txt", "rb") as f: # 注意是二进制模式 'rb'
print(f"指针在开头: {f.tell()}") # 输出: 0
data = f.read(6) # 一个汉字在UTF-8中占3字节,'静夜思\n' 占 3*3+1=10 字节
print(f"读取6字节后: {f.tell()}") # 输出: 6
f.seek(0) # 移回文件开头
print(f"seek(0)后: {f.tell()}") # 输出: 0
2
3
4
5
6
# 7. 稳健编程:处理文件错误
直接操作文件很容易遇到问题,如文件不存在或没有权限。使用 try...except...finally
能让你的程序更健壮。
f = None # 在 try 外初始化,确保 finally 能访问
try:
# with 语句可以写在 try 块内部
with open("non_existent_file.txt", "r") as f:
print(f.read())
except FileNotFoundError:
print("错误:文件未找到!请检查文件名和路径。")
except PermissionError:
print("错误:没有权限读取此文件。")
except Exception as e:
print(f"发生了未知错误: {e}")
finally:
if f and not f.closed:
print("文件在 finally 块中被关闭。")
f.close()
else:
print("程序结束。")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8. 文件系统管理:os 与 pathlib 模块
除了读写文件内容,我们还经常需要管理文件本身和它们所在的目录,例如创建、删除、移动、重命名等。Python 的 os
模块和 pathlib
模块为此提供了强大的支持。
# 8.1 传统方式:os
模块
os
模块是 Python 与操作系统交互的传统接口,功能非常全面。它主要包含两部分:直接的 os
函数(用于目录和文件操作)和 os.path
子模块(用于路径处理)。
# 路径处理 (os.path
)
os.path
模块可以帮助你以跨平台的方式处理文件路径字符串。
import os
path_str = "/Users/alice/Documents/report.txt"
# 1. 路径拼接: os.path.join() - **极其重要**
# 自动使用适合操作系统的路径分隔符('/' 或 '\'),避免手动拼接错误
folder = "project"
filename = "main.py"
full_path = os.path.join(os.getcwd(), folder, filename)
print(f"安全拼接的路径: {full_path}")
# 2. 获取绝对路径: os.path.abspath()
relative_path = "my_script.py"
abs_path = os.path.abspath(relative_path)
print(f"绝对路径: {abs_path}")
# 3. 路径拆分
print(f"目录名: {os.path.dirname(path_str)}") # /Users/alice/Documents
print(f"文件名: {os.path.basename(path_str)}") # report.txt
print(f"目录和文件名元组: {os.path.split(path_str)}") # ('/Users/alice/Documents', 'report.txt')
# 4. 分离文件名和扩展名: os.path.splitext()
file_root, file_ext = os.path.splitext(path_str)
print(f"文件名部分: {file_root}") # /Users/alice/Documents/report
print(f"扩展名部分: {file_ext}") # .txt
# 5. 检查路径
print(f"路径是否存在? {os.path.exists(path_str)}")
print(f"是否为文件? {os.path.isfile(path_str)}")
print(f"是否为目录? {os.path.isdir(os.path.dirname(path_str))}")
# 6. 获取文件大小 (字节)
# 先创建一个文件来获取大小
with open("temp_size_file.txt", "w") as f: f.write("12345")
print(f"文件大小: {os.path.getsize('temp_size_file.txt')} 字节")
os.remove("temp_size_file.txt") # 清理
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 目录操作
# 1. 获取和切换当前工作目录
current_dir = os.getcwd()
print(f"当前工作目录: {current_dir}")
# os.chdir('/target/directory') # 可以切换到其他目录
# 2. 创建目录
# os.mkdir('single_folder') # 只能创建单层目录,如果已存在会报错
os.makedirs('level1/level2', exist_ok=True) # 推荐!可以递归创建多层目录,exist_ok=True 表示如果已存在则不报错
# 3. 列出目录内容
print(f"当前目录内容: {os.listdir('.')}") # '.' 表示当前目录
# 4. 删除目录
os.rmdir('level1/level2') # 只能删除空目录
os.rmdir('level1')
# os.removedirs('path') # 会递归删除路径中所有的空目录
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 文件操作
# 1. 重命名文件
with open("old.txt", "w") as f: f.write("data")
os.rename("old.txt", "new.txt")
# 2. 移动文件 (rename 的妙用)
os.makedirs("storage", exist_ok=True)
os.rename("new.txt", os.path.join("storage", "moved.txt")) # 当目标路径在不同目录时,rename 实现移动效果
# 3. 删除文件
os.remove(os.path.join("storage", "moved.txt"))
os.rmdir("storage")
2
3
4
5
6
7
8
9
10
11
# 8.2 现代方式:pathlib
模块 (推荐)
pathlib
在 Python 3.4+ 中引入,提供了一个面向对象的接口来处理文件系统路径,代码更直观、易读、不易出错。
from pathlib import Path
# 创建 Path 对象
p = Path("/Users/alice/Documents/report.txt")
# 1. 路径拼接 (使用 / 运算符)
project_dir = Path.cwd() # 获取当前工作目录,等同于 os.getcwd()
new_path = project_dir / "data" / "config.json"
print(f"Pathlib 拼接路径: {new_path}")
# 2. 获取路径的各个部分 (作为属性访问)
print(f"父目录: {p.parent}") # /Users/alice/Documents
print(f"文件名: {p.name}") # report.txt
print(f"文件名词干: {p.stem}") # report
print(f"文件后缀: {p.suffix}") # .txt
# 3. 检查路径
print(f"路径是否存在? {new_path.exists()}")
print(f"是否为文件? {p.is_file()}")
print(f"是否为目录? {p.is_dir()}")
# 4. 直接打开文件
# with p.open('r', encoding='utf-8') as f: ...
# 5. 创建和删除目录
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)
# data_dir.rmdir() # 只能删除空目录
# 6. 重命名和移动
# report_path = Path('report.txt')
# report_path.touch() # 创建空文件
# report_path.rename('new_report.txt')
# report_path.rename(data_dir / 'moved_report.txt')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 8.3 综合案例:自动整理下载文件夹
假设你的下载文件夹里乱七八糟,有各种文件。我们来写一个脚本,按文件类型(后缀名)自动将它们归类到不同的子文件夹中。
# 版本 A: 使用 os
模块实现
import os
import shutil # 使用 shutil.move 进行移动更稳健
# 设定源文件夹和目标文件夹
SOURCE_DIR = os.getcwd() # 假设脚本就在下载文件夹里运行
DEST_DIR = os.path.join(SOURCE_DIR, "Organized_Files")
os.makedirs(DEST_DIR, exist_ok=True)
# 遍历源文件夹中的所有条目
for filename in os.listdir(SOURCE_DIR):
source_path = os.path.join(SOURCE_DIR, filename)
# 只处理文件,跳过目录和脚本自身
if os.path.isdir(source_path) or filename.endswith(".py"):
continue
# 获取文件后缀
_, file_ext = os.path.splitext(filename)
file_ext = file_ext[1:].lower() if file_ext else "no_extension" # 去掉点,转小写
# 创建目标子文件夹
dest_folder_path = os.path.join(DEST_DIR, file_ext)
os.makedirs(dest_folder_path, exist_ok=True)
# 移动文件
dest_path = os.path.join(dest_folder_path, filename)
print(f"正在移动: {filename} -> {dest_folder_path}")
shutil.move(source_path, dest_path)
print("文件整理完毕!")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 版本 B: 使用 pathlib
模块实现 (更简洁)
from pathlib import Path
import shutil
SOURCE_DIR = Path.cwd()
DEST_DIR = SOURCE_DIR / "Organized_Files"
DEST_DIR.mkdir(exist_ok=True)
# Path.iterdir() 返回一个生成器,效率更高
for path_object in SOURCE_DIR.iterdir():
# 只处理文件,跳过目录和脚本自身
if path_object.is_dir() or path_object.name.endswith(".py"):
continue
# 获取文件后缀
file_ext = path_object.suffix[1:].lower() if path_object.suffix else "no_extension"
# 创建目标子文件夹
dest_folder_path = DEST_DIR / file_ext
dest_folder_path.mkdir(exist_ok=True)
# 移动文件
dest_path = dest_folder_path / path_object.name
print(f"正在移动: {path_object.name} -> {dest_folder_path}")
shutil.move(str(path_object), str(dest_path)) # shutil.move 接受字符串路径
print("文件整理完毕!")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 9. 综合案例:统计日志中的 IP 地址
这个案例将串联起文件的读取、字符串处理、字典统计和结果写入等多个核心知识点。
任务:读取一个日志文件 access.log
,统计其中每个 IP 地址的访问次数,按访问次数从高到低排序,并将格式化后的报告写入 ip_counts_sorted.txt
。
# 1. 准备日志文件
log_data = """192.168.1.1 - - [10/Mar/2023] "GET /home"
203.0.113.45 - - [10/Mar/2023] "GET /about"
192.168.1.1 - - [10/Mar/2023] "POST /login"
192.168.1.2 - - [10/Mar/2023] "GET /home"
203.0.113.45 - - [10/Mar/2023] "GET /products"
"""
with open("access.log", "w", encoding="utf-8") as f:
f.write(log_data)
# 2. 分析日志并统计
ip_counts = {}
try:
with open("access.log", "r", encoding="utf-8") as f:
for line in f:
if not line.strip(): continue # 跳过空行
ip = line.split(" ")[0]
ip_counts[ip] = ip_counts.get(ip, 0) + 1 # 使用 .get() 更简洁
except FileNotFoundError:
print("日志文件未找到!")
ip_counts = {} # 确保 ip_counts 是一个空字典
# 3. 排序统计结果
# sorted() 返回一个列表,元素是 (ip, count) 元组
# key=lambda item: item[1] 表示按元组的第二个元素(即 count)排序
# reverse=True 表示降序
sorted_ips = sorted(ip_counts.items(), key=lambda item: item[1], reverse=True)
# 4. 将结果写入报告文件
with open("ip_counts_sorted.txt", "w", encoding="utf-8") as report_file:
report_file.write("IP 访问统计报告 (按次数降序)\n")
report_file.write("="*30 + "\n")
for ip, count in sorted_ips:
report_file.write(f"{ip:<20} | 次数: {count}\n") # 左对齐,增加格式美感
print("排序后的报告生成完毕!")
# ip_counts_sorted.txt 内容:
# IP 访问统计报告 (按次数降序)
# ==============================
# 192.168.1.1 | 次数: 2
# 203.0.113.45 | 次数: 2
# 192.168.1.2 | 次数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42