第3章 3.4 文件读写与目录遍历
🎯 开场:为什么要学这个?
上一章我们学会了用字符串函数和正则表达式处理文字,但处理完的数据放哪儿?总不能每次重启程序就消失吧?
你有没有遇到过这些崩溃时刻?
- 写了个爬虫跑了一晚上,早上起来发现数据全丢了——程序一关,啥都没了
- 帮老师整理了 200 份学生成绩 Excel,改了半天结果覆盖了原文件,欲哭无泪
- 想批量重命名一个文件夹里的 500 张照片,手动改到怀疑人生
这一章你能学到:
学完本章,你的数据能「持久化保存」——程序关了数据还在,下次运行还能读出来。你还能批量处理文件夹里的文件,彻底告别机械重复操作。
🧱 基础:文件读写与目录遍历的核心概念
3.4.1 文件读写:数据的「收纳盒」
是什么?
文件就是电脑硬盘上的一个「收纳盒」,程序可以把数据放进去(写),也可以从里面取出来(读)。
为什么要用?
变量和列表里的数据,程序一关机就没了。文件就像一个永久的「储物柜」,让你的数据跨程序存活。
怎么用?
P\n\n
\n\n
\n\nython 里用 open() 函数打开文件,用完记得「关门」:
# 打开文件(如果不存在,会自动创建)
# 'w' 是写入模式,会覆盖原内容
# 'a' 是追加模式,在原内容后面添加
# 'r' 是读取模式
# 写入数据
with open('笔记.txt', 'w', encoding='utf-8') as f:
f.write('今天学会了文件读写!\n')
f.write('感觉打开了新世界的大门')
# 读取数据
with open('笔记.txt', 'r', encoding='utf-8') as f:
内容 = f.read()
print(内容)
运行结果:
今天学会了文件读写!
感觉打开了新世界的大门
这代码在干嘛:
- open('笔记.txt', 'w') 打开一个叫「笔记.txt」的文件,准备写入
- with ... as f: 这个语法是「用完自动关门」,防止忘记 close()
- f.write() 把文字塞进文件
- f.read() 把文件内容全部读出来
💡 敲黑板:
encoding='utf-8'必须写!不写的话,中文会变成乱码,踩过的人都说后悔。
3.4.2 按行读写:处理大文件的正确姿势
是什么?
有时候文件很大(几万行),一次性全部读入会撑爆内存。按行读就是「一条一条处理」,像吃自助餐一样取完一条再取下一条。
为什么要用?
假设有个 100 万行的日志文件,一次性读入会占用几个 G 内存,电脑直接卡死。按行读每次只占用一行内存,稳定又安全。
怎么用?
# 写入多行
lines_to_write = ['第一行:春天\n', '第二行:夏天\n', '第三行:秋天\n', '第四行:冬天\n']
with open('四季.txt', 'w', encoding='utf-8') as f:
f.writelines(lines_to_write)
# 按行读取(方法1:readlines 一次性读所有行)
with open('四季.txt', 'r', encoding='utf-8') as f:
all_lines = f.readlines()
for line in all_lines:
print(line.strip()) # strip() 去掉换行符
print('---分割线---')
# 按行读取(方法2:for 循环,更省内存)
with open('四季.txt', 'r', encoding='utf-8') as f:
for line in f:
print(f'读取到:{line.strip()}')
运行结果:
第一行:春天
第二行:夏天
第三行:秋天
第四行:冬天
---分割线---
读取到:第一行:春天
读取到:第二行:夏天
读取到:第三行:秋天
读取到:第四行:冬天
这代码在干嘛:
- readlines() 把所有行读进内存(返回一个列表)
- for line in f: 是更优雅的方式,一行一行读,不占内存
- strip() 去掉每行末尾的 \n(换行符),不然打印出来会有空行
⚠️ 注意:方法2的
for line in f:才是生产环境推荐用法,方法1只适合小文件。
3.4.3 JSON 文件:让数据「活」过来
是什么?
纯文本文件只能存字符串,但程序里常用字典、列表这些「有结构」的数据。JSON 格式就是让这些结构化数据能存进文件、再读出来。
为什么要用?
你做了一个成绩管理系统,学生信息是 {'name': '张三', 'score': 95} 这样的字典。不用 JSON 的话,你得自己设计一种格式来存「嵌套结构」,麻烦死了。
怎么用?
import json
# 定义一个数据结构(字典里嵌套列表)
学生数据 = {
'班级': '初三一班',
'人数': 3,
'学生列表': [
{'姓名': '王小明', '语文': 92, '数学': 88},
{'姓名': '李小红', '语文': 97, '数学': 95},
{'姓名': '张铁柱', '语文': 85, '数学': 90}
]
}
# 写入 JSON 文件
with open('成绩单.json', 'w', encoding='utf-8') as f:
json.dump(学生数据, f, ensure_ascii=False, indent=2)
# ensure_ascii=False 让中文正常显示
# indent=2 让 JSON 格式化,易读
# 读取 JSON 文件
with open('成绩单.json', 'r', encoding='utf-8') as f:
读出的数据 = json.load(f)
print(f"班级:{读出的数据['班级']}")
print(f"第一个学生:{读出的数据['学生列表'][0]['姓名']}")
运行结果:
班级:初三一班
第一个学生:王小明
这代码在干嘛:
- json.dump() 把字典写入文件
- json.load() 把文件内容读出来,自动变回 Python 字典
- 读出来的东西和写入前一模一样,结构完全保留
3.4.4 目录遍历:批量处理文件的神器
是什么?
目录就是文件夹。目录遍历就是让程序自动「走进」一个文件夹,把里面的文件一个一个拿出来处理。
为什么要用?
你想把一个文件夹里所有 .txt 文件改名为「日期_原名」,手动改 100 个会疯掉。目录遍历让程序自动化处理,省时省力。
怎么用?
import os
# 获取当前目录下的所有文件和文件夹
当前目录内容 = os.listdir('.')
print('当前目录内容:', 当前目录内容)
# 判断是文件还是文件夹
for item in 当前目录内容:
完整路径 = os.path.join('.', item)
if os.path.isfile(完整路径):
print(f'📄 文件:{item}')
elif os.path.isdir(完整路径):
print(f'📁 文件夹:{item}')
# 遍历特定后缀的文件( glob 方式,更简洁)
import glob
print('\n所有 .py 文件:')
for python文件 in glob.glob('*.py'):
print(f' {python文件}')
# 递归遍历(包含子文件夹)
print('\n所有文件(递归):')
for 根目录, 子文件夹列表, 文件列表 in os.walk('.'):
for 文件 in 文件列表:
print(os.path.join(根目录, 文件))
运行结果:
当前目录内容:['笔记.txt', '四季.txt', '成绩单.json', 'demo.py']
📄 文件:笔记.txt
📄 文件:四季.txt
📄 文件:成绩单.json
📄 文件:demo.py
所有 .py 文件:
demo.py
所有文件(递归):
./笔记.txt
./四季.txt
./成绩单.json
./demo.py
这代码在干嘛:
- os.listdir() 列出目录下所有内容
- os.path.isfile() 判断是不是文件
- os.path.isdir() 判断是不是文件夹
- glob.glob('*.py') 找出所有匹配通配符的文件
- os.walk() 会递归进入每个子文件夹,是批量处理的神器
💡 小技巧:
*.py里的*是通配符,代表「任意字符」。*.txt就是所有 txt 文件。
🔥 实战:3 个递进的小项目
项目 1:个人备忘录(5 分钟)
场景:你想做一个命令行备忘录,能添加笔记、能查看历史。
import json
import os
备忘录文件 = 'memo.json'
# 如果文件不存在,创建一个空备忘录
if not os.path.exists(备忘录文件):
with open(备忘录文件, 'w', encoding='utf-8') as f:
json.dump([], f)
# 读取现有备忘录
with open(备忘录文件, 'r', encoding='utf-8') as f:
备忘录列表 = json.load(f)
print('=== 我的备忘录 ===')
print(f'共 {len(备忘录列表)} 条记录\n')
# 显示所有记录
for i, 条 in enumerate(备忘录列表, 1):
print(f'{i}. [{条["时间"]}] {条["内容"]}')
# 添加新记录
新记录 = input('\n请输入新备忘录(直接回车退出):')
if 新记录.strip():
import datetime
当前时间 = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
备忘录列表.append({'时间': 当前时间, '内容': 新记录})
with open(备忘录文件, 'w', encoding='utf-8') as f:
json.dump(备忘录列表, f, ensure_ascii=False, indent=2)
print('✅ 已保存!')
运行结果:
=== 我的备忘录 ===
共 2 条记录
1. [2024-01-15 10:30] 买牛奶
2. [2024-01-15 14:20] 开会讨论方案
请输入新备忘录(直接回车退出):学习文件读写
✅ 已保存!
一句话解释:用 JSON 文件存储备忘录列表,每次运行程序都能「记住」之前的内容。
项目 2:批量重命名工具(15 分钟)
场景:你下载了 50 张照片,文件名全是 IMG_001.jpg、IMG_002.jpg 这种,想改成 旅行_001.jpg、旅行_002.jpg。
import os
import glob
# 配置区
源文件夹 = './photos'
文件前缀 = '旅行'
文件后缀 = '.jpg'
# 确保文件夹存在
if not os.path.exists(源文件夹):
os.makedirs(源文件夹)
print(f'已创建文件夹 {源文件夹},请放入图片后重新运行')
exit()
# 找出所有 jpg 文件
所有图片 = glob.glob(os.path.join(源文件夹, '*.jpg')) + \
glob.glob(os.path.join(源文件夹, '*.jpeg')) + \
glob.glob(os.path.join(源文件夹, '*.png'))
print(f'找到 {len(所有图片)} 个图片文件')
# 统计重命名结果
成功数 = 0
跳过数 = 0
for 序号, 旧路径 in enumerate(所有图片, 1):
# 分离文件名和扩展名
目录, 原名 = os.path.split(旧路径)
_, 扩展名 = os.path.splitext(原名)
# 生成新文件名
新文件名 = f'{文件前缀}_{序号:03d}{扩展名}' # :03d 意思是补齐3位
新路径 = os.path.join(目录, 新文件名)
# 检查新文件名是否已存在
if os.path.exists(新路径):
print(f'⏭️ 跳过(已存在):{新文件名}')
跳过数 += 1
continue
# 执行重命名
os.rename(旧路径, 新路径)
print(f'✅ {原名} → {新文件名}')
成功数 += 1
print(f'\n完成!成功 {成功数} 个,跳过 {跳过数} 个')
运行结果:
找到 5 个图片文件
✅ IMG_001.jpg → 旅行_001.jpg
✅ IMG_002.jpg → 旅行_002.jpg
✅ IMG_003.jpg → 旅行_003.jpg
⏭️ 跳过(已存在):旅行_001.jpg
✅ vacation.jpg → 旅行_004.jpg
完成!成功 4 个,跳过 1 个
一句话解释:用 glob 找出所有图片,用 os.rename 批量改名字,还加了「文件已存在就跳过」的保护逻辑。
项目 3:日志分析小工具(15 分钟)
场景:你有一个网站的访问日志,想统计哪些页面最受欢迎、哪些 IP 访问最频繁。
先准备一个测试日志文件:
# 运行一次生成测试数据
测试日志内容 = """192.168.1.100 - - [15/Jan/2024:10:30:00 +0800] "GET /index.html HTTP/1.1" 200 2326
192.168.1.101 - - [15/Jan/2024:10:30:05 +0800] "GET /products.html HTTP/1.1" 200 4521
192.168.1.100 - - [15/Jan/2024:10:30:10 +0800] "GET /index.html HTTP/1.1" 200 2326
192.168.1.102 - - [15/Jan/2024:10:30:15 +0800] "GET /about.html HTTP/1.1" 200 1234
192.168.1.100 - - [15/Jan/2024:10:30:20 +0800] "GET /products.html HTTP/1.1" 200 4521
192.168.1.101 - - [15/Jan/2024:10:30:25 +0800] "GET /index.html HTTP/1.1" 200 2326
192.168.1.103 - - [15/Jan/2024:10:30:30 +0800] "GET /contact.html HTTP/1.1" 404 512
192.168.1.100 - - [15/Jan/2024:10:30:35 +0800] "GET /index.html HTTP/1.1" 200 2326"""
with open('access.log', 'w', encoding='utf-8') as f:
f.write(测试日志内容)
print('测试日志文件已生成')
然后运行分析工具:
import re
from collections import Counter
# 读取日志文件
with open('access.log', 'r', encoding='utf-8') as f:
日志内容 = f.readlines()
# 统计变量
页面计数 = Counter()
IP计数 = Counter()
错误请求 = []
# 逐行分析(正则表达式解析 Apache 日志格式)
日志格式 = re.compile(
r'^(\d+\.\d+\.\d+\.\d+).*' # IP 地址
r'\[.*?\]\s*' # 时间戳(跳过)
r'"(GET|POST)\s+([^"]+)"' # 请求方法和页面路径
r'\s+\d+\s+(\d+)' # 状态码和大小
)
for 行 in 日志内容:
匹配结果 = 日志格式.match(行)
if 匹配结果:
IP, 请求方法, 页面路径, 状态码 = 匹配结果.groups()
页面计数[页面路径] += 1
IP计数[IP] += 1
# 记录 404 等错误
if 状态码 != '200':
错误请求.append({
'IP': IP,
'页面': 页面路径,
'状态码': 状态码
})
# 输出报告
print('=' * 50)
print('📊 日志分析报告')
print('=' * 50)
print('\n🏆 最受欢迎的页面(Top 5):')
for 页面, 次数 in 页面计数.most_common(5):
print(f' {页面:<20} 访问 {次数} 次')
print('\n👥 访问最多的 IP(Top 5):')
for IP, 次数 in IP计数.most_common(5):
print(f' {IP:<15} 访问 {次数} 次')
if 错误请求:
print('\n⚠️ 非正常请求:')
for 错 in 错误请求:
print(f' {错["IP"]} 请求 {错["页面"]} 返回 {错["状态码"]}')
else:
print('\n✅ 没有发现错误请求')
# 把报告也存一份到文件
报告内容 = []
报告内容.append('=' * 50)
报告内容.append('日志分析报告\n')
报告内容.append('=' * 50)
报告内容.append('\n最受欢迎的页面:')
for 页面, 次数 in 页面计数.most_common():
报告内容.append(f' {页面}: {次数} 次')
报告内容.append('\n访问最多的 IP:')
for IP, 次数 in IP计数.most_common():
报告内容.append(f' {IP}: {次数} 次')
with open('日志报告.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(报告内容))
print('\n📄 报告已保存到 日志报告.txt')
运行结果:
==================================================
📊 日志分析报告
==================================================
🏆 最受欢迎的页面(Top 5):
index.html 访问 4 次
products.html 访问 2 次
about.html 访问 1 次
contact.html 访问 1 次
👥 访问最多的 IP(Top 5):
92.168.1.100 访问 4 次
92.168.1.101 访问 2 次
92.168.1.102 访问 1 次
92.168.1.103 访问 1 次
⚠️ 非正常请求:
92.168.1.103 请求 /contact.html 返回 404
📄 报告已保存到 日志报告.txt
一句话解释:用正则表达式拆解日志每一行,用 Counter 统计出现次数,最后把分析结果存成报告文件。
💪 进阶:常见坑 + 性能小贴士
坑 1:忘记 encoding='utf-8',中文全变乱码
# ❌ 错误写法
with open('笔记.txt', 'w') as f:
f.write('今天很开心')
# ✅ 正确写法
with open('笔记.txt', 'w', encoding='utf-8') as f:
f.write('今天很开心')
坑来了:Windows 默认编码是 GBK,不写
encoding='utf-8'的话,中文文件会变成乱码。
坑 2:写入模式 w 会覆盖原文件
# ❌ 错误操作:以为会追加,结果原内容没了
with open('数据.txt', 'w') as f:
f.write('新内容')
# ✅ 正确操作:想追加用 'a' 模式
with open('数据.txt', 'a') as f:
f.write('新内容')
坑来了:
w模式每次都会「先清空再写入」。如果想保留原内容,必须用a(append)模式。
坑 3:文件路径跨平台问题
# ❌ Windows 用户这样写没问题,Mac/Linux 用户就挂了
文件路径 = 'C:\\Users\\Admin\\Desktop\\文件.txt'
# ✅ 正确写法:用 os.path.join
import os
文件路径 = os.path.join('Users', 'Admin', 'Desktop', '文件.txt')
# 或者 pathlib(更现代)
from pathlib import Path
文件路径 = Path('Users') / 'Admin' / 'Desktop' / '文件.txt'
坑 4:读取大文件用 read() 撑爆内存
# ❌ 大文件这样读会崩
with open('大日志.log', 'r') as f:
全部内容 = f.read() # 几个 G 的文件直接内存爆炸
# ✅ 正确做法:逐行处理
with open('大日志.log', 'r') as f:
for 行 in f: # 一行一行读,不占内存
if '错误' in 行:
print(行)
坑 5:os.walk() 的返回值容易搞混顺序
# os.walk() 返回的是 (根目录, [子文件夹列表], [文件列表])
# 很多新手搞不清楚这个顺序
for 根目录, 子文件夹, 文件列表 in os.walk('.'):
print(f'当前在:{根目录}')
print(f'子文件夹:{子文件夹}')
print(f'文件:{文件列表}')
记住技巧:
os.walk的返回顺序是「根、子、文件」,谐音「根(du)子(gen)文(wen)件」= 根子文。
性能小贴士:写入大量数据时用缓冲
# 如果要写入几万行数据,一行一行写很慢
# 可以先把所有内容攒一起,最后一次性写
大量行 = [f'第{i}行数据\n' for i in range(100000)]
# ❌ 慢:每行都触发一次磁盘写入
with open('慢.txt', 'w') as f:
for 行 in 大量行:
f.write(行)
# ✅ 快:攒一起只写一次
with open('快.txt', 'w') as f:
f.writelines(大量行) # 一次性写入
调试技巧:用 print 打日志
# 读文件前先检查文件存不存在
import os
文件路径 = '数据.json'
print(f'尝试打开:{文件路径}') # 调试:打印路径确认
if os.path.exists(文件路径):
with open(文件路径, 'r') as f:
数据 = f.read()
print(f'成功读取,内容长度:{len(数据)}') # 调试:确认读到了多少
else:
print('❌ 文件不存在!') # 调试:定位问题
调试口诀:怀疑哪行有问题,就在那行前后加
✏️ 练习题
练习 1(2 分钟):追加内容到备忘录
- 输入:运行项目 1 的代码,连续添加两条备忘录
- 预期输出:第二次运行时能看到两条记录
- 提示:用
'a'模式打开文件可以追加而不是覆盖
练习 2(2 分钟):判断文件还是文件夹
- 输入:用
os.path.isfile()和os.path.isdir()判断当前目录下的成绩单.json是什么 - 预期输出:
成绩单.json 是文件或成绩单.json 是文件夹 - 提示:传入完整路径或确保在正确目录下运行
练习 3(5 分钟):读取并统计 JSON 数据
- 输入:修改项目 1,添加「统计共有多少条备忘录」的功能
- 预期输出:显示
共 X 条备忘录 - 提示:
len(备忘录列表)就能得到数量
练习 4(8 分钟):统计文件夹里有多少个 .txt 文件
- 输入:用
glob.glob('*.txt')统计当前目录的 txt 文件数量 - 预期输出:
当前目录有 X 个 .txt 文件 - 提示:先确保有 2-3 个 txt 文件,不然结果可能是 0
练习 5(10 分钟):修复报错(挑战题)
- 输入:以下代码运行时报错,看图分析原因并修复
# 代码意图:读取一个可能不存在的配置文件
with open('config.json', 'r') as f:
配置 = json.load(f)
- 预期输出:如果文件不存在,给出友好提示而不是崩溃
- 提示:
try...except可以捕获错误,或者先os.path.exists()检查
作业:做一个「个人图书管理器」
需求描述:
做一个命令行图书管理工具,帮你管理自己的小书库。
功能点:
1. 能添加图书(书名、作者、ISBN)
2. 能查看所有图书列表
3. 能搜索图书(按书名或作者关键字)
4. 能删除图书(按 ISBN)
5. 数据保存到 JSON 文件,程序重启不丢失
加分项:
1. 添加图书时检查 ISBN 是否已存在
2. 显示统计信息(共有多少本书、多少位作者)
验收标准:
- 能成功添加、查看、搜索、删除图书
- 关闭程序后重新打开,数据还在
- 代码有注释,能看懂
提交方式:评论区贴代码或 GitHub 链接
📚 总结
这一章我们学了 3 个核心技能:
- 文件读写:用
open()+with语法安全地读写文件,记住encoding='utf-8' - JSON 存储:用
json.dump()和json.load()让复杂数据结构持久化 - 目录遍历:用
glob和os.walk批量处理文件夹里的文件
推荐资源:
- 官方文档:https://docs.python.org/3/library/filesys.html
- 书籍:《Python编程:从入门到实践》第 8 章
- 视频:B 站搜索「Python 文件操作 教程」
互动钩子:
💬 你有没有遇到过「文件数据丢失」的惨痛经历?或者用文件读写做过什么有趣的小工具?评论区聊聊,老粉优先回复!
下章预告:
学完了文件读写,下一章我们要做一个「CSV 导入导出工具」——想象一下,你手里有一份学生成绩表,怎么用 Python 自动读取、筛选、然后导出一份新的报表?下一章见!

评论(0)