六、文件 I/O 与序列化
阶段定位 :文件操作和序列化是任何应用程序都无法回避的基础能力。Python 提供了丰富的工具链,从底层的 os 到现代的 pathlib,从 json 到 pickle。本阶段从工程实践出发,讲解如何安全、高效地处理文件和数据持久化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌─────────────────────────────────────────────────────────────────┐ │ 文件 I/O 与序列化知识图谱 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 文件读写 ──→ open() / with / 文本模式 / 二进制模式 │ │ │ │ │ 2. 上下文管理器 ─→ with / @contextmanager / ExitStack │ │ │ │ │ 3. 文件系统 ───→ pathlib / os / os.path / shutil │ │ │ │ │ 4. 序列化 ─────→ JSON / Pickle / CSV / XML │ │ │ │ │ 5. 压缩归档 ───→ zipfile / tarfile / gzip │ │ │ │ │ 6. 内存流 ────→ io.StringIO / io.BytesIO │ │ │ │ │ 7. 工程实践 ───→ 大文件 / 临时文件 / 文件锁 / 原子写入 / sqlite3 │ │ │ └─────────────────────────────────────────────────────────────────┘
1. 文件读写基础 1.1 基本文件操作 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 with open ("data.txt" , "r" , encoding="utf-8" ) as f: content = f.read() print (content) with open ("data.txt" , "r" , encoding="utf-8" ) as f: for line in f: print (line.strip()) with open ("output.txt" , "w" , encoding="utf-8" ) as f: f.write("第一行\n" ) f.write("第二行\n" ) f.writelines(["第三行\n" , "第四行\n" ]) with open ("output.txt" , "a" , encoding="utf-8" ) as f: f.write("追加内容\n" )
文件打开模式组合 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌─────────────────────────────────────────────────────────┐ │ 文件打开模式组合 │ ├──────────────┬──────────────┬──────────────────────────┤ │ 模式 │ 含义 │ 特性 │ ├──────────────┼──────────────┼──────────────────────────┤ │ 'r' │ 只读 │ 默认模式,文件不存在报错 │ │ 'w' │ 写入(覆盖) │ 文件不存在创建,存在清空 │ │ 'a' │ 追加 │ 文件不存在创建,指针在末尾 │ │ 'x' │ 独占创建 │ 文件已存在则报错 │ │ 'b' │ 二进制 │ 与其他模式组合使用 │ │ '+' │ 读写 │ 与其他模式组合使用 │ │ 'r+' │ 读写(不清空) │ 文件必须存在,指针在开头 │ │ 'w+' │ 读写(覆盖) │ 文件不存在创建,存在清空 │ │ 'a+' │ 读写(追加) │ 文件不存在创建,指针在末尾 │ └──────────────┴──────────────┴──────────────────────────┘
1.2 文件指针与随机访问 1 2 3 4 5 6 7 8 9 10 11 12 13 with open ("data.txt" , "r+" , encoding="utf-8" ) as f: chunk = f.read(10 ) print (f"当前位置: {f.tell()} " ) f.seek(0 ) f.seek(20 ) f.seek(5 , 1 )
seek() 的 whence 参数 :
参数
含义
0(默认)
从文件开头计算偏移量
1
从当前位置计算偏移量(仅二进制模式)
2
从文件末尾计算偏移量(仅二进制模式)
1.3 二进制文件读写 1 2 3 4 5 6 7 8 9 10 11 12 13 14 with open ("image.jpg" , "rb" ) as f: header = f.read(4 ) print (header) def copy_file (src, dst, chunk_size=8192 ): """高效复制文件,使用分块读取避免大文件占用过多内存。""" with open (src, "rb" ) as fsrc, open (dst, "wb" ) as fdst: while True : chunk = fsrc.read(chunk_size) if not chunk: break fdst.write(chunk)
工程建议 :复制文件时使用 shutil.copy2() 替代手动实现,它更高效且保留元数据。
2. with 语句与上下文管理器 2.1 with 的本质 1 2 3 4 5 6 7 8 9 10 11 12 13 with open ("file.txt" , "r" ) as f: data = f.read() f = open ("file.txt" , "r" ) try : data = f.read() finally : f.close()
上下文管理器协议 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌─────────────────────────────────────────────────────────┐ │ 上下文管理器协议 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 一个对象只要实现了 __enter__ 和 __exit__ 方法, │ │ 就可以在 with 语句中使用。 │ │ │ │ class Resource: │ │ def __enter__(self): │ │ # 资源获取逻辑 │ │ return self │ │ │ │ def __exit__(self, exc_type, exc_val, exc_tb): │ │ # 资源释放逻辑(无论是否发生异常都会执行) │ │ return False # 返回 True 会抑制异常 │ │ │ └─────────────────────────────────────────────────────────┘
2.2 同时打开多个文件 1 2 3 4 5 6 7 8 9 10 11 12 13 with open ("input.txt" , "r" , encoding="utf-8" ) as fin, \ open ("output.txt" , "w" , encoding="utf-8" ) as fout: for line in fin: processed = line.strip().upper() fout.write(processed + "\n" ) with ( open ("input.txt" , "r" , encoding="utf-8" ) as fin, open ("output.txt" , "w" , encoding="utf-8" ) as fout, ): fout.write(fin.read())
2.3 自定义上下文管理器 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 from contextlib import contextmanager@contextmanager def managed_file (filename, mode="r" ): """自定义文件上下文管理器。""" f = open (filename, mode) try : print (f"打开文件: {filename} " ) yield f finally : f.close() print (f"关闭文件: {filename} " ) with managed_file("test.txt" , "w" ) as f: f.write("Hello" ) from contextlib import suppress, redirect_stdoutwith suppress(FileNotFoundError): os.remove("nonexistent.txt" ) with open ("log.txt" , "w" ) as f: with redirect_stdout(f): print ("这行会写入 log.txt" ) from contextlib import ExitStackwith ExitStack() as stack: files = [ stack.enter_context(open (f"file{i} .txt" )) for i in range (10 ) ]
3. 文件系统操作:os、os.path、pathlib 3.1 pathlib(推荐,3.4+) pathlib 是面向对象的文件路径操作库,取代了传统的 os.path。
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 from pathlib import Pathp = Path("/home/user/documents/file.txt" ) print (p.name) print (p.stem) print (p.suffix) print (p.suffixes) print (p.parent) print (p.parents[0 ]) print (p.parents[1 ]) print (p.parts) print (p.anchor) base = Path("/home/user" ) doc = base / "documents" / "file.txt" print (doc) print (Path("relative.txt" ).is_absolute()) print (Path("relative.txt" ).resolve()) print (p.exists())print (p.is_file())print (p.is_dir())
3.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 from pathlib import Pathbase = Path("my_project" ) base.mkdir(parents=True , exist_ok=True ) (base / "src" ).mkdir(exist_ok=True ) (base / "tests" ).mkdir(exist_ok=True ) config = base / "config.json" config.write_text('{"debug": true}' , encoding="utf-8" ) text = config.read_text(encoding="utf-8" ) print (text)target = base / "config_backup.json" config.rename(target) target.replace(config) config.copy(target) config.unlink() target.unlink(missing_ok=True ) for item in base.iterdir(): print (item.name) for py_file in base.rglob("*.py" ): print (py_file) for txt in base.glob("*.txt" ): print (txt)
3.3 os 与 os.path 对照表
操作
os/os.path
pathlib(推荐)
路径拼接
os.path.join(a, b)
Path(a) / b
绝对路径
os.path.abspath(p)
Path(p).resolve()
存在检查
os.path.exists(p)
Path(p).exists()
是否为文件
os.path.isfile(p)
Path(p).is_file()
是否为目录
os.path.isdir(p)
Path(p).is_dir()
文件名
os.path.basename(p)
Path(p).name
目录名
os.path.dirname(p)
Path(p).parent
扩展名
os.path.splitext(p)
Path(p).suffix
创建目录
os.makedirs(p, exist_ok=True)
Path(p).mkdir(parents=True, exist_ok=True)
删除文件
os.remove(p)
Path(p).unlink()
删除目录
os.rmdir(p)
Path(p).rmdir()
重命名
os.rename(src, dst)
Path(src).rename(dst)
4. shutil:高级文件操作 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 import shutilfrom pathlib import Pathsrc = Path("source" ) dst = Path("destination" ) shutil.copy2("file.txt" , "backup/file.txt" ) shutil.copy("file.txt" , "backup/" ) shutil.copytree("project" , "project_backup" ) shutil.copytree("project" , "project_backup" , dirs_exist_ok=True ) shutil.move("old_dir" , "new_dir" ) shutil.rmtree("temp_dir" ) total, used, free = shutil.disk_usage("/" ) print (f"总空间: {total // (1024 **3 )} GB" )print (f"已用: {used // (1024 **3 )} GB" )print (f"可用: {free // (1024 **3 )} GB" )shutil.which("python" )
5. 序列化 5.1 JSON 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 import jsonfrom pathlib import Pathdata = { "name" : "Alice" , "age" : 30 , "skills" : ["Python" , "Go" , "Rust" ], "address" : { "city" : "Beijing" , "zipcode" : "100000" }, "is_active" : True , } json_str = json.dumps(data, ensure_ascii=False , indent=2 ) print (json_str)Path("data.json" ).write_text( json.dumps(data, ensure_ascii=False , indent=2 ), encoding="utf-8" ) with open ("data.json" , "w" , encoding="utf-8" ) as f: json.dump(data, f, ensure_ascii=False , indent=2 ) loaded = json.loads(json_str) print (loaded["name" ])with open ("data.json" , "r" , encoding="utf-8" ) as f: loaded = json.load(f)
JSON 参数速查 :
参数
说明
ensure_ascii=False
允许输出中文,不转义为 Unicode 编码
indent=2
格式化缩进
sort_keys=True
按键排序(提高可读性/可比较性)
separators=(',', ':')
紧凑输出(生产环境减小体积)
自定义 JSON 编码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from datetime import datetimeimport jsonclass CustomEncoder (json.JSONEncoder): """自定义 JSON 编码器。""" def default (self, obj ): if isinstance (obj, datetime): return obj.isoformat() if isinstance (obj, set ): return list (obj) return super ().default(obj) data = { "created_at" : datetime.now(), "tags" : {"python" , "json" }, } json_str = json.dumps(data, cls=CustomEncoder, ensure_ascii=False ) print (json_str)
5.2 Pickle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import pickledata = { "name" : "Alice" , "func" : lambda x: x * 2 , } with open ("data.pkl" , "wb" ) as f: pickle.dump(data, f) with open ("data.pkl" , "rb" ) as f: loaded = pickle.load(f) print (loaded["func" ](5 ))
JSON vs Pickle 对比 :
特性
JSON
Pickle
格式
文本,人类可读
二进制
跨语言
是
否(仅限 Python)
安全性
安全
不安全(不要加载不信任的数据)
对象支持
基础类型
几乎所有 Python 对象
速度
较慢
较快
适用场景
API 数据交换、配置文件
缓存、临时状态保存
安全警告 :pickle.load() 会执行任意 Python 代码,永远不要反序列化来自不可信来源的数据 。
5.3 CSV 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 import csvfrom pathlib import Pathusers = [ ["name" , "age" , "city" ], ["Alice" , 30 , "Beijing" ], ["Bob" , 25 , "Shanghai" ], ["Carol" , 35 , "Shenzhen" ], ] with open ("users.csv" , "w" , newline="" , encoding="utf-8" ) as f: writer = csv.writer(f) writer.writerows(users) with open ("users.csv" , "r" , encoding="utf-8" ) as f: reader = csv.reader(f) for row in reader: print (row) with open ("users.csv" , "r" , encoding="utf-8" ) as f: reader = csv.DictReader(f) for row in reader: print (f"{row['name' ]} 来自 {row['city' ]} " ) with open ("users.csv" , "w" , newline="" , encoding="utf-8" ) as f: fieldnames = ["name" , "age" , "city" ] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerow({"name" : "Alice" , "age" : 30 , "city" : "Beijing" })
5.4 XML 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 import xml.etree.ElementTree as ETxml_data = """ <library> <book id="1"> <title>Python编程</title> <author>张三</author> <price>59.00</price> </book> <book id="2"> <title>数据结构</title> <author>李四</author> <price>45.00</price> </book> </library> """ root = ET.fromstring(xml_data) for book in root.findall("book" ): book_id = book.get("id" ) title = book.find("title" ).text author = book.find("author" ).text price = float (book.find("price" ).text) print (f"[{book_id} ] {title} - {author} ({price} 元)" ) new_book = ET.SubElement(root, "book" , {"id" : "3" }) ET.SubElement(new_book, "title" ).text = "算法导论" ET.SubElement(new_book, "author" ).text = "王五" ET.SubElement(new_book, "price" ).text = "89.00" ET.indent(root, space=" " ) tree = ET.ElementTree(root) tree.write("library.xml" , encoding="utf-8" , xml_declaration=True )
6. 压缩与归档 6.1 zipfile 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 import zipfilefrom pathlib import Pathwith zipfile.ZipFile("archive.zip" , "w" , zipfile.ZIP_DEFLATED) as zf: zf.write("file1.txt" ) zf.write("file2.txt" , "renamed.txt" ) with zipfile.ZipFile("archive.zip" , "a" ) as zf: zf.write("file3.txt" ) with zipfile.ZipFile("archive.zip" , "r" ) as zf: print (zf.namelist()) with zf.open ("file1.txt" ) as f: print (f.read().decode("utf-8" )) zf.extractall("extracted/" ) zf.extract("file1.txt" , "extracted/" ) with zipfile.ZipFile("secret.zip" , "r" ) as zf: zf.extractall("extracted/" , pwd=b"password" )
6.2 tarfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import tarfilefrom pathlib import Pathwith tarfile.open ("archive.tar.gz" , "w:gz" ) as tf: tf.add("file1.txt" ) tf.add("dir/" , arcname="data/" ) with tarfile.open ("archive.tar.gz" , "r:gz" ) as tf: print (tf.getnames()) tf.extractall("extracted/" ) tf.extract("file1.txt" , "extracted/" )
6.3 gzip 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import gzipwith open ("data.txt" , "rb" ) as fsrc: with gzip.open ("data.txt.gz" , "wb" ) as fdst: fdst.write(fsrc.read()) with gzip.open ("data.txt.gz" , "wt" , encoding="utf-8" ) as f: f.write("Hello, World!" ) with gzip.open ("data.txt.gz" , "rt" , encoding="utf-8" ) as f: print (f.read())
7. 内存流:io.StringIO 与 io.BytesIO 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 from io import StringIO, BytesIOoutput = StringIO() output.write("第一行\n" ) output.write("第二行\n" ) print ("第三行" , file=output)content = output.getvalue() print (content)output.seek(0 ) for line in output: print (line.strip()) output.close() binary = BytesIO() binary.write(b"Hello" ) binary.write(b" World" ) binary.seek(0 ) print (binary.read()) def process_csv_in_memory (rows ): """在内存中生成 CSV,直接发送给 HTTP 响应。""" from io import StringIO import csv output = StringIO() writer = csv.writer(output) writer.writerows(rows) output.seek(0 ) return output.getvalue()
8. 文件 I/O 工程实践 8.1 大文件处理 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 from pathlib import Pathdef process_large_file (filepath, chunk_size=8192 ): """分块读取大文件,避免内存溢出。""" with open (filepath, "rb" ) as f: while True : chunk = f.read(chunk_size) if not chunk: break yield chunk with open ("huge.log" , "r" , encoding="utf-8" ) as f: for line in f: process_line(line) import mmapwith open ("huge.bin" , "r+b" ) as f: with mmap.mmap(f.fileno(), 0 ) as mm: print (mm[:100 ]) mm.seek(1000000 ) print (mm.read(100 ))
8.2 临时文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import tempfilefrom pathlib import Pathwith tempfile.NamedTemporaryFile(mode="w" , suffix=".txt" , delete=True ) as tmp: tmp.write("临时内容" ) tmp.flush() print (f"临时文件路径: {tmp.name} " ) with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "data.json" path.write_text("{}" ) print (f"临时目录: {tmpdir} " ) tmp = tempfile.NamedTemporaryFile(mode="w" , suffix=".txt" , delete=False ) tmp.write("保留的内容" ) tmp.close() print (f"保留的临时文件: {tmp.name} " )
8.3 文件锁(跨进程同步) 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 import fcntl def acquire_lock (filepath ): """获取文件锁(Linux/macOS)。""" with open (filepath, "w" ) as f: fcntl.flock(f, fcntl.LOCK_EX) try : yield f finally : fcntl.flock(f, fcntl.LOCK_UN) from filelock import FileLocklock = FileLock("data.lock" ) with lock: with open ("data.json" , "r+" ) as f: data = json.load(f) data["count" ] += 1 f.seek(0 ) json.dump(data, f) f.truncate()
8.4 原子写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pathlib import Pathimport tempfileimport osdef atomic_write (filepath, content ): """原子写入:要么完全成功,要么完全不写(避免写入中断导致文件损坏)。""" filepath = Path(filepath) tmp = tempfile.NamedTemporaryFile( mode="w" , dir =filepath.parent, suffix=".tmp" , delete=False , encoding="utf-8" ) try : tmp.write(content) tmp.close() os.replace(tmp.name, filepath) except Exception: os.unlink(tmp.name) raise atomic_write("config.json" , '{"key": "value"}' )
8.5 sqlite3:内置数据库 Python 自带 SQLite 数据库引擎,无需安装额外服务,轻量且功能完整。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 import sqlite3conn = sqlite3.connect("app.db" ) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, email TEXT UNIQUE ) """ )cur.execute( "INSERT INTO users (name, age, email) VALUES (?, ?, ?)" , ("Alice" , 30 , "alice@example.com" ) ) conn.commit() cur.execute("SELECT * FROM users WHERE age > ?" , (25 ,)) for row in cur.fetchall(): print (f"ID: {row[0 ]} , Name: {row[1 ]} , Age: {row[2 ]} " ) conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute("SELECT * FROM users LIMIT 1" ) row = cur.fetchone() print (row["name" ], row["email" ]) users = [ ("Bob" , 25 , "bob@example.com" ), ("Carol" , 35 , "carol@example.com" ), ] cur.executemany("INSERT INTO users (name, age, email) VALUES (?, ?, ?)" , users) conn.commit() try : cur.execute("UPDATE users SET age = ? WHERE name = ?" , (31 , "Alice" )) conn.commit() except Exception as e: conn.rollback() print (f"事务回滚: {e} " ) conn.close()
9. 工作实战场景 9.1 日志轮转系统 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 43 44 45 46 47 48 49 50 51 import loggingimport logging.handlersimport osfrom datetime import datetimedef setup_logging (app_name="app" , log_dir="logs" , max_size=10 *1024 *1024 , backup_count=5 ): """设置日志轮转系统。""" os.makedirs(log_dir, exist_ok=True ) formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d: %(message)s" , datefmt="%Y-%m-%d %H:%M:%S" ) console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(formatter) file_handler = logging.handlers.RotatingFileHandler( os.path.join(log_dir, f"{app_name} .log" ), maxBytes=max_size, backupCount=backup_count, encoding="utf-8" ) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) error_handler = logging.handlers.RotatingFileHandler( os.path.join(log_dir, f"{app_name} _error.log" ), maxBytes=max_size, backupCount=backup_count, encoding="utf-8" ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(formatter) logger = logging.getLogger(app_name) logger.setLevel(logging.DEBUG) logger.addHandler(console_handler) logger.addHandler(file_handler) logger.addHandler(error_handler) return logger logger = setup_logging("myapp" ) logger.info("应用启动" ) logger.error("数据库连接失败" )
9.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 43 44 45 46 47 import jsonimport timefrom pathlib import Pathclass ConfigManager : """配置管理:支持热更新。""" def __init__ (self, config_path ): self .config_path = Path(config_path) self ._config = {} self ._last_modified = 0 self .load() def load (self ): """加载配置文件。""" if self .config_path.exists(): with open (self .config_path, "r" , encoding="utf-8" ) as f: self ._config = json.load(f) self ._last_modified = self .config_path.stat().st_mtime def get (self, key, default=None ): """获取配置值(自动检测文件变化)。""" if self .config_path.exists(): current_modified = self .config_path.stat().st_mtime if current_modified > self ._last_modified: self .load() return self ._config.get(key, default) def watch (self, callback ): """监听配置文件变化。""" while True : if self .config_path.exists(): current_modified = self .config_path.stat().st_mtime if current_modified > self ._last_modified: self .load() callback(self ._config) time.sleep(1 ) config = ConfigManager("config.json" ) def on_config_change (new_config ): print (f"配置已更新: {new_config} " ) import threadingthreading.Thread(target=config.watch, args=(on_config_change,), daemon=True ).start()
9.3 文件上传与处理 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 from pathlib import Pathimport hashlibimport magicclass FileUploader : """文件上传处理器:验证、存储、去重。""" def __init__ (self, upload_dir="uploads" , max_size=10 *1024 *1024 ): self .upload_dir = Path(upload_dir) self .max_size = max_size self .allowed_types = {"image/jpeg" , "image/png" , "application/pdf" } self .upload_dir.mkdir(parents=True , exist_ok=True ) def _calculate_hash (self, file_path ): """计算文件 MD5 哈希值。""" hash_md5 = hashlib.md5() with open (file_path, "rb" ) as f: for chunk in iter (lambda : f.read(8192 ), b"" ): hash_md5.update(chunk) return hash_md5.hexdigest() def _validate_file (self, file_path ): """验证文件类型和大小。""" if file_path.stat().st_size > self .max_size: raise ValueError("文件过大" ) file_type = magic.from_file(str (file_path), mime=True ) if file_type not in self .allowed_types: raise ValueError(f"不支持的文件类型: {file_type} " ) return file_type def upload (self, source_path ): """上传文件:验证、哈希去重、存储。""" source = Path(source_path) file_type = self ._validate_file(source) file_hash = self ._calculate_hash(source) extension = source.suffix.lower() target_path = self .upload_dir / f"{file_hash} {extension} " if target_path.exists(): return str (target_path) import shutil shutil.copy2(source, target_path) target_path.chmod(0o644 ) return str (target_path) uploader = FileUploader() result = uploader.upload("photo.jpg" ) print (f"文件已上传到: {result} " )
9.4 数据导入导出工具 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import jsonimport csvfrom pathlib import Pathfrom datetime import datetimeclass DataExporter : """数据导入导出工具:支持 JSON/CSV 格式。""" @staticmethod def export_json (data, filepath, **kwargs ): """导出为 JSON。""" kwargs.setdefault("ensure_ascii" , False ) kwargs.setdefault("indent" , 2 ) with open (filepath, "w" , encoding="utf-8" ) as f: json.dump(data, f, **kwargs) print (f"数据已导出到: {filepath} " ) @staticmethod def import_json (filepath ): """从 JSON 导入。""" with open (filepath, "r" , encoding="utf-8" ) as f: return json.load(f) @staticmethod def export_csv (data, filepath, headers=None ): """导出为 CSV。""" with open (filepath, "w" , newline="" , encoding="utf-8" ) as f: writer = csv.writer(f) if headers: writer.writerow(headers) if isinstance (data, list ) and isinstance (data[0 ], dict ): if not headers: headers = data[0 ].keys() writer.writerow(headers) for row in data: writer.writerow([row.get(h) for h in headers]) else : writer.writerows(data) print (f"数据已导出到: {filepath} " ) @staticmethod def import_csv (filepath ): """从 CSV 导入(返回字典列表)。""" with open (filepath, "r" , encoding="utf-8" ) as f: reader = csv.DictReader(f) return list (reader) data = [ {"name" : "Alice" , "age" : 30 , "city" : "Beijing" }, {"name" : "Bob" , "age" : 25 , "city" : "Shanghai" }, ] exporter = DataExporter() exporter.export_json(data, "users.json" ) exporter.export_csv(data, "users.csv" ) loaded_data = exporter.import_json("users.json" ) csv_data = exporter.import_csv("users.csv" )
10. 常见面试题汇总 10.1 基础概念题 Q1:with 语句的作用是什么?它的原理是什么?
A :with 语句用于自动管理资源(文件、锁、数据库连接等),确保资源在使用完毕后被正确释放,即使发生异常也会释放。
原理:实现了上下文管理器协议,即对象必须实现 __enter__ 和 __exit__ 方法:
__enter__:进入 with 块时调用,返回资源对象
__exit__:退出 with 块时调用,负责资源清理
Q2:文件打开模式 r、w、a、x 的区别?
A :
r:只读(默认),文件不存在报错
w:写入(覆盖),文件不存在创建,存在清空
a:追加,文件不存在创建,指针在末尾
x:独占创建,文件已存在则报错
Q3:pathlib 相比 os.path 有什么优势?
A :
面向对象 :路径是对象而非字符串,支持方法调用
操作符重载 :使用 / 进行路径拼接,更直观
链式调用 :Path("a") / "b" / "c" 可以连续操作
统一接口 :文件操作(read_text/write_text)直接在路径对象上调用
跨平台 :自动处理不同操作系统的路径分隔符
Q4:JSON 和 Pickle 的区别?
A :
特性
JSON
Pickle
格式
文本,人类可读
二进制
跨语言
是
否(仅限 Python)
安全性
安全
不安全(不要加载不信任的数据)
对象支持
基础类型
几乎所有 Python 对象
Q5:什么是原子写入?为什么需要原子写入?
A :原子写入是指文件写入操作要么完全成功,要么完全不写。使用临时文件写入后再原子替换的方式实现。
原因:避免写入过程中发生异常(如断电、磁盘满)导致文件损坏或内容不完整。
10.2 实战应用题 Q6:如何处理大文件(GB 级别)?
A :
逐行读取 :for line in f:(最省内存)
分块读取 :f.read(chunk_size),每次读取固定大小
内存映射 :mmap 模块,像操作内存一样操作文件
生成器 :使用 yield 分块处理,避免一次性加载
Q7:如何实现文件上传的去重功能?
A :
计算文件的哈希值(MD5/SHA256)
使用哈希值作为文件名存储
上传前检查哈希值是否已存在
如果已存在,直接返回已有路径
Q8:如何安全地处理用户上传的文件?
A :
验证文件类型 :检查 MIME 类型和文件头
限制文件大小 :防止大文件攻击
重命名文件 :使用哈希值或随机字符串,避免路径遍历攻击
存储在非 Web 可访问目录 :或使用安全的文件服务
扫描病毒 :使用 ClamAV 等工具
设置合理的文件权限 :避免执行权限
Q9:如何实现配置文件的热更新?
A :
定期检查配置文件的修改时间
如果文件被修改,重新加载配置
使用线程或定时器后台监听
提供回调函数处理配置变化
Q10:sqlite3 的适用场景是什么?
A :
原型开发、本地缓存、嵌入式应用、数据分析
优势:零配置、单文件、标准库自带、支持 SQL
不适用:高并发、大规模数据、多进程写入
10.3 代码分析题 Q11:分析以下代码的问题:
1 2 3 f = open ("data.txt" , "w" ) f.write("hello" )
A :没有关闭文件,可能导致:
数据未立即写入磁盘(缓冲问题)
文件句柄泄漏
程序退出时才能释放资源
应使用 with 语句:
1 2 with open ("data.txt" , "w" ) as f: f.write("hello" )
Q12:分析以下代码的问题:
1 2 3 4 5 import jsondata = {"name" : "张三" , "age" : 30 } with open ("data.json" , "w" ) as f: json.dump(data, f)
A :中文会被转义为 Unicode 编码(如 \u5f20\u4e09),可读性差。应添加 ensure_ascii=False:
1 json.dump(data, f, ensure_ascii=False , indent=2 )
Q13:以下代码能否正确读取大文件?为什么?
1 2 3 with open ("huge_file.txt" , "r" ) as f: content = f.read() print (content)
A :不能。f.read() 会一次性读取整个文件到内存,如果文件过大(如 GB 级别),会导致内存溢出。应使用逐行读取或分块读取:
1 2 3 with open ("huge_file.txt" , "r" ) as f: for line in f: process(line)
11. 实战项目:文件管理工具 11.1 项目概述 项目目标 :开发一个命令行文件管理工具 filetool,支持文件压缩、解压、搜索、批量重命名等功能。
功能需求 :
filetool compress <dir> - 压缩目录
filetool extract <archive> - 解压归档
filetool search <pattern> - 搜索文件
filetool rename <pattern> <replace> - 批量重命名
filetool hash <file> - 计算文件哈希
11.2 项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 filetool/ ├── README.md ├── pyproject.toml ├── src/ │ └── filetool/ │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── commands/ │ │ ├── __init__.py │ │ ├── compress.py │ │ ├── extract.py │ │ ├── search.py │ │ ├── rename.py │ │ └── hash.py │ └── utils/ │ ├── __init__.py │ └── helpers.py └── tests/ ├── __init__.py └── test_filetool.py
11.3 核心代码 pyproject.toml :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [build-system] requires = ["setuptools>=61.0" , "wheel" ]build-backend = "setuptools.build_meta" [project] name = "filetool" version = "1.0.0" description = "A command-line file management tool" readme = "README.md" license = {text = "MIT" }authors = [ {name = "Your Name" , email = "you@example.com" } ] requires-python = ">=3.9" dependencies = [ "click>=8.0" , ] [project.scripts] filetool = "filetool.cli:main" [tool.setuptools.packages.find] where = ["src" ]
cli.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import clickimport importlibimport os@click.group() def main (): """FileTool - 文件管理工具""" pass def _load_commands (): commands_dir = os.path.join(os.path.dirname(__file__), "commands" ) for filename in os.listdir(commands_dir): if filename.endswith(".py" ) and not filename.startswith("_" ): module_name = filename[:-3 ] module = importlib.import_module(f"filetool.commands.{module_name} " ) if hasattr (module, "command" ): main.add_command(module.command) _load_commands()
commands/compress.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import clickimport tarfileimport os@click.command("compress" ) @click.argument("source" ) @click.option("-o" , "--output" , help ="输出文件名" ) @click.option("-f" , "--format" , default="gz" , type =click.Choice(["gz" , "bz2" , "xz" ] ) ) def command (source, output, format ): """压缩目录或文件。""" if output is None : output = f"{os.path.basename(source)} .tar.{format } " mode = f"w:{format } " with tarfile.open (output, mode) as tf: tf.add(source, arcname=os.path.basename(source)) click.echo(f"压缩完成: {output} " )
commands/search.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import clickimport osimport fnmatch@click.command("search" ) @click.argument("pattern" ) @click.option("-d" , "--directory" , default="." , help ="搜索目录" ) def command (pattern, directory ): """搜索匹配模式的文件。""" matches = [] for root, dirs, files in os.walk(directory): for filename in files: if fnmatch.fnmatch(filename, pattern): matches.append(os.path.join(root, filename)) if matches: click.echo(f"找到 {len (matches)} 个文件:" ) for filepath in matches: click.echo(f" {filepath} " ) else : click.echo("未找到匹配的文件" )
commands/rename.py :
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 import clickimport osimport re@click.command("rename" ) @click.argument("pattern" ) @click.argument("replace" ) @click.option("-d" , "--directory" , default="." , help ="操作目录" ) @click.option("-n" , "--dry-run" , is_flag=True , help ="模拟运行,不实际修改" ) def command (pattern, replace, directory, dry_run ): """批量重命名文件。""" count = 0 for filename in os.listdir(directory): new_name = re.sub(pattern, replace, filename) if new_name != filename: old_path = os.path.join(directory, filename) new_path = os.path.join(directory, new_name) if dry_run: click.echo(f"[模拟] {filename} -> {new_name} " ) else : os.rename(old_path, new_path) click.echo(f"{filename} -> {new_name} " ) count += 1 click.echo(f"共重命名 {count} 个文件" )
commands/hash.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import clickimport hashlibimport os@click.command("hash" ) @click.argument("file" ) @click.option("-a" , "--algorithm" , default="md5" , type =click.Choice(["md5" , "sha256" ] ) ) def command (file, algorithm ): """计算文件哈希值。""" hash_obj = hashlib.new(algorithm) with open (file, "rb" ) as f: for chunk in iter (lambda : f.read(8192 ), b"" ): hash_obj.update(chunk) click.echo(f"{algorithm.upper()} : {hash_obj.hexdigest()} " )
utils/helpers.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import osdef validate_path (path ): """验证路径是否存在。""" if not os.path.exists(path): raise FileNotFoundError(f"路径不存在: {path} " ) return path def get_file_size (path ): """获取文件大小(人类可读格式)。""" size = os.path.getsize(path) for unit in ["B" , "KB" , "MB" , "GB" ]: if size < 1024 : return f"{size:.2 f} {unit} " size /= 1024 return f"{size:.2 f} TB"
11.4 项目运行 1 2 3 4 5 6 7 8 9 10 11 12 pip install -e . filetool compress project/ filetool extract archive.tar.gz filetool search "*.py" filetool rename "(\d+)-" "" filetool hash data.zip python -m filetool hash data.zip
11.5 项目亮点
功能丰富 :覆盖文件压缩、解压、搜索、重命名、哈希计算等常用操作
模块化设计 :每个命令独立模块,易于扩展
安全可靠 :批量操作支持 dry-run 模式,避免误操作
用户友好 :使用 click 库提供丰富的命令行参数和帮助信息
附录:第六阶段自检清单 在继续学习第七阶段之前,请确保你能:
工程师寄语 :文件 I/O 是最容易出问题的领域——编码错误、路径分隔符、并发写入、磁盘满……永远假设文件操作会失败,并做好错误处理。pathlib 让路径操作从”字符串拼接”变成了”对象操作”,这是 Python 3 带来的最大生产力提升之一。