Python学习历程------文件
- 概述
- 一、文件操作基础
-
- [1.1 `open()` 函数:打开文件](#1.1
open()函数:打开文件) -
- [1. 基本语法:`open(file, mode='r', encoding=None)`](#1. 基本语法:
open(file, mode='r', encoding=None)) - [2. 关键参数:`file` (文件名/路径), `mode` (模式), `encoding` (编码)](#2. 关键参数:
file(文件名/路径),mode(模式),encoding(编码)) - [3. 与Java的对比](#3. 与Java的对比)
- [1. 基本语法:`open(file, mode='r', encoding=None)`](#1. 基本语法:
- [1.2 读取文件内容](#1.2 读取文件内容)
-
- [1. `read(size)`:读取指定字节数或全部内容](#1.
read(size):读取指定字节数或全部内容) - [2. `readline()`:读取一行内容](#2.
readline():读取一行内容) - [3. `readlines()`:读取所有行并返回一个列表](#3.
readlines():读取所有行并返回一个列表) - [4. 迭代文件对象:`for line in file:` (内存效率最高的方式)](#4. 迭代文件对象:
for line in file:(内存效率最高的方式))
- [1. `read(size)`:读取指定字节数或全部内容](#1.
- [1.3 写入文件内容](#1.3 写入文件内容)
-
- [1. `write(string)`:将字符串写入文件](#1.
write(string):将字符串写入文件) - [2. `writelines(list_of_strings)`:将一个字符串列表写入文件](#2.
writelines(list_of_strings):将一个字符串列表写入文件)
- [1. `write(string)`:将字符串写入文件](#1.
- [1.4 关闭文件](#1.4 关闭文件)
-
- [1. `close()` 方法:释放文件资源](#1.
close()方法:释放文件资源) - [2. 文件自动关闭:`with open(...) as f:` 语句块 (推荐)](#2. 文件自动关闭:
with open(...) as f:语句块 (推荐)) - [3. 为什么必须关闭文件:数据缓冲刷新、系统资源限制](#3. 为什么必须关闭文件:数据缓冲刷新、系统资源限制)
- 思考:为什么类似抖音这种平台读写速度很快?怎么能从程序和硬件上优化系统的读写速度?
- [1. `close()` 方法:释放文件资源](#1.
- [1.1 `open()` 函数:打开文件](#1.1
- 二、文件模式与指针控制
-
- [2.1 文件访问模式](#2.1 文件访问模式)
-
- [1. `r`:只读 (默认)](#1.
r:只读 (默认)) - [2. `w`:写入 (文件存在则清空,不存在则创建)](#2.
w:写入 (文件存在则清空,不存在则创建)) - [3. `a`:追加 (在文件末尾写入,不存在则创建)](#3.
a:追加 (在文件末尾写入,不存在则创建)) - [4. `x`:独占创建 (文件已存在则报错)](#4.
x:独占创建 (文件已存在则报错)) - [5. `b`:二进制模式 (用于图片、视频等非文本文件)](#5.
b:二进制模式 (用于图片、视频等非文本文件)) - [6. `t`:文本模式 (默认)](#6.
t:文本模式 (默认)) - [7. `+`:更新模式 (读写)](#7.
+:更新模式 (读写))
- [1. `r`:只读 (默认)](#1.
- [2.2 文件指针 (File Pointer)](#2.2 文件指针 (File Pointer))
-
- [1. `tell()`:获取当前指针位置](#1.
tell():获取当前指针位置) - [2. `seek(offset, whence=0)`:移动指针到指定位置](#2.
seek(offset, whence=0):移动指针到指定位置) - [3. 内存映射文件(先简单了解)](#3. 内存映射文件(先简单了解))
- [1. `tell()`:获取当前指针位置](#1.
- 三、文件与目录管理
-
- [3.1 路径处理:`os.path` 子模块](#3.1 路径处理:
os.path子模块) -
- [1. `os.path.join()`:智能拼接路径 (跨平台)](#1.
os.path.join():智能拼接路径 (跨平台)) - [2. `os.path.abspath()`:获取绝对路径](#2.
os.path.abspath():获取绝对路径) - [3. `os.path.basename()`:获取文件名](#3.
os.path.basename():获取文件名) - [4. `os.path.dirname()`:获取文件所在目录](#4.
os.path.dirname():获取文件所在目录) - [5. `os.path.exists()`:判断路径是否存在](#5.
os.path.exists():判断路径是否存在) - [6. `os.path.isfile()` / `os.path.isdir()`:判断是文件还是目录](#6.
os.path.isfile()/os.path.isdir():判断是文件还是目录) - [7. `os.path.getsize()`:获取文件大小](#7.
os.path.getsize():获取文件大小)
- [1. `os.path.join()`:智能拼接路径 (跨平台)](#1.
- [3.2 文件操作](#3.2 文件操作)
-
- [1. `os.rename()`:重命名文件或目录](#1.
os.rename():重命名文件或目录) - [2. `os.remove()`:删除文件](#2.
os.remove():删除文件)
- [1. `os.rename()`:重命名文件或目录](#1.
- [3.3 目录操作](#3.3 目录操作)
-
- [1. `os.getcwd()`:获取当前工作目录](#1.
os.getcwd():获取当前工作目录) - [2. `os.chdir()`:改变当前工作目录](#2.
os.chdir():改变当前工作目录) - [3. `os.listdir()`:列出目录下的所有文件和子目录](#3.
os.listdir():列出目录下的所有文件和子目录) - [4. `os.mkdir()`:创建单层目录](#4.
os.mkdir():创建单层目录) - [5. `os.makedirs()`:创建多层目录](#5.
os.makedirs():创建多层目录) - [6. `os.rmdir()`:删除空目录](#6.
os.rmdir():删除空目录) - [7. `shutil.rmtree()`:递归删除目录 (危险操作!)](#7.
shutil.rmtree():递归删除目录 (危险操作!))
- [1. `os.getcwd()`:获取当前工作目录](#1.
- [3.1 路径处理:`os.path` 子模块](#3.1 路径处理:
- 四、现代路径操作:pathlib模块
-
- [4.1 `pathlib` 简介:面向对象的路径处理方式](#4.1
pathlib简介:面向对象的路径处理方式) - [4.2 创建路径对象:`Path()`](#4.2 创建路径对象:
Path()) - [4.3 路径拼接与操作:使用 `/` 运算符](#4.3 路径拼接与操作:使用
/运算符) - [4.4 路径属性访问:`.name`, `.parent`, `.stem`, `.suffix`](#4.4 路径属性访问:
.name,.parent,.stem,.suffix) - [4.5 路径状态检查:`.exists()`, `.is_file()`, `.is_dir()`](#4.5 路径状态检查:
.exists(),.is_file(),.is_dir()) - [4.6 文件操作:`.read_text()`, `.write_text()`, `.read_bytes()`, `.write_bytes()`](#4.6 文件操作:
.read_text(),.write_text(),.read_bytes(),.write_bytes()) - [4.7 目录迭代:`.iterdir()`, `.glob()`](#4.7 目录迭代:
.iterdir(),.glob())
- [4.1 `pathlib` 简介:面向对象的路径处理方式](#4.1
- 五、处理常见文件格式
-
- [5.1 CSV (逗号分隔值) 文件:`csv` 模块](#5.1 CSV (逗号分隔值) 文件:
csv模块) -
- [1. 读取CSV:`csv.reader`](#1. 读取CSV:
csv.reader) - [2. 写入CSV:`csv.writer`](#2. 写入CSV:
csv.writer) - [3. 字典形式读写:`csv.DictReader`, `csv.DictWriter`](#3. 字典形式读写:
csv.DictReader,csv.DictWriter)
- [1. 读取CSV:`csv.reader`](#1. 读取CSV:
- [5.2 JSON (JavaScript对象表示法) 文件:`json` 模块](#5.2 JSON (JavaScript对象表示法) 文件:
json模块) -
- [1. 将Python对象写入JSON文件:`json.dump()`](#1. 将Python对象写入JSON文件:
json.dump()) - [2. 从JSON文件读取到Python对象:`json.load()`](#2. 从JSON文件读取到Python对象:
json.load()) - [3. 字符串与Python对象的转换:`json.dumps()`, `json.loads()`](#3. 字符串与Python对象的转换:
json.dumps(),json.loads())
- [1. 将Python对象写入JSON文件:`json.dump()`](#1. 将Python对象写入JSON文件:
- [5.1 CSV (逗号分隔值) 文件:`csv` 模块](#5.1 CSV (逗号分隔值) 文件:
- 六、序列化与对象持久化
-
- [6.1 `pickle` 模块:Python专用二进制协议](#6.1
pickle模块:Python专用二进制协议) - [6.2 序列化 (Pickling):`pickle.dump()` - 将任意Python对象存入文件](#6.2 序列化 (Pickling):
pickle.dump()- 将任意Python对象存入文件) - [6.3 反序列化 (Unpickling):`pickle.load()` - 从文件恢复Python对象](#6.3 反序列化 (Unpickling):
pickle.load()- 从文件恢复Python对象) - [6.4 `pickle` 的优缺点 (速度快,但存在安全风险且不跨语言)](#6.4
pickle的优缺点 (速度快,但存在安全风险且不跨语言))
- [6.1 `pickle` 模块:Python专用二进制协议](#6.1
- 七、高级主题与最佳实践
-
- [7.1 字符编码 (Character Encoding)](#7.1 字符编码 (Character Encoding))
-
- [1. 编码的重要性 (如 `utf-8`, `gbk`)](#1. 编码的重要性 (如
utf-8,gbk)) - [2. 在 `open()` 中指定 `encoding` 参数避免乱码](#2. 在
open()中指定encoding参数避免乱码)
- [1. 编码的重要性 (如 `utf-8`, `gbk`)](#1. 编码的重要性 (如
- [7.2 异常处理 (Error Handling)](#7.2 异常处理 (Error Handling))
-
- [1. `FileNotFoundError`:文件不存在](#1.
FileNotFoundError:文件不存在) - [2. `PermissionError`:权限不足](#2.
PermissionError:权限不足) - [3. `IsADirectoryError`: 尝试像文件一样打开目录](#3.
IsADirectoryError: 尝试像文件一样打开目录) - [4. 使用 `try...except` 块处理文件相关异常](#4. 使用
try...except块处理文件相关异常)
- [1. `FileNotFoundError`:文件不存在](#1.
- [7.3 文件缓冲 (File Buffering)](#7.3 文件缓冲 (File Buffering))
-
- [1. 概念理解:数据并非立即写入磁盘](#1. 概念理解:数据并非立即写入磁盘)
- [2. `file.flush()`:手动将缓冲区内容写入磁盘](#2.
file.flush():手动将缓冲区内容写入磁盘)
- [7.4 临时文件和目录:`tempfile` 模块](#7.4 临时文件和目录:
tempfile模块) - [7.5 内存中的文件:`io.StringIO` 和 `io.BytesIO`](#7.5 内存中的文件:
io.StringIO和io.BytesIO)
- 总结

概述
Python的设计之初就是四个字"
化繁为简",这四个字在文件这一章节体现的淋漓尽致,相比于Java的复杂操作简化的太多了。不过我们还是会依照Java进行比对,针对某些复杂的场景Python和Java究竟哪个更有优势。
一、文件操作基础
1.1 open() 函数:打开文件
1. 基本语法:open(file, mode='r', encoding=None)
python
# 打开一个文件用于读取
# f 是我们得到的文件对象
f = open('my_document.txt', 'r', encoding='utf-8')
# ... 在这里进行读写操作 ...
# 操作完成后,必须关闭文件
f.close()
2. 关键参数:file (文件名/路径), mode (模式), encoding (编码)
- file (文件名/路径):一个字符串,指定要打开的文件的路径。
- 相对路径: 如 'my_document.txt',表示文件位于当前Python脚本运行的目录下。
- 绝对路径: 如 'C:\Users\Admin\Documents\my_document.txt' (Windows) 或 '/home/user/documents/my_document.txt' (Linux/macOS)。
- mode (模式):一个字符串,决定了打开文件的目的。最基础的模式有:
- 'r':Read (读取) - 默认模式。如果文件不存在,会抛出 FileNotFoundError 异常。
- 'w':Write (写入) - 如果文件存在,会清空原有内容;如果文件不存在,则会创建新文件。
- 'a':Append (追加) - 在文件末尾追加内容。如果文件不存在,则会创建新文件。
(更多模式如 'b', '+' 等将在第二章详细介绍。)
-
encoding (编码):指定用于解码(读取时)或编码(写入时)文件的编码格式。
- 这是一个至关重要的参数,尤其是在处理文本文件时。
- 如果不指定,Python会使用操作系统的默认编码,这可能导致在不同系统上(如Windows的gbk和Linux/macOS的utf-8)出现乱码问题。
完整的open语法如下:
python
def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)

一般会使用到的其它参数有:
- buffering:缓冲区大小,可以看下面的内容进行了解。
- errors :指定在编码或解码过程中遇到无法处理的字符时该怎么办。
- 只在文本模式下有效。
- 'strict':默认值。如果遇到编码错误,立即抛出 UnicodeError 异常。
- 'ignore':忽略无法解码的字符。这可能会导致数据丢失,需谨慎使用。
- 'replace':将无法解码的字符替换为一个标记,通常是问号 ?
- newline :控制"通用换行符模式"如何工作。
- 只在文本模式下有效。
- None:默认值。启用通用换行符。读取时,'\n', '\r', '\r\n' 都会被统一转换为 '\n'。写入时,'\n' 会被转换成系统的默认行分隔符(Windows上是\r\n,Linux/macOS上是\n)。
- ' ' (空字符串):不进行任何换行符转换。读取时按原样返回,写入时\n就是\n。在处理CSV文件时, 通常需要设置为 newline='',以防止 csv 模块自己处理换行时出现空行。
- '\n', '\r', '\r\n': 写入时,强制使用指定的字符作为换行符。
3. 与Java的对比
python
// 需要处理可能抛出的异常
try {
// 1. 创建一个FileOutputStream来连接到物理文件
FileOutputStream fos = new FileOutputStream("data.txt");
// 2. 创建一个OutputStreamWriter来指定编码
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
// 3. 使用BufferedWriter来获得缓冲功能,提高效率
BufferedWriter writer = new BufferedWriter(osw);
// ... 在这里使用 writer 对象进行写入 ...
writer.close(); // 关闭流
} catch (IOException e) {
e.printStackTrace();
}
可以看出Java打开文件的步骤很繁琐,Python只需要一行即可。
1.2 读取文件内容
假设文件名为poem.txt,内容如下
txt
Hello, world.
Welcome to Python.
Enjoy your journey.
1. read(size):读取指定字节数或全部内容
- size参数是指定一次读取文件大小的,如果不指定就读取全部,最后返回一个字符串。
- 但是一定要注意⚠️:如果文件太大会导致内存耗尽,因为文件读取的操作大部分都是经过内存的,内存往往没有那么大。
python
with open('poem.txt', 'r', encoding='utf-8') as f:
content = f.read()
print(content)
# 输出:
# Hello, world.
# Welcome to Python.
# Enjoy your journey.
2. readline():读取一行内容
- 每次调用 f.readline() 会读取文件中的一行(直到并包括换行符 \n)。
- 当文件读取到
末尾时,它会返回一个空字符串' '。
python
with open('poem.txt', 'r', encoding='utf-8') as f:
line1 = f.readline()
print(f"第一行: {line1.strip()}") # .strip() 去掉末尾的换行符
line2 = f.readline()
print(f"第二行: {line2.strip()}")
# 输出:
# 第一行: Hello, world.
# 第二行: Welcome to Python.
注意这里的
with关键字,这是Python中替代try except finally的重要属性,会自动关闭资源,即使出现了异常。
3. readlines():读取所有行并返回一个列表
- f.readlines() 会一次性读取所有行,并将它们作为一个字符串列表返回。列表中的每个元素都是文件的一行(包含末尾的换行符)。
- ⚠️警告: 与 read() 类似,这也可能消耗大量内存。
python
with open('poem.txt', 'r', encoding='utf-8') as f:
lines = f.readlines()
print(lines)
# 输出:
# ['Hello, world.\n', 'Welcome to Python.\n', 'Enjoy your journey.']
4. 迭代文件对象:for line in file: (内存效率最高的方式)
- 一次读取一行到内存,效率最高。
python
with open('poem.txt', 'r', encoding='utf-8') as f:
for line in f:
print(line.strip()) # 推荐在循环内部处理每一行
# 输出:
# Hello, world.
# Welcome to Python.
# Enjoy your journey.
- Java8之后的stream流简化了操作,但Python依旧是经典
java
try (Stream<String> stream = Files.lines(Paths.get("poem.txt"))) {
stream.forEach(System.out::println);
}
1.3 写入文件内容
写入操作需要以 'w' 或 'a' 模式打开文件。
1. write(string):将字符串写入文件
- 此方法将指定的字符串写入文件。
- 注意: write() 不会自动添加换行符。你需要手动添加 \n。
python
with open('output.txt', 'w', encoding='utf-8') as f:
f.write("这是第一行。\n")
f.write("这是第二行。")
2. writelines(list_of_strings):将一个字符串列表写入文件
- 此方法接收一个字符串列表(或任何可迭代对象),并将其中每个字符串连续写入文件。
- 注意: 和 write() 一样,writelines() 也
不会在元素之间添加任何分隔符或换行符。
python
lines_to_write = ['第一行\n', '第二行\n', '第三行\n']
with open('output_lines.txt', 'w', encoding='utf-8') as f:
f.writelines(lines_to_write)
注意在Java中有一个newLine方法可以避免我们手动插入换行符,但目前来看,大多数系统已经完全支持\n操作符。
java
// BufferedWriter 提供了更高效的写入
writer.write("some string");
writer.newLine(); // 推荐使用 newLine() 来换行,它会写入平台相关的换行符
1.4 关闭文件
1. close() 方法:释放文件资源
python
f = open('data.txt', 'w')
try:
f.write('hello')
finally:
# 无论 try 块中是否发生错误,finally 块都会执行
f.close()
2. 文件自动关闭:with open(...) as f: 语句块 (推荐)
无论 with 块内部的代码是正常执行完毕,还是中途发生异常,Python都会自动保证文件被正确关闭。
python
# 这段代码等价于上面的 try...finally 结构,但更简洁、更安全
with open('data.txt', 'w', encoding='utf-8') as f:
f.write('Hello, Python!')
# 当代码执行离开 with 语句块时,f.close() 会被自动调用
3. 为什么必须关闭文件:数据缓冲刷新、系统资源限制
- 系统资源限制:操作系统能同时打开的文件句柄数量是有限的。如果程序打开了大量文件却不关闭,会耗尽系统资源,导致程序或系统崩溃。
- 数据缓冲:为了提高效率,向文件写入数据时,内容通常会先存放在内存的缓冲区中,当缓冲区满了或文件关闭时,才会一次性写入磁盘。如果不关闭文件,你写入的最后一部分数据可能永远留在缓冲区里,导致数据丢失。
思考:为什么类似抖音这种平台读写速度很快?怎么能从程序和硬件上优化系统的读写速度?
按照上面所说,一个系统可以打开的文件句柄是有上限的,例如Linux系统,一个进程默认只能打开1024个文件句柄,整个系统的上限大概在几十万------几百万之间,而抖音这种平台可能同时在线的用户都在几千万甚至上亿,到底是怎么处理的呢?
抖音平台怎么处理
| 维度/关注点 | 无状态的HTTP流式传输 | HTTPS传输 |
|---|---|---|
| 核心概念 | 一种数据传输策略 | 一个安全通信协议 |
| 解决的问题 | 如何高效、可扩展地传输大文件,减少等待时间和资源消耗。 | 如何保证通信过程的机密性、完整性和对方身份的真实性。 |
| 关键技术 | HTTP Range请求、分块传输编码 (Chunked Transfer Encoding) | SSL/TLS 加密、数字证书、哈希算法 |
| 生活中的比喻 | 分期送货。一个大包裹(视频)分成很多小件,每次只送一件。快递员(服务器)送完就走,不记得到底送了多少件。 | 使用武装押运车送货。无论送的是什么,送几次,整个运输过程都被严密保护,防止被偷窥和抢劫。 |
| 它们的关系 | 正交、可组合。它们是两个不同维度的事情。 | 正交、可组合。HTTPS 为 HTTP 提供了安全层。 |
- 无状态的http流式传输
上面的表格有两个很重要的特性:
无状态和流传输。而https是安全的、可控、可追溯的一种协议,不会使用在视频或图片等。
- 无状态 :每一次HTTP请求都是完全独立的、自包含的。服务器不会记录客户端之前的任何请求信息,类似看到哪里了等等,而且即使某台服务器宕机,立马会转发请求到另一台服务器,无缝衔接。
- 流传输 :服务器一块一块的发送,客户端一块一块的接收,块的大小可以调整,保证了及时的响应,也不会拖慢速度,因为内存中根本就不会存在整个文件。
- 内容分发网络(CDN)
这是非常重要的核心,视频这种文件都是存放在专业的分布式系统中,通过CDN的方式进行全球分发。
- 源站 (Origin Server):视频上传后,被存放在一个
高可靠的中心存储,比如Amazon S3, Google Cloud Storage等。 - 边缘节点 (Edge Nodes):CDN在全球各地部署了
成千上万的缓存服务器,这些就是边缘节点。 - 智能DNS解析 :当您在北京刷抖音时,DNS会把视频的请求解析到离您
最近的、速度最快的北京或天津的CDN边缘节点上。 - 缓存命中 (Cache Hit):如果这个视频已经被其他北京用户看过,它很可能已经被缓存(Cache)在该节点上了。节点会直接从自己的高速硬盘/内存中把数据块发给您,根本
不需要请求源站。
CDN的作用是决定性的:
- 极低延迟:用户从地理位置最近的服务器获取数据。
- 极高并发:上亿用户的请求被分散到了全球成千上万个CDN节点上,每个节点只需要处理一小部分流量。
- 保护源站:源站的负载被大大降低,可能一个热门视频一天也只被源站读取几次(被不同地区的CDN节点首次请求时)。
何为分布式系统,注意哈,这是系统,不是服务器,就一句话:用一堆普通的、廉价的服务器,通过精巧的软件设计和协议,组合成一个超级稳定、海量容量、性能卓越的"虚拟"存储巨无霸。
分布式系统的核心是将数据分块然后分布存储,即使有多台服务器宕机,你的数据也不会丢失,就像一个视频会切割为几百几千份,然后复制多份存放在不同地区的服务器。
| 系统名称 | 类型 | 典型应用场景 | 简单说明 |
|---|---|---|---|
| HDFS | 文件系统 | 大数据分析(Hadoop生态) | 适合一次写入、多次读取的批量处理场景,吞吐量高。 |
| Ceph | 统一存储 | 云平台(OpenStack, Kubernetes) | 能同时提供对象存储、块存储、文件系统三种服务,非常灵活。 |
| Amazon S3 | 对象存储 | 互联网应用、静态网站、备份归档 | 通过简单的API(PUT/GET)来存储海量非结构化数据(图片、视频)。 |
| GlusterFS | 文件系统 | 媒体仓库、日志存储 | 通过堆叠普通服务器形成一个大容量的分布式文件系统。 |
| Redis Cluster | 内存数据库 | 高速缓存、会话存储 | 将数据分片存储在多个Redis节点的内存中,性能极高。 |
| Cassandra | 数据库 | 写入密集型的Web应用(如消息、物联网) | 无中心架构,所有节点平等,扩展性和可用性极好。 |
- 负载均衡
即使在同一个CDN区域,也不会只有一台服务器。请求会先到达一个
负载均衡器(熟悉吧,nginx是我们常用的),它会像交通警察一样,把请求分发给后面一组服务器中当前最空闲的一台。这确保了没有单台服务器会被压垮。
从程序和硬件上优化系统的读写速度
程序与软件层面
- 使用缓冲 (Buffering) :这是最基础也是最重要的优化。直接对文件进行频繁的小数据块读写,会导致大量的系统调用,开销巨大。
缓冲I/O会将多次小写入合并成一次大写入,或者预先读取一大块数据到内存中,从而显著减少系统调用次数。
- Python实践: open()函数默认就是带缓冲的。你可以通过buffering参数控制缓冲区大小。
- Java实践: FileInputStream是无缓冲的,而BufferedInputStream和BufferedWriter则提供了缓冲功能,通常推荐组合使用它们。
-
异步I/O (Asynchronous I/O):在传统(同步)I/O中,当程序发起一个读写请求时,它会被阻塞,直到操作完成。在异步I/O中,程序发起请求后可以立即返回,继续做其他事情。当I/O操作完成时,操作系统会通过回调、事件等方式通知程序。
-
内存映射文件 (Memory-Mapped Files) :这是一种
高级技术,它将文件的一部分或全部内容直接映射到进程的虚拟地址空间。之后,你可以像操作内存数组一样直接读写文件内容,省去了read()/write()的系统调用和内存拷贝。操作系统会负责在需要时将内存中的修改同步回磁盘。 -
选择合适的数据结构和序列化格式:
-
文本 vs. 二进制: JSON, XML, CSV等文本格式可读性好,但解析慢、体积大。像Protocol Buffers, Avro, Parquet等二进制格式通常更紧凑,读写速度快得多。
-
数据压缩: 对数据进行压缩(如Gzip, Snappy)可以减少磁盘I/O量,但会增加CPU开销。需要根据CPU和磁盘速度进行权衡。
- 利用操作系统的特性:
- 例如Linux的sendfile()系统调用,可以直接在两个文件描述符之间(如文件和网络socket)传输数据,完全在内核态完成,避免了数据在内核和用户空间之间的多次拷贝,效率极高。很多高性能Web服务器(如Nginx)都用它来发送静态文件。
硬件与系统配置层面 (Hardware & System Configuration)
- 存储介质升级(效果最显著):
HDD(机械硬盘): 速度最慢,依赖物理磁头的寻道和旋转,随机读写性能很差。SATA SSD(固态硬盘): 比HDD快一个数量级,没有机械部件,随机读写性能优秀。NVMe SSD: 目前消费级和企业级的顶级选择,通过PCIe总线直接与CPU连接,延迟更低,速度比SATA SSD快几倍到几十倍。
-
增加内存 (RAM):操作系统会利用所有空闲的内存作为页面缓存 (Page Cache)。当你读取一个文件时,操作系统会将其内容缓存到RAM中。下次再读取同一文件时,如果它还在缓存里,就会直接从飞快的RAM中获取,而不是慢速的磁盘。更多的RAM意味着更大的缓存,能命中缓存的概率就更高。
-
使用RAID (磁盘阵列):通过组合多块硬盘来提升性能或可靠性。
- RAID 0 (条带化): 将数据分块写入多块硬盘,读写速度理论上是单块硬盘的N倍(N为硬盘数量)。缺点是没有数据冗余,一块硬盘损坏,所有数据丢失。
- RAID 10 (镜像+条带): 兼具RAID 0的速度和RAID 1的可靠性,是数据库等高性能、高可用场景的常用选择。
- 选择合适的文件系统 (File System):不同的文件系统有不同的特性和性能表现。
- ext4: Linux下最常见、最成熟的选择,综合性能好。
- XFS: 对于处理海量大文件有性能优势。
- ZFS/Btrfs: 提供快照、内建压缩、数据校验等高级功能,但可能带来一些性能开销。
- CPU: 虽然I/O操作本身不主要消耗CPU,但相关任务如数据压缩/解压、序列化/反序列化、加密/解密等都是CPU密集型的。一颗强大的CPU可以更快地处理数据,为I/O操作做好准备。
二、文件模式与指针控制
2.1 文件访问模式
| 模式 | 描述 | 文件不存在 | 指针位置 | 读写权限 |
|---|---|---|---|---|
r |
只读 | 报错 | 开头 | 只能读 |
r+ |
读写 | 报错 | 开头 | 可读可写 |
w |
只写 | 创建 | 开头 | 只能写(会清空文件) |
w+ |
读写 | 创建 | 开头 | 可读可写(会清空文件) |
a |
追加只写 | 创建 | 结尾 | 只能写(追加) |
a+ |
追加读写 | 创建 | 结尾 | 可读可写(追加) |
x |
排他创建 | 必须不存在 | 开头 | 只能写 |
1. r:只读 (默认)
- open()函数的默认模式。如果文件不存在,会抛出FileNotFoundError 异常。尝试写入文件会抛出UnsupportedOperation异常。
python
try:
f = open('data.txt', 'r')
content = f.read()
print(content)
f.close()
except FileNotFoundError:
print("文件不存在")
2. w:写入 (文件存在则清空,不存在则创建)
如果文件存在,会立即清空文件内容;如果文件不存在,则会创建一个新文件。此模式只能写入,不能读取。
python
# 这会覆盖data.txt的全部内容
with open('data.txt', 'w') as f:
f.write('Hello, World!')
3. a:追加 (在文件末尾写入,不存在则创建)
- 如果文件存在,新的内容会
追加到文件末尾;如果文件不存在,则会创建一个新文件。此模式只能写入,不能读取
python
with open('log.txt', 'a') as f:
f.write('New log entry.\n')
4. x:独占创建 (文件已存在则报错)
- 只用于
创建新文件并写入。如果文件已经存在,会抛出FileExistsError异常。这是一种更安全的文件创建方式,可以避免意外覆盖。
python
try:
with open('config.json', 'x') as f:
f.write('{}')
except FileExistsError:
print("配置文件已存在,无法创建。")
接下来的三种是
模式修饰符,和上述的r、w、a、x 组合使用
5. b:二进制模式 (用于图片、视频等非文本文件)
- 用于
处理非文本文件,如图片、音频、视频等。在二进制模式下,读写的是字节(bytes)而不是字符串(str)。它不进行任何编码或换行符转换
python
with open('image.jpg', 'rb') as f:
image_data = f.read() # image_data是bytes类型
with open('image_copy.jpg', 'wb') as f:
f.write(image_data)
6. t:文本模式 (默认)
- 这是
默认模式,用于处理文本文件。它会自动处理平台相关的行尾符(如Windows的\r\n转为\n),并使用默认或指定的编码(如UTF-8)对内容进行编解码。 Java中所有基于Reader和Writer的类(如FileReader, FileWriter)都是处理文本数据的。它们在内部处理字符编码。
7. +:更新模式 (读写)
- 将
基础模式扩展为读写。例如,r+是可读可写的只读模式(文件必须存在),w+是可读可写的写入模式(文件被清空),a+是可读可写的追加模式(初始指针在末尾)。
2.2 文件指针 (File Pointer)
💡 文件指针可以想象成文本编辑器中的光标。它标记了下一次读写操作将要发生的位置,以字节为单位。
1. tell():获取当前指针位置
- 返回一个整数,表示指针
当前位置距离文件开头的字节数。
python
with open('data.txt', 'rb') as f:
f.read(5) # 读取5个字节
position = f.tell()
print(f"读取5字节后,指针位置在: {position}") # 输出: 读取5字节后,指针位置在: 5
2. seek(offset, whence=0):移动指针到指定位置
- offset:要移动的字节数。
- whence :参考点。
whence=0:从文件开头计算 (默认)whence=1:从当前位置计算whence=2:从文件末尾计算
- 注意:在文本模式下该方法受限,尽量在二进制模式下使用,常见的使用就在视频中。
python
with open('data.bin', 'rb') as f:
# whence=0: 移动到第10个字节处
f.seek(10)
print(f"当前位置: {f.tell()}") # 10
# whence=1: 从当前位置向后移动5个字节
f.seek(5, 1)
print(f"当前位置: {f.tell()}") # 15
# whence=2: 移动到文件末尾倒数8个字节处
f.seek(-8, 2)
print(f"当前位置: {f.tell()}")
3. 内存映射文件(先简单了解)
对于超大文件,反复使用read和seek可能会导致性能瓶颈。Python的
mmap模块和Java的java.nio.MappedByteBuffer允许你将文件的一部分直接映射到内存中。操作系统会负责处理实际的I/O,你可以像操作一个巨大数组一样操作文件,性能极高。
三、文件与目录管理
3.1 路径处理:os.path 子模块
💡 os.path模块的核心思想是:对路径字符串进行操作,而不关心路径是否真实存在。
1. os.path.join():智能拼接路径 (跨平台)
- 这是处理路径拼接的正确方式。它会根据当前操作系统自动使用正确的路径分隔符(Windows上是\,Linux/macOS上是/)。
python
import os
path = os.path.join('home', 'user', 'documents', 'file.txt')
print(path) # 在Linux上输出 'home/user/documents/file.txt'
# 在Windows上输出 'home\\user\\documents\\file.txt'
2. os.path.abspath():获取绝对路径
- 将一个相对路径(如'./data.csv')转换为从根目录开始的完整路径。
python
import os
abs_path = os.path.abspath('myfile.txt')
print(abs_path) # 输出如 '/home/user/project/myfile.txt'
3. os.path.basename():获取文件名
- 从一个完整路径中提取出最后一部分,通常是文件名或目录名。
python
import os
path = '/home/user/data.csv'
filename = os.path.basename(path)
print(filename) # 输出 'data.csv'
再一次感慨,为什么Python方便了,这在Java一般都需要自己写一个工具类实现。
4. os.path.dirname():获取文件所在目录
- 从一个完整路径中提取出除了最后一部分之外的所有内容。
python
import os
path = '/home/user/data.csv'
dir_name = os.path.dirname(path)
print(dir_name) # 输出 '/home/user'
5. os.path.exists():判断路径是否存在
- 查一个路径(文件或目录)是否真实存在于文件系统中。返回True或False。
python
import os
if os.path.exists('/home/user/data.csv'):
print("文件存在")
6. os.path.isfile() / os.path.isdir():判断是文件还是目录
- isfile()判断路径是否存在且为文件,isdir()判断是否存在且为目录。
python
import os
path = '/home/user'
print(f"是文件吗? {os.path.isfile(path)}") # False
print(f"是目录吗? {os.path.isdir(path)}") # True
7. os.path.getsize():获取文件大小
- 返回文件的大小,单位是字节。如果路径是目录或不存在,会抛出异常。
python
import os
size_in_bytes = os.path.getsize('my_large_file.zip')
print(f"文件大小: {size_in_bytes / 1024:.2f} KB")
3.2 文件操作
1. os.rename():重命名文件或目录
- 将src(源路径)重命名为dst(目标路径)。也可以用来移动文件。
python
import os
# 创建一个源文件
with open('old_name.txt', 'w') as f:
f.write('rename test')
print(f"重命名前,'old_name.txt' 是否存在: {os.path.exists('old_name.txt')}")
# 执行重命名
os.rename('old_name.txt', 'new_name.txt')
print(f"重命名后,'old_name.txt' 是否存在: {os.path.exists('old_name.txt')}")
print(f"重命名后,'new_name.txt' 是否存在: {os.path.exists('new_name.txt')}")
os.remove('new_name.txt') # 清理
虽然给出的示例是直接的文件名,但因为这是基于当前项目的路径,一般都是需要完整的路径进行重命名和移动。
2. os.remove():删除文件
- 删除指定路径的文件。如果路径是一个目录,会抛出IsADirectoryError。
python
import os
# 创建一个待删除的文件
with open('file_to_delete.txt', 'w') as f:
f.write('delete me')
print(f"删除前,文件是否存在: {os.path.exists('file_to_delete.txt')}")
# 执行删除
os.remove('file_to_delete.txt')
print(f"删除后,文件是否存在: {os.path.exists('file_to_delete.txt')}")
3.3 目录操作
1. os.getcwd():获取当前工作目录
- 返回程序当前正在运行的目录路径。
python
import os
current_dir = os.getcwd()
print(f"当前工作目录是: {current_dir}")
Java一般是:System.getProperty("user.dir")
2. os.chdir():改变当前工作目录
- 将程序的当前工作目录更改为指定的路径。
python
import os
original_dir = os.getcwd()
print(f"原始目录: {original_dir}")
# 创建并进入一个新目录
if not os.path.exists('temp_dir'): os.mkdir('temp_dir')
os.chdir('temp_dir')
print(f"切换后目录: {os.getcwd()}")
# 切回原始目录
os.chdir(original_dir)
print(f"切回后目录: {os.getcwd()}")
os.rmdir('temp_dir') # 清理
⚠️ 注意:这个方法一定要慎用,这是基于进程维度的操作,如果你变了,那整个系统都会受到影响(仅限使用相对路径读取文件的时候),同时对一些方法的使用也会有影响,比如同时存在两个DateUtils,本来A的优先级高,结果切换后导致B的优先级高。
3. os.listdir():列出目录下的所有文件和子目录
- 返回一个列表,包含指定目录下所有文件和子目录的名称。
python
import os
# 创建一个测试环境
os.mkdir('list_dir')
open(os.path.join('list_dir', 'file.txt'), 'w').close()
os.mkdir(os.path.join('list_dir', 'subdir'))
# 列出内容
contents = os.listdir('list_dir')
print(f"'list_dir' 目录下的内容: {contents}") # 输出可能为 ['file.txt', 'subdir'] (顺序不定)
# 清理
os.remove(os.path.join('list_dir', 'file.txt'))
os.rmdir(os.path.join('list_dir', 'subdir'))
os.rmdir('list_dir')
注意:这可不自动递归子目录,就是直接下级。
4. os.mkdir():创建单层目录
- 创建一个目录。如果父目录不存在,会抛出FileNotFoundError。
python
import os
dir_name = 'single_dir'
if not os.path.exists(dir_name):
os.mkdir(dir_name)
print(f"目录 '{dir_name}' 创建成功。")
print(f"目录是否存在: {os.path.isdir(dir_name)}")
os.rmdir(dir_name) # 清理
5. os.makedirs():创建多层目录
- 递归创建目录。如果中间目录不存在,会自动创建。
python
import os
import shutil
path = os.path.join('data', 'images', 'thumbnails')
if not os.path.exists(path):
os.makedirs(path)
print(f"多层目录 '{path}' 创建成功。")
print(f"路径是否存在: {os.path.exists(path)}")
shutil.rmtree('data') # 使用shutil清理整个树
6. os.rmdir():删除空目录
- 删除一个空目录。如果目录不为空,会抛出OSError。
python
import os
dir_name = 'empty_folder'
os.mkdir(dir_name) # 创建一个空目录
print(f"删除前,目录是否存在: {os.path.exists(dir_name)}")
os.rmdir(dir_name) # 删除它
print(f"删除后,目录是否存在: {os.path.exists(dir_name)}")
7. shutil.rmtree():递归删除目录 (危险操作!)
- 来自shutil模块。它会删除一个目录及其包含的所有内容,无论目录是否为空。使用时必须极其小心。
python
import os
import shutil
# 创建一个复杂的目录结构
os.makedirs('tree_to_delete/subfolder')
with open('tree_to_delete/subfolder/file.txt', 'w') as f:
f.write('test')
path = 'tree_to_delete'
print(f"删除前,目录树是否存在: {os.path.exists(path)}")
# !! 危险操作 !!
shutil.rmtree(path)
print(f"删除后,目录树是否存在: {os.path.exists(path)}")
总之,这个方法慎用。
四、现代路径操作:pathlib模块
4.1 pathlib 简介:面向对象的路径处理方式
💡 在pathlib出现之前,处理路径需要导入os.path模块并使用一系列函数,如os.path.join(), os.path.exists()。这些函数接收和返回字符串。pathlib改变了这一点,它让你创建一个Path对象,然后直接在这个对象上调用方法,如path.exists()。这种方式将数据(路径本身)和操作(检查存在性、获取父目录等)封装在一起,是典型的面向对象设计。
怎么说呢,Java中文件的类型是File,类自带一些方法,包括后面出现的NIO中的Path,Files工具类等都是面向对象编程,而Python中大多数都是基于os模块将文件传入进行操作的,所以也出现了Path对象,只不过我感觉不太习惯,因为毕竟是Path,老是把它当做一个路径。
4.2 创建路径对象:Path()
- 从pathlib模块导入Path类,然后用
一个字符串路径来实例化它。你也可以使用类方法Path.cwd()获取当前工作目录或Path.home()获取用户主目录。
python
from pathlib import Path
# 从字符串创建Path对象
p = Path('documents/report.docx')
print(p)
# 获取当前工作目录
current_dir = Path.cwd()
print(f"当前目录: {current_dir}")
# 获取用户主目录
home_dir = Path.home()
print(f"主目录: {home_dir}")
4.3 路径拼接与操作:使用 / 运算符
- 这是pathlib最优雅和最受欢迎的特性之一。你可以使用
斜杠/运算符来拼接路径,pathlib会自动处理好不同操作系统的分隔符。这比os.path.join()更具可读性。
python
from pathlib import Path
home = Path.home()
# 使用 / 运算符进行路径拼接
full_path = home / 'documents' / 'projects' / 'main.py'
print(full_path)
# 在Linux上输出: /home/user/documents/projects/main.py
# 在Windows上输出: C:\Users\user\documents\projects\main.py
4.4 路径属性访问:.name, .parent, .stem, .suffix
💡 Path对象提供了方便的属性来直接获取路径的各个部分,无需进行字符串解析
- .name: 路径的最后一部分(文件名或目录名)。
- .parent: 包含此路径的父目录。
- .stem: 文件名中去掉最后一个后缀的部分。
- .suffix: 最后一个后缀名(包括点)。
python
from pathlib import Path
p = Path('/home/user/data/archive.tar.gz')
print(f"完整名称: {p.name}") # 'archive.tar.gz'
print(f"父目录: {p.parent}") # '/home/user/data'
print(f"文件名主干: {p.stem}") # 'archive.tar' (注意:只去掉最后一个后缀)
print(f"后缀: {p.suffix}") # '.gz'
print(f"所有后缀: {p.suffixes}") # ['.tar', '.gz']
4.5 路径状态检查:.exists(), .is_file(), .is_dir()
- 这些方法直接在Path对象上调用,用于查询文件系统的真实状态。
python
from pathlib import Path
p = Path('.') # 当前目录
print(f"'{p}' 是否存在? {p.exists()}") # True
print(f"'{p}' 是文件吗? {p.is_file()}") # False
print(f"'{p}' 是目录吗? {p.is_dir()}") # True
4.6 文件操作:.read_text(), .write_text(), .read_bytes(), .write_bytes()
- 这些是极其方便的快捷方法,用于
简单的文件读写。它们在内部处理了打开和关闭文件的整个流程。
python
from pathlib import Path
p = Path('greeting.txt')
# 写入文本 (自动处理 open/write/close)
p.write_text('Hello, pathlib!', encoding='utf-8')
# 读取文本
content = p.read_text(encoding='utf-8')
print(content) # 'Hello, pathlib!'
p.unlink() # 删除文件,相当于 os.remove()
Java中也有类似的方法
- p.write_text(content) 对应 Files.writeString(p, content) (Java 11+)。
- p.read_text() 对应 Files.readString§ (Java 11+) 或 new String(Files.readAllBytes§)。
- p.write_bytes(data) 对应 Files.write(p, data)。
- p.read_bytes() 对应 Files.readAllBytes§。
4.7 目录迭代:.iterdir(), .glob()
- .iterdir(): 返回一个迭代器,用于遍历目录下的直接子项(文件和子目录)。
- .glob(pattern): 更强大,支持通配符模式匹配。
- *.txt: 匹配所有.txt文件。
- **/*.py: 递归匹配所有子目录下的.py文件。
这里还是要注意使用递归匹配的方法,一般在正式环境中目录的层级较深,而且文件较多,这种没有任何阻断和条件判断的递归始终存在隐患。
五、处理常见文件格式
💡 在Python中有很多模块用来处理常见的文件格式,方便我们进行处理,相比之下Java也是一样的。
5.1 CSV (逗号分隔值) 文件:csv 模块
首先你要了解什么是csv文件,CSV是一种简单的文本格式,用于存储表格数据。csv模块可以让你不必手动处理逗号、引号和换行符带来的麻烦。
csv
Name,Department,Salary
John Doe,Engineering,90000
Jane Smith,Marketing,110000
Peter Jones,Engineering,95000
1. 读取CSV:csv.reader
- csv.reader创建一个阅读器对象,它是一个迭代器。遍历这个迭代器,每一行都会被解析成一个字符串列表。
python
import csv
# 关键点: 打开文件时使用 newline='' 来避免空行问题
with open('data.csv', 'r', newline='', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader) # 读取并跳过头部
print(f"头部: {header}")
for row in reader:
# row 是一个列表, e.g., ['John Doe', 'Engineering', '90000']
print(f"{row[0]} in {row[1]} earns ${row[2]}")
为什么说 newline=' ' 可以避免空行,这是因为
不同系统对于换行符的处理是不一样的,window是\r\n,其余大多数都是\n,如果让Python在读取文件时自动处理,那可能就导致csv的内容识别错误了,这时候使用 newline=' ' 就是把换行符交给csv模块来处理,它们的逻辑可以自动识别。
- Java中读取csv:使用Apache Commons CSV库。
java
// Java Example with Apache Commons CSV
Reader in = new FileReader("data.csv");
Iterable<CSVRecord> records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(in);
for (CSVRecord record : records) {
String name = record.get("Name");
String salary = record.get("Salary");
System.out.println(name + " earns $" + salary);
}
2. 写入CSV:csv.writer
- csv.writer创建一个写入器对象。使用.
writerow()写入单行(接收一个列表),或.writerows()写入多行(接收一个列表的列表)。
python
import csv
data_to_write = [
['Name', 'Department', 'Salary'],
['Alice Brown', 'HR', 75000],
['Bob Johnson', 'Sales', 120000]
]
with open('output.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(data_to_write)
# 或者逐行写入:
# writer.writerow(['Charlie Davis', 'Support', 60000])
print("'output.csv' has been created.")
- Java中的写入
java
// Java Example with Apache Commons CSV
Writer out = new FileWriter("output.csv");
try (CSVPrinter printer = new CSVPrinter(out, CSVFormat.DEFAULT)) {
printer.printRecord("Name", "Department", "Salary");
printer.printRecord("Alice Brown", "HR", 75000);
}
3. 字典形式读写:csv.DictReader, csv.DictWriter
- 这是处理带标题行的CSV文件的更推荐、更健壮的方式。DictReader将每一行读取为一个字典,键是列标题。DictWriter则从字典写入行。表格的行说白了就是一个对象,只是展开了而已。
读取:
python
import csv
with open('data.csv', 'r', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
# row 是一个字典, e.g., {'Name': 'John Doe', 'Department': 'Engineering', 'Salary': '90000'}
print(f"{row['Name']} earns ${row['Salary']}")
写入:
python
import csv
data_to_write = [
{'Name': 'Alice Brown', 'Department': 'HR', 'Salary': 75000},
{'Name': 'Bob Johnson', 'Department': 'Sales', 'Salary': 120000}
]
# 必须指定列的顺序
fieldnames = ['Name', 'Department', 'Salary']
with open('output_dict.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader() # 写入标题行
writer.writerows(data_to_write)
print("'output_dict.csv' has been created.")
5.2 JSON (JavaScript对象表示法) 文件:json 模块
💡 json模块的核心是dump/load(处理文件)和dumps/loads(处理字符串)这四对函数。
💡 为什么要说json,一般和第三方对接数据首选肯定是json,本系统的不同模块对接也可以用json。
1. 将Python对象写入JSON文件:json.dump()
- 将Python对象(主要是字典和列表)序列化为JSON格式并写入到一个文件流中。
- indent=4:保持字符缩进为4,看起来更加美观。
- ensure_ascii=False:允许非ASCII字符(如中文、泰文等)直接显示
python
import json
user_data = {
"name": "Guido van Rossum",
"id": 1001,
"is_active": True,
"roles": ["BDFL", "Python Creator"],
"settings": None
}
with open('user.json', 'w', encoding='utf-8') as f:
# indent=4 ทำให้ไฟล์ JSON มีการจัดรูปแบบที่สวยงาม อ่านง่าย
json.dump(user_data, f, indent=4, ensure_ascii=False)
print("'user.json' has been created.")
2. 从JSON文件读取到Python对象:json.load()
- 从一个文件流中读取JSON数据,并将其反序列化为Python对象。JSON对象变为Python字典,数组变为列表,字符串、数字、布尔值和null也都有对应的Python类型。
python
import json
with open('user.json', 'r', encoding='utf-8') as f:
data = json.load(f)
print(type(data)) # <class 'dict'>
print(data['name']) # Guido van Rossum
print(data['roles']) # ['BDFL', 'Python Creator']
3. 字符串与Python对象的转换:json.dumps(), json.loads()
- dumps代表 "dump string" ,将Python对象转换成JSON格式的字符串。loads代表 "load string",将JSON格式的字符串解析成Python对象。这在处理网络API响应时非常常用。
python
import json
# 1. Python object to JSON string (dumps)
py_object = {'city': 'Amsterdam', 'country': 'Netherlands'}
json_string = json.dumps(py_object, indent=2)
print("--- JSON String ---")
print(json_string)
# 2. JSON string to Python object (loads)
api_response_string = '{"product_id": 90210, "in_stock": true}'
py_dict = json.loads(api_response_string)
print("\n--- Python Dictionary ---")
print(py_dict)
print(f"Product ID: {py_dict['product_id']}")
六、序列化与对象持久化
💡 就是Java的Serializable接口,效果一样的,这是一个必备的实现,我们一直在使用,但大多数场景都是用来网络传输,只有在某些特定的时候需要将对象持久化到用户的本地设备。但是,没有安全保障,所以一般肯定不会使用原生的,都会使用专业的第三方。
6.1 pickle 模块:Python专用二进制协议
与JSON或CSV不同,pickle不是一种人类可读的文本格式。它是一种二进制协议,专门设计用来表示Python对象。它的强大之处在于,它几乎可以序列化任何Python对象,包括自定义类的实例、函数,甚至是递归数据结构,而这些是JSON无法处理的。它存储的是对象的"快照",保留了对象的类型和内部状态。
6.2 序列化 (Pickling):pickle.dump() - 将任意Python对象存入文件
- pickle.dump(obj, file)函数接收两个参数:要序列化的对象obj,以及一个以二进制写入模式 ('wb') 打开的文件对象file。因为pickle生成的是字节,所以必须使用二进制模式。
python
import pickle
# 定义一个自定义类
class Player:
def __init__(self, name, level, inventory):
self.name = name
self.level = level
self.inventory = inventory
def display(self):
print(f"Player: {self.name}, Level: {self.level}, Inventory: {self.inventory}")
# 创建一个对象实例
player1 = Player('Gandalf', 99, ['Staff of Power', 'Glamdring', 'Lembas Bread'])
# 将对象序列化到文件
# 注意:必须使用 'wb' (write binary) 模式
with open('player.pkl', 'wb') as f:
pickle.dump(player1, f)
print("对象已成功序列化到 'player.pkl'")
6.3 反序列化 (Unpickling):pickle.load() - 从文件恢复Python对象
- pickle.load(file)从一个以二进制读取模式 ('rb') 打开的文件对象file中读取字节流,并将其重建为一个完整的Python对象。
python
import pickle
# 确保 Player 类的定义在当前作用域中是可用的
class Player:
def __init__(self, name, level, inventory):
self.name = name
self.level = level
self.inventory = inventory
def display(self):
print(f"Player: {self.name}, Level: {self.level}, Inventory: {self.inventory}")
# 从文件反序列化对象
# 注意:必须使用 'rb' (read binary) 模式
with open('player.pkl', 'rb') as f:
restored_player = pickle.load(f)
print("对象已从 'player.pkl' 恢复")
print(f"恢复对象的类型: {type(restored_player)}")
# 验证对象是否完整恢复,包括它的方法
restored_player.display()
# 输出: Player: Gandalf, Level: 99, Inventory: ['Staff of Power', 'Glamdring', 'Lembas Bread']
6.4 pickle 的优缺点 (速度快,但存在安全风险且不跨语言)
优点:
- 功能强大:可以处理绝大多数Python对象,包括复杂的自定义对象、函数和类,保留其完整的内部状态。
- 简单易用:API非常简洁,只有dump()和load()两个核心函数。
- 性能相对较高:作为一种紧凑的二进制格式,其序列化和反序列化的速度通常比文本格式的JSON快。
缺点:
- 安全风险 (最重要):绝对不要从不可信或未经验证的来源反序列化数据。pickle的字节流可以被恶意构造,使其在反序列化时执行任意代码,这可能导致远程代码执行漏洞。这与Java Serializable的风险完全相同。
- 不跨语言:pickle是Python独有的格式。一个由Python程序创建的.pkl文件对于Java、C++、JavaScript等其他语言来说是无法解析的。
- 版本兼容性问题:用较新版本的Python创建的pickle文件可能无法被较旧版本的Python正确读取。虽然pickle协议有多个版本,但这仍然是一个潜在的维护问题。
七、高级主题与最佳实践
这一部分我就不做过多解释了,都是基本的编码要求,重点说一下
tempfile和内存中的文件两个知识点。
7.1 字符编码 (Character Encoding)
1. 编码的重要性 (如 utf-8, gbk)
2. 在 open() 中指定 encoding 参数避免乱码
7.2 异常处理 (Error Handling)
1. FileNotFoundError:文件不存在
2. PermissionError:权限不足
3. IsADirectoryError: 尝试像文件一样打开目录
4. 使用 try...except 块处理文件相关异常
7.3 文件缓冲 (File Buffering)
1. 概念理解:数据并非立即写入磁盘
2. file.flush():手动将缓冲区内容写入磁盘
7.4 临时文件和目录:tempfile 模块
- tempfile模块是Python标准库中用于创建临时文件和目录的安全、跨平台的方式。它能
自动处理命名冲突和权限问题,并在不再需要时(通常)自动清理这些文件,是处理临时数据的最佳选择。
python
import tempfile
# 创建一个在关闭时会自动删除的临时文件
with tempfile.TemporaryFile(mode='w+', encoding='utf-8') as tf:
tf.write("这是一些临时数据。\n")
tf.seek(0) # 回到文件开头
print(f"从临时文件中读出: {tf.read().strip()}")
# with 块结束后,文件已从磁盘上消失
# 创建一个有名字的临时文件,在with结束后不会自动删除
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False, suffix='.tmp') as ntf:
print(f"创建了带名字的临时文件: {ntf.name}")
ntf.write("持久化的临时数据")
# ntf.name 可以在后续代码中使用,但需要你手动删除
# import os; os.remove(ntf.name)
7.5 内存中的文件:io.StringIO 和 io.BytesIO
-
io.StringIO和io.BytesIO让你能够创建"内存中的文件"。它们提供了与真实文件对象相同的接口(如.read(), .write(), .seek()),但所有操作都在内存中进行,不涉及任何磁盘I/O。StringIO处理文本(字符串),而BytesIO处理二进制数据(字节)。这在当你需要与一个只接受文件对象的API交互,但你的数据却在字符串或字节序列中时非常有用。
-
这直接对应于Java的StringReader/StringWriter和ByteArrayInputStream/ByteArrayOutputStream。
python
import io
import csv
# 假设一个函数需要一个文件对象来写入CSV
def write_csv_to_file_object(file_obj, data):
writer = csv.writer(file_obj)
writer.writerows(data)
data = [['Name', 'Age'], ['Alice', 30], ['Bob', 25]]
# 我们不想创建真实文件,而是想直接得到CSV格式的字符串
string_buffer = io.StringIO()
write_csv_to_file_object(string_buffer, data)
# 从缓冲区获取完整的字符串值
csv_string = string_buffer.getvalue()
print("---内存中生成的CSV字符串---")
print(csv_string)
python
import io
from PIL import Image # 需要安装Pillow库: pip install Pillow
# 创建一个简单的黑色图片
img = Image.new('RGB', (60, 30), color = 'black')
# 假设一个API需要将图片"保存"到一个二进制文件流
byte_buffer = io.BytesIO()
img.save(byte_buffer, format='PNG')
# 获取图片的二进制数据,可以用于网络传输等
image_bytes = byte_buffer.getvalue()
print(f"内存中生成的PNG图片字节数: {len(image_bytes)}")
总结
文件就到这里了,后面可以根据自己的需求一边写一边学,这些都是基础的,接下来我们要学习的是:线程、进程、队列。