12 - 文件操作
跟文件打交道是写程序绕不开的事。读配置、写日志、处理数据… 这章把 Python 的文件操作讲清楚。
读写文本文件
写文件
# 写入文件(覆盖原有内容)withopen("hello.txt","w",encoding="utf-8")asf:f.write("你好,世界\n")f.write("第二行\n")几个要点:
"w"是写入模式(write),会覆盖已有内容encoding="utf-8"指定编码,防止中文乱码。强烈建议每次都写这个with语句会自动帮你关闭文件,不用手动f.close()
读文件
# 读取整个文件withopen("hello.txt","r",encoding="utf-8")asf:content=f.read()print(content)# 按行读取(更省内存,大文件推荐)withopen("hello.txt","r",encoding="utf-8")asf:forlineinf:print(line.strip())# strip() 去掉行尾的换行符# 读取所有行到列表withopen("hello.txt","r",encoding="utf-8")asf:lines=f.readlines()print(lines)# ['你好,世界\n', '第二行\n']追加写入
# "a" 是追加模式(append),不会覆盖原有内容withopen("hello.txt","a",encoding="utf-8")asf:f.write("这是追加的内容\n")打开模式一览
| 模式 | 说明 |
|---|---|
"r" | 读取(默认),文件不存在会报错 |
"w" | 写入,文件不存在会创建,存在会覆盖 |
"a" | 追加,文件不存在会创建 |
"x" | 创建,文件已存在会报错 |
"b" | 二进制模式(跟上面组合,如"rb"、"wb") |
"+" | 读写模式(如"r+") |
with 语句的重要性
你可能会看到有人这样写:
f=open("hello.txt","r")content=f.read()f.close()这能用,但有个问题:如果f.read()出了异常,f.close()就不会被执行,文件就一直开着。虽然 Python 退出时会自动关闭,但这不是好习惯。
with语句(上下文管理器)保证不管发生什么,文件最终都会被关闭:
withopen("hello.txt","r")asf:content=f.read()# 不管这里有没有异常,f 都会被关闭永远用with操作文件,别犹豫。
路径处理
用 pathlib(推荐)
Python 3.4 引入的pathlib模块,比老式的os.path好用太多了:
frompathlibimportPath# 创建路径对象p=Path("data/output/result.txt")# 常用属性print(p.name)# result.txt(文件名)print(p.stem)# result(文件名不含后缀)print(p.suffix)# .txt(后缀)print(p.parent)# data/output(父目录)print(p.parts)# ('data', 'output', 'result.txt')# 拼接路径(用 / 运算符,非常直观)base=Path("data")output=base/"output"/"result.txt"print(output)# data/output/result.txt文件是否存在
p=Path("hello.txt")print(p.exists())# True/Falseprint(p.is_file())# 是不是文件print(p.is_dir())# 是不是目录创建目录
# 创建目录(父目录不存在会自动创建)Path("data/output").mkdir(parents=True,exist_ok=True)遍历目录
# 列出当前目录下的所有文件forfinPath(".").iterdir():print(f)# 用 glob 模式匹配forfinPath(".").glob("*.py"):print(f)# 递归查找forfinPath(".").rglob("*.py"):print(f)读取和写入(pathlib 的简写)
p=Path("hello.txt")# 一行读取content=p.read_text(encoding="utf-8")# 一行写入p.write_text("你好",encoding="utf-8")# 二进制读写data=p.read_bytes()p.write_bytes(b"\x00\x01\x02")小文件用这种写法特别方便,省了 with 语句。大文件还是老老实实用open()逐行读。
处理 CSV 文件
CSV 就是逗号分隔的文本文件,Excel 能打开那种。
用标准库的 csv 模块
importcsv# 写 CSVwithopen("data.csv","w",newline="",encoding="utf-8")asf:writer=csv.writer(f)writer.writerow(["姓名","年龄","城市"])# 写表头writer.writerow(["小明",25,"北京"])writer.writerow(["小红",22,"上海"])# 读 CSVwithopen("data.csv","r",encoding="utf-8")asf:reader=csv.reader(f)header=next(reader)# 跳过表头forrowinreader:print(f"{row[0]},{row[1]}岁,{row[2]}")# 用 DictReader 更方便(直接用列名访问)withopen("data.csv","r",encoding="utf-8")asf:reader=csv.DictReader(f)forrowinreader:print(f"{row['姓名']},{row['年龄']}岁")newline=""是为了防止 Windows 下出现空行。
用 pandas(更强大)
如果数据量大或者需要复杂操作,pandas 更合适:
# 先安装# uv add pandasimportpandasaspd# 读df=pd.read_csv("data.csv")print(df)# 写df.to_csv("output.csv",index=False)处理 JSON 文件
JSON 是最常用的数据交换格式,跟 Python 字典长得很像。
importjson# Python 字典转 JSON 字符串data={"name":"小明","age":25,"hobbies":["读书","游泳"]}# 转为 JSON 字符串json_str=json.dumps(data,ensure_ascii=False,indent=2)print(json_str)# JSON 字符串转 Python 字典parsed=json.loads(json_str)print(parsed["name"])# 小明# 写入文件withopen("data.json","w",encoding="utf-8")asf:json.dump(data,f,ensure_ascii=False,indent=2)# 从文件读取withopen("data.json","r",encoding="utf-8")asf:data=json.load(f)注意ensure_ascii=False,不加的话中文会被转成\uXXXX形式。indent=2让输出有缩进,好看一些。
JSON 和 Python 的类型对应
| Python | JSON |
|---|---|
| dict | object{} |
| list/tuple | array[] |
| str | string"" |
| int/float | number |
| True/False | true/false |
| None | null |
处理二进制文件
图片、音频、视频这些不是文本,需要用二进制模式:
# 复制文件withopen("photo.jpg","rb")assrc:data=src.read()withopen("photo_copy.jpg","wb")asdst:dst.write(data)# 也可以用 shutil 一行搞定importshutil shutil.copy2("photo.jpg","photo_copy.jpg")文件操作的常见模式
读取配置文件
importjsonfrompathlibimportPathdefload_config(path="config.json"):p=Path(path)ifp.exists():returnjson.loads(p.read_text(encoding="utf-8"))return{}# 文件不存在返回空配置defsave_config(config,path="config.json"):Path(path).write_text(json.dumps(config,ensure_ascii=False,indent=2),encoding="utf-8")日志记录
fromdatetimeimportdatetimefrompathlibimportPathdeflog(message,logfile="app.log"):timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")withopen(logfile,"a",encoding="utf-8")asf:f.write(f"[{timestamp}]{message}\n")log("程序启动")log("用户登录")log("程序结束")当然实际项目中会用logging标准库,比手写方便得多:
importlogging logging.basicConfig(filename="app.log",level=logging.INFO,format="%(asctime)s %(levelname)s %(message)s")logging.info("程序启动")logging.warning("内存不足")logging.error("数据库连接失败")本章小结
- 用
with open()操作文件,自动管理资源 - 读写时记得指定
encoding="utf-8" pathlib是现代 Python 处理路径的首选方式- CSV 用
csv模块,JSON 用json模块 - 小文件可以一次读完,大文件用逐行读取
logging模块比手写日志文件方便得多
面试题
Q1:为什么推荐用with语句操作文件?
with语句(上下文管理器)保证文件在使用完毕后自动关闭,即使中间发生异常也不例外。
不用with的话,如果读取过程中出错,close()不会被执行,可能导致文件句柄泄漏。用try/finally也能解决但代码更啰嗦。
with不仅用于文件,任何需要"用完清理"的资源(数据库连接、锁等)都适合用上下文管理器。
Q2:读取大文件时应该怎么做?为什么不能直接用f.read()?
f.read()会一次性把整个文件读进内存。如果文件很大(比如几个 GB),会导致内存不足或程序变慢。
正确的做法是逐行读取:
withopen("big_file.txt")asf:forlineinf:process(line)for line in f是惰性读取,每次只在内存中保留一行。无论文件多大,内存占用都很小。
Q3:json.dumps()中ensure_ascii=False的作用是什么?
默认情况下json.dumps()会把非 ASCII 字符(如中文)转成\uXXXX形式:
json.dumps({"name":"小明"})# '{"name": "\\u5c0f\\u660e"}'设置ensure_ascii=False后,会保留原始字符:
json.dumps({"name":"小明"},ensure_ascii=False)# '{"name": "小明"}'在处理中文数据时通常需要设置这个参数,否则输出的 JSON 不可读。
Q4:pathlib相比os.path有什么优势?
- 面向对象:
pathlib用 Path 对象代替字符串,有丰富的方法和属性 - 路径拼接直观:用
/运算符代替os.path.join() - 跨平台:自动处理不同系统的路径分隔符
- 功能集成:读写文件、遍历目录等操作都集成在 Path 对象上
- 链式调用:
Path("data").joinpath("output").with_suffix(".json")
os.path也能做到所有事情,但代码更冗长。新项目推荐用pathlib。