Python 文件操作

297 阅读16分钟

Python 文件操作

1. 文件操作概述

文件操作是编程中非常常见且重要的功能,它允许程序与外部文件进行交互,读取存储在文件中的数据,或将程序生成的数据写入文件。Python 提供了简单而强大的内置函数和模块来处理文件。

2. 文件打开与关闭

在进行文件操作之前,首先需要打开文件。完成操作后,务必关闭文件以释放系统资源。

2.1 open() 函数

使用内置的 open() 函数来打开文件。它至少需要一个文件路径参数,通常还会指定打开模式。

# 基本用法:打开一个文件对象
file_object = open("path/to/your/file.txt", "mode", encoding="utf-8")
  • file_path (文件路径) : 字符串类型,表示要打开的文件的路径(可以是相对路径或绝对路径)。
  • mode (打开模式) : 字符串类型,指定文件打开的方式(读、写、追加、二进制等)。
  • encoding (编码) : 可选参数,指定文件的编码格式,对于文本文件非常重要,例如 'utf-8'。如果不指定,会使用系统默认编码。

2.2 文件打开模式

文件打开模式决定了你可以对文件执行的操作(读、写、追加)以及文件的处理方式(文本或二进制)。

模式描述示例
'r'读取模式 (read) :默认模式。文件必须存在,否则会抛出 FileNotFoundError。文件指针位于文件开头。open('file.txt', 'r')
'w'写入模式 (write) :如果文件不存在,则创建新文件。如果文件已存在,则截断 (清空) 文件内容。文件指针位于文件开头。open('file.txt', 'w')
'a'追加模式 (append) :如果文件不存在,则创建新文件。如果文件已存在,则在文件末尾追加内容,不会清空原有内容。文件指针位于文件末尾。open('file.txt', 'a')
'x'独占创建模式 (exclusive creation) :如果文件已存在,则抛出 FileExistsError。如果文件不存在,则创建并以写入模式打开。用于确保文件是新创建的。open('file.txt', 'x')
'b'二进制模式 (binary) :用于处理非文本文件(如图片、视频、音频等)。必须与 'r', 'w', 'a', 'x' 中的一个结合使用,例如 'rb', 'wb'。在此模式下,读写的数据是字节 (bytes)。open('image.png', 'rb')
't'文本模式 (text) :默认模式。用于处理文本文件。读写的数据是字符串。可以与 'r', 'w', 'a', 'x' 中的一个结合使用,例如 'rt', 'wt'open('text.txt', 'rt') (等同于 'r')
'+'更新模式 (update) :表示可以同时进行读写操作。必须与 'r', 'w', 'a', 'x' 中的一个结合使用,例如 'r+' (读写,文件必须存在), 'w+' (写读,清空文件或创建), 'a+' (追加读写)。open('data.txt', 'r+')

常见模式组合:

  • 'r':只读,文件不存在会报错。
  • 'w':只写,文件不存在则创建,存在则清空内容。
  • 'a':只写,文件不存在则创建,存在则在末尾追加内容。
  • 'rb':只读二进制文件。
  • 'wb':只写二进制文件。
  • 'r+':读写模式,文件必须存在。
  • 'w+':写读模式,文件不存在则创建,存在则清空内容。
  • 'a+':追加读写模式,文件不存在则创建,存在则在末尾追加内容。

2.3 关闭文件

文件操作完成后,必须关闭文件以释放系统资源,并确保所有写入操作都已同步到磁盘。

推荐方式:使用 with 语句 (上下文管理器)

with 语句是处理文件操作的最佳实践。它能确保文件在操作完成后被正确关闭,即使在文件操作过程中发生异常,也能自动处理关闭。

# 写入文本文件示例
try:
    with open("example_write.txt", "w", encoding="utf-8") as file:
        file.write("Hello, Python!\n")
        file.write("这是使用 with 语句写入的第二行。\n")
    print("内容已成功写入 example_write.txt")
except IOError as e:
    print(f"写入文件时发生错误: {e}")

# 读取文本文件示例
try:
    with open("example_write.txt", "r", encoding="utf-8") as file:
        content = file.read()
        print("\n读取到的内容:")
        print(content)
except FileNotFoundError:
    print("错误: example_write.txt 文件不存在。")
except IOError as e:
    print(f"读取文件时发生错误: {e}")

# 追加内容到文本文件示例
try:
    with open("example_write.txt", "a", encoding="utf-8") as file:
        file.write("这是追加的新内容。\n")
    print("\n新内容已追加到 example_write.txt")
except IOError as e:
    print(f"追加内容时发生错误: {e}")

# 处理二进制文件示例 (写入一些字节)
try:
    with open("binary_data.bin", "wb") as file:
        file.write(b'\x01\x02\x03\x04\xff\x00') # 写入字节数据
    print("\n二进制内容已写入 binary_data.bin")
except IOError as e:
    print(f"写入二进制文件时发生错误: {e}")

# 读取二进制文件示例
try:
    with open("binary_data.bin", "rb") as file:
        binary_content = file.read()
        print("读取到的二进制内容:", binary_content)
except FileNotFoundError:
    print("错误: binary_data.bin 文件不存在。")
except IOError as e:
    print(f"读取二进制文件时发生错误: {e}")

传统方式:open()close()

这种方式需要你手动调用 file_object.close() 方法来关闭文件。如果忘记关闭或在操作过程中发生异常,文件可能不会被正确关闭,导致数据丢失或资源泄露。

file_obj = None # 初始化文件对象为 None
try:
    file_obj = open("manual_close_example.txt", "w", encoding="utf-8")
    file_obj.write("这是一个手动关闭文件的例子。\n")
    # 模拟一个可能导致异常的操作
    # 1 / 0
except IOError as e:
    print(f"文件操作错误: {e}")
except Exception as e:
    print(f"发生其他错误: {e}")
finally:
    # 无论是否发生异常,都会执行 finally 块,确保文件被关闭
    if file_obj: # 检查文件对象是否已成功创建
        file_obj.close()
        print("文件已手动关闭。")

3. 读取文件内容

一旦文件以读取模式打开,就可以使用多种方法来获取其内容。

3.1 file.read(size=-1)

  • 功能: 读取文件中的所有内容并作为字符串返回 (文本模式) 或字节串返回 (二进制模式)。
  • size 参数: 可选。如果提供了 size,则读取指定数量的字符 (文本模式) 或字节 (二进制模式)。如果省略或为负数,则读取整个文件。
  • 文件指针: 读取后,文件指针会移动到读取内容的末尾。
with open("example_read.txt", "w", encoding="utf-8") as f:
    f.write("第一行文本。\n")
    f.write("第二行文本。\n")
    f.write("第三行文本。\n")

print("--- 使用 read() 读取 ---")
with open("example_read.txt", "r", encoding="utf-8") as f:
    # 读取前5个字符
    first_chars = f.read(5)
    print(f"读取前5个字符: '{first_chars}'") # 文件指针移动到第5个字符后

    # 继续读取剩余所有内容
    rest_content = f.read()
    print(f"读取剩余内容:\n'{rest_content}'") # 文件指针移动到文件末尾

    # 再次调用 read() 将返回空字符串,因为已到文件末尾
    empty_read = f.read()
    print(f"再次读取 (空): '{empty_read}'")

3.2 file.readline(size=-1)

  • 功能: 读取文件中的一行内容,包括行末的换行符 \n
  • size 参数: 可选。如果提供了 size,则最多读取指定数量的字符。
  • 文件指针: 读取一行后,文件指针会移动到下一行的开头。
print("\n--- 使用 readline() 读取 ---")
with open("example_read.txt", "r", encoding="utf-8") as f:
    line1 = f.readline()
    print(f"读取第一行: '{line1.strip()}'") # .strip() 用于移除首尾空白符,包括换行符

    line2 = f.readline()
    print(f"读取第二行: '{line2.strip()}'")

    line3 = f.readline()
    print(f"读取第三行: '{line3.strip()}'")

    empty_line = f.readline()
    print(f"再次读取 (空): '{empty_line}'")

3.3 file.readlines()

  • 功能: 读取文件中的所有行,并将它们作为字符串列表返回。列表中的每个字符串都包含行末的换行符 \n
  • 文件指针: 读取后,文件指针会移动到文件末尾。
print("\n--- 使用 readlines() 读取 ---")
with open("example_read.txt", "r", encoding="utf-8") as f:
    all_lines = f.readlines()
    print("所有行列表:")
    for line in all_lines:
        print(f"'{line.strip()}'")

3.4 直接迭代文件对象 (推荐用于大文件)

  • 功能: 文件对象本身是可迭代的。可以直接在 for 循环中遍历文件对象,每次迭代读取一行。
  • 优点: 这种方式内存效率非常高,因为它不会一次性将整个文件加载到内存中,而是逐行读取,特别适合处理大型文件。
  • 文件指针: 逐行读取,指针随之移动。
print("\n--- 通过迭代文件对象逐行读取 (推荐) ---")
with open("example_read.txt", "r", encoding="utf-8") as f:
    for line_num, line in enumerate(f, 1):
        print(f"第 {line_num} 行: '{line.strip()}'")

4. 写入文件内容

一旦文件以写入 ('w') 或追加 ('a') 模式打开,就可以向其中写入内容。

4.1 file.write(string)

  • 功能: 将指定的字符串写入文件。
  • 返回值: 返回写入的字符数。
  • 注意: write() 方法不会自动添加换行符。如果你希望每段内容单独成行,需要手动在字符串末尾添加 \n
print("\n--- 使用 write() 写入 ---")
# 'w' 模式会清空文件内容再写入
with open("example_write_single.txt", "w", encoding="utf-8") as f:
    f.write("这是第一行,后面有换行符。\n")
    f.write("这是第二行,后面也有换行符。\n")
    f.write("这是第三行,后面没有换行符。") # 注意这里没有 \n
    print("内容已写入 example_write_single.txt")

# 验证写入结果
with open("example_write_single.txt", "r", encoding="utf-8") as f:
    print("\nexample_write_single.txt 的内容:")
    print(f.read())

4.2 file.writelines(list_of_strings)

  • 功能: 将字符串列表中的所有字符串写入文件。
  • 注意: writelines() 也不会自动添加换行符。你需要确保列表中的每个字符串都包含 \n,以便它们在文件中单独成行。
print("\n--- 使用 writelines() 写入 ---")
lines_to_write = [
    "列表写入的第一行。\n",
    "列表写入的第二行。\n",
    "列表写入的第三行 (这个也有换行符)。\n",
    "列表写入的第四行 (这个没有换行符)。"
]

with open("example_writelines.txt", "w", encoding="utf-8") as f:
    f.writelines(lines_to_write)
    print("内容已通过 writelines 写入 example_writelines.txt")

# 验证写入结果
with open("example_writelines.txt", "r", encoding="utf-8") as f:
    print("\nexample_writelines.txt 的内容:")
    print(f.read())

5. 文件指针操作

文件指针(或文件游标)指示了文件当前读写的位置。

5.1 file.tell()

  • 功能: 返回文件当前指针的位置。对于文本文件,这个位置是字符数;对于二进制文件,是字节数。
  • 返回值: 一个整数,表示从文件开头算起的偏移量。
print("\n--- 文件指针操作: tell() ---")
with open("example_pointer.txt", "w+", encoding="utf-8") as f: # w+ 模式允许读写
    f.write("Hello World!") # 写入 12 个字符
    print(f"写入后指针位置: {f.tell()}") # 12

    f.seek(0) # 将指针移回文件开头
    print(f"移回开头后指针位置: {f.tell()}") # 0

    content = f.read(5) # 读取前5个字符
    print(f"读取5个字符后指针位置: {f.tell()}") # 5
    print(f"读取内容: '{content}'")

5.2 file.seek(offset, whence=0)

  • 功能: 改变文件指针的位置。

  • offset: 偏移量,表示从 whence 指定的位置开始移动的字节数。

  • whence: 偏移的参考位置。

    • 0 (默认值): 文件开头offset 必须是非负整数。
    • 1: 当前位置offset 可以是正数(向前移动)或负数(向后移动)。仅在二进制模式下可用。
    • 2: 文件末尾offset 通常是负数(从末尾向前移动)。仅在二进制模式下可用。
  • 重要注意事项:

    • 文本模式下,seek()offset 参数只能是 0 (文件开头),或者由 tell() 返回的值。这是因为文本编码(如 UTF-8)中一个字符可能占用多个字节,导致精确的字节偏移量难以计算。
    • 对于任意偏移量,通常需要在二进制模式 ('b') 下进行 seek() 操作。
print("\n--- 文件指针操作: seek() ---")
# 文本模式下的 seek
with open("example_seek_text.txt", "w+", encoding="utf-8") as f:
    f.write("Python 文件操作示例。\n")
    f.write("这是第二行。\n")
    f.seek(0) # 移回文件开头
    print(f"文本模式下,移回开头后读取: '{f.read()}'")

    f.seek(6) # 文本模式下,除了0和tell()的返回值,其他seek可能会有意外行为或报错
    # print(f.read()) # 可能会报错或读取不完整字符

# 二进制模式下的 seek (更灵活)
with open("example_seek_binary.bin", "wb+") as f: # wb+ 允许读写二进制
    f.write(b"ABCDEFGH") # 写入 8 字节
    print(f"二进制模式,写入后指针: {f.tell()}") # 8

    f.seek(2) # 从开头偏移 2 字节
    print(f"二进制模式,seek(2) 后指针: {f.tell()}") # 2
    print(f"读取: {f.read(3)}") # 读取 3 字节,输出 b'CDE'

    f.seek(-2, 2) # 从文件末尾向前偏移 2 字节 (whence=2)
    print(f"二进制模式,seek(-2, 2) 后指针: {f.tell()}") # 6 (8-2)
    print(f"读取: {f.read(2)}") # 读取 2 字节,输出 b'GH'

    f.seek(1, 1) # 从当前位置向前偏移 1 字节 (whence=1)
    print(f"二进制模式,seek(1, 1) 后指针: {f.tell()}") # 9 (8+1) - 实际是7+1=8, 因为上面读了2个字节,指针在8
    # 实际上,这里f.tell()会是8,因为上次read(2)后指针在8,再seek(1,1)就会到9。
    # 为了演示,我们重置一下指针
    f.seek(0)
    f.read(5) # 指针到5
    print(f"指针在5,seek(1,1) 后指针: {f.tell()}") # 5
    f.seek(1, 1) # 从当前位置向前偏移 1 字节
    print(f"指针在5,seek(1,1) 后指针: {f.tell()}") # 6
    print(f"读取: {f.read(2)}") # 输出 b'FG'

6. 文件路径操作 (os 模块和 shutil 模块)

os 模块提供了与操作系统交互的功能,包括文件和目录的创建、删除、重命名、判断等。shutil 模块则提供了一些更高级的文件操作,如复制、移动、删除非空目录等。

import os
import shutil

# --- 路径获取与拼接 ---
print("\n--- 文件路径操作 ---")
# 获取当前工作目录
current_directory = os.getcwd()
print(f"当前工作目录: {current_directory}")

# 路径拼接 (推荐使用 os.path.join(),因为它能自动处理不同操作系统的路径分隔符)
folder_name = "my_data_folder"
file_name = "report.txt"
full_path = os.path.join(current_directory, folder_name, file_name)
print(f"拼接后的文件路径: {full_path}")

# --- 目录操作 ---
# 创建单级目录
new_dir = "temp_dir"
if not os.path.exists(new_dir):
    os.mkdir(new_dir)
    print(f"已创建目录: {new_dir}")
else:
    print(f"目录 {new_dir} 已存在。")

# 创建多级目录 (如果父目录不存在也会一并创建)
nested_dir = "project/src/data"
if not os.path.exists(nested_dir):
    os.makedirs(nested_dir)
    print(f"已创建多级目录: {nested_dir}")
else:
    print(f"目录 {nested_dir} 已存在。")

# 列出目录内容
print(f"\n'{current_directory}' 目录内容:")
for item in os.listdir(current_directory):
    print(f"- {item}")

# --- 文件/目录判断 ---
# 创建一个测试文件
with open("test_file.txt", "w") as f:
    f.write("这是一个测试文件。")

print("\n--- 文件/目录判断 ---")
print(f"'test_file.txt' 是否存在: {os.path.exists('test_file.txt')}")
print(f"'test_file.txt' 是否是文件: {os.path.isfile('test_file.txt')}")
print(f"'{new_dir}' 是否是目录: {os.path.isdir(new_dir)}")
print(f"'non_existent_item' 是否存在: {os.path.exists('non_existent_item')}")

# --- 文件/目录重命名 ---
# os.rename("old_name.txt", "new_name.txt") # 重命名文件
# os.rename("old_dir", "new_dir") # 重命名目录

# --- 文件/目录删除 ---
print("\n--- 文件/目录删除 ---")
# 删除文件
if os.path.exists("test_file.txt"):
    os.remove("test_file.txt")
    print("test_file.txt 已删除。")

# 删除空目录
if os.path.exists(new_dir):
    os.rmdir(new_dir)
    print(f"空目录 '{new_dir}' 已删除。")

# 删除非空目录 (使用 shutil.rmtree)
# 注意:这个操作会递归删除目录及其所有内容,请谨慎使用!
if os.path.exists("project"):
    shutil.rmtree("project")
    print("非空目录 'project' 及其所有内容已删除。")

7. 错误处理

在进行文件操作时,可能会遇到各种错误,例如文件不存在、权限不足、磁盘空间不足等。使用 try-except 块是处理这些错误的最佳实践,以增强程序的健壮性。

print("\n--- 错误处理 ---")

# 尝试打开一个不存在的文件
try:
    with open("non_existent_file.txt", "r") as f:
        content = f.read()
        print(content)
except FileNotFoundError:
    print("错误: 文件 'non_existent_file.txt' 不存在!")
except Exception as e: # 捕获其他可能的异常
    print(f"发生未知错误: {e}")

# 尝试写入一个可能没有权限的路径 (例如系统根目录)
# 注意:在某些操作系统或环境下,这可能会真的尝试写入并引发权限错误
try:
    # 假设我们尝试写入一个受保护的路径 (例如 Linux/macOS 的 /root 或 Windows 的 C:\Windows)
    # 这里只是一个示例,实际运行时请勿尝试写入敏感系统目录
    # with open("/root/protected_file.txt", "w") as f:
    #     f.write("尝试写入受保护的文件。")
    print("跳过写入受保护路径的示例,以避免权限错误。")
except PermissionError:
    print("错误: 没有足够的权限写入该文件或目录!")
except IOError as e:
    print(f"发生IO错误: {e}")
except Exception as e:
    print(f"发生未知错误: {e}")

# 确保文件关闭的 finally 块示例 (虽然 with 语句更推荐)
file_handle = None
try:
    file_handle = open("temp_file_for_error.txt", "w")
    file_handle.write("写入一些数据...")
    # 模拟一个错误
    # 1 / 0
except ZeroDivisionError:
    print("捕获到 ZeroDivisionError。")
except Exception as e:
    print(f"捕获到其他异常: {e}")
finally:
    if file_handle:
        file_handle.close()
        print("在 finally 块中关闭了文件。")

8. 常见文件操作场景

8.1 逐行读取大文件

这是处理日志文件或大型数据集的常用方法,可以避免一次性将整个文件加载到内存中。

print("\n--- 常见场景: 逐行读取大文件 ---")
# 创建一个模拟的大文件
with open("large_log.txt", "w", encoding="utf-8") as f:
    for i in range(1000):
        f.write(f"日志条目 {i}: 这是一个模拟的日志信息。\n")

line_count = 0
with open("large_log.txt", "r", encoding="utf-8") as f:
    for line in f:
        line_count += 1
        if line_count <= 5: # 只打印前5行作为示例
            print(f"读取行 {line_count}: {line.strip()}")
        elif line_count == 6:
            print("...(更多行)...")
print(f"总共读取了 {line_count} 行。")
os.remove("large_log.txt") # 清理测试文件

8.2 CSV 文件读写

使用 Python 内置的 csv 模块可以方便地处理 CSV (Comma Separated Values) 文件。

import csv

print("\n--- 常见场景: CSV 文件读写 ---")
# 写入 CSV 文件
csv_data = [
    ["Name", "Age", "City"],
    ["Alice", 30, "New York"],
    ["Bob", 24, "London"],
    ["Charlie", 35, "Paris"]
]

with open("people.csv", "w", newline="", encoding="utf-8") as csvfile:
    csv_writer = csv.writer(csvfile)
    csv_writer.writerows(csv_data)
print("数据已写入 people.csv")

# 读取 CSV 文件
with open("people.csv", "r", newline="", encoding="utf-8") as csvfile:
    csv_reader = csv.reader(csvfile)
    print("\n读取到的 CSV 内容:")
    for row in csv_reader:
        print(row)

# 使用 DictReader/DictWriter (将行作为字典处理)
print("\n--- 常见场景: CSV 文件读写 (字典方式) ---")
dict_data = [
    {"Name": "David", "Age": 28, "City": "Berlin"},
    {"Name": "Eve", "Age": 29, "City": "Tokyo"}
]

with open("more_people.csv", "w", newline="", encoding="utf-8") as csvfile:
    fieldnames = ["Name", "Age", "City"]
    csv_writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    csv_writer.writeheader() # 写入表头
    csv_writer.writerows(dict_data)
print("字典数据已写入 more_people.csv")

with open("more_people.csv", "r", newline="", encoding="utf-8") as csvfile:
    csv_reader = csv.DictReader(csvfile)
    print("\n读取到的 CSV 内容 (字典方式):")
    for row in csv_reader:
        print(row)

os.remove("people.csv") # 清理测试文件
os.remove("more_people.csv") # 清理测试文件

8.3 JSON 文件读写

使用 Python 内置的 json 模块可以方便地处理 JSON (JavaScript Object Notation) 文件。

import json

print("\n--- 常见场景: JSON 文件读写 ---")
# 写入 JSON 文件
data_to_write = {
    "name": "Python User",
    "age": 42,
    "is_active": True,
    "courses": ["Python Basics", "File Operations", "Web Scraping"],
    "address": {
        "street": "123 Main St",
        "city": "Anytown"
    }
}

with open("data.json", "w", encoding="utf-8") as json_file:
    json.dump(data_to_write, json_file, indent=4, ensure_ascii=False)
    # indent=4 使输出更易读,ensure_ascii=False 确保非ASCII字符正常显示
print("数据已写入 data.json")

# 读取 JSON 文件
with open("data.json", "r", encoding="utf-8") as json_file:
    loaded_data = json.load(json_file)
    print("\n读取到的 JSON 内容:")
    print(loaded_data)
    print(f"用户名: {loaded_data['name']}")
    print(f"课程列表: {loaded_data['courses']}")

os.remove("data.json") # 清理测试文件