开门见山说一句如果你正在跟Python从入门到精通这样一套课程走到第40天那你今天学的东西值得你烧壶水坐下来慢慢看。为什么因为day40这个位置正好是从会写脚本跨向能写工具的分水岭。前面几十天你学了变量、列表、字典、函数甚至可能已经能为一个模块设计函数但这些都是内存里玩的逻辑到了文件读写、数据持久化这一课Python才真正开始帮你把计算结果留下来、把配置文件读进来、把日志写下去。今天这一篇我按自己当时学这套课的经验把day40涉及的核心内容、底层机制、实操细节和踩过的坑全部摊开来讲不绕弯子。整个day40如果让我用一句话概括就是把Python程序变成能落地、能复用、能接受真实世界数据输入输出的工具。适合谁看刚走到这个节点的自学者、复习文件操作的老手以及那些学了语法但没真正做过文件处理项目的同学都有你能带走的东西。1. 为什么第40天这个位置非得学文件与数据持久化1.1 从内存到磁盘编程思维的一次升级前面30多天你写的几乎都是内存程序定义一个列表往里面塞数据然后打印出来。程序一结束内存里所有东西灰飞烟灭。这本身没错但如果你只停留在这个层面后面做什么都会卡壳。举个最日常的例子你写了一个爬虫脚本第一天跑完把结果打印在屏幕上还没来及复制就关了终端数据没了。这时候你最需要的是什么是把结果写进文件。或者你写了一个配置文件驱动的程序里面存着数据库地址、接口超时时间、并发数如果这些要改就得改代码、重新部署那这个程序根本没有达到工具的及格线——而把配置放到外部文件里、用脚本去读取就是day40给出的答案。所以day40学的不只是几个文件名、函数名而是一整套程序如何与外部世界交换数据的思维模型内存里的数据是易失的外部文件是持久的程序可以读入外部状态也可以把计算结果固化下来。这一步跨过去了后面的网络请求、数据库操作、GUI程序开发都建立在同一套思维上。1.2 学完这天你的代码能解决哪些实际问题很多初学者会觉得文件读写太简单就是open()加read()。但到了day40课程会带着你往纵深走学完你会发现自己能处理的具体问题突然变多了。第一类问题是配置管理。比如你手上有个数据处理脚本每天要连一个服务地址、带一个token、控制一批参数。用命令行传参太繁琐写死在代码里又没法改用JSON或INI配置文件让程序自己读取就清爽多了。第二类问题是结果保存。跑数据统计、批量处理图片、抓取网页列表能输出CSV或JSON文件别人就能用Excel打开、用其他程序接手你的代码才真正提供了交付物。第三类问题是日志与断点续跑。程序跑了一半出错能不能把已经处理完的记录写到本地下次启动时跳过已经处理的部分这就是文件系统的妙用。我个人的体会是代码能力有没有质的提升不是看你会不会写类、会不会用装饰器而是看你对数据从哪来、加工完去哪儿有没有概念。day40给的正是这个概念。2. 三种文件读写模式的底层逻辑与选型2.1 文本模式、二进制模式与编码选择的底层逻辑open()函数是世界上被误解最多的函数之一因为大家只记住了打开文件却忽略了它的mode参数决定了整个数据流怎么被处理。第一个底层逻辑是文本和二进制没有谁优谁劣只有谁合适。文本模式r、w、a下Python会把文件里的字节按照指定编码解码成字符串二进制模式rb、wb、ab下Python原封不动地返回字节对象bytes你不做解码它就不解码。所以当你处理图片、音频、压缩包时必须用二进制模式如果你用文本模式去读一个PDF轻则乱码重则碰到解码错误直接抛异常。第二个底层逻辑是编码问题。day40一定会碰这个坎因为它太经典了。文本文件的本质是字节序列而字符串是经过解码的语义产物。你用UTF-8写入的内容如果换一个用GBK默认解码的环境去读就会产生乱码或UnicodeDecodeError。我的建议是所有新写的代码打开文件时一律显式传encodingutf-8不要吃系统默认编码的暗亏。第三个底层逻辑是打开模式的语义。w会先清空目标文件再开始写a是追加到尾部r是读写同时打开但位置指针默认在开头。这里最容易出错的正是r——很多人以为它像a一样会自动到文件末尾其实不是r打开后光标在0你直接写入会从第一个字符开始覆盖。提示如果不确定当前文件操作会覆盖还是追加先在本地小文件上用打印验证比什么都快。2.2 读文件时最常见的三个坑第一个坑路径分隔符。Windows 下写C:\Users\xxx\data.txt\U和\x会被识别成转义字符。解决办法不是把路径写死在代码里而是用pathlib.Path比如Path(rC:\Users\xxx\data.txt)。这个模块到了第40天你已经完全应该掌握了——路径拼接、判断存在、读文件一气呵成。第二个坑忘记关闭文件。几年前我排查过一个诡异的问题某脚本持续运行两天后报文件被占用写不了日志。定位后发现是一个分支里用了open()但没close()句柄泄漏文件被锁住。从day40开始就应该养成无脑使用with的习惯半秒钟的事能避开一整夜的排查。第三个坑大文件直接read()。如果文件有2GB你直接content f.read()内存当场爆炸。正确做法是分块或逐行读取后面的章节我会详细给代码方案。3. 上下文管理器with语句背后的完整机制3.1 __enter__与__exit__到底做了什么with语句在day40不是新知识但这一天你会第一次真正意识到之前写的是皮毛。它的背后是上下文管理器协议也就是两个魔法方法__enter__负责进场准备返回被管理的资源__exit__负责退场清理哪怕代码中间抛出异常它也会被调用。我当年第一次看到原理时的反应是哦原来我每次with open(...) as f都在用协议拨开云雾的感觉。后来我去自定义数据库连接的关闭逻辑、时间统计工具时才发现这个模式的价值远不止关文件。一个让我印象深刻的例子是修改文件后自动恢复当前目录。某项目需要在操作临时文件后把工作目录切回去如果只用try/finally也能写但用上下文管理器封装后调用方只需要一个with代码风格立刻从防御式堆叠变成了声明式直白。3.2 自定义上下文管理器的两种写法与适用场景第一种写法是写类实现__enter__和__exit__。适合需要携带状态、逻辑复杂的场景。比如我想让一段代码自动计时并把耗时写入日志import time class Timer: def __enter__(self): self.start time.perf_counter() return self def __exit__(self, exc_type, exc_value, exc_tb): self.cost time.perf_counter() - self.start print(f耗时: {self.cost:.4f} 秒) return False # 不吞异常让异常继续向上抛 with Timer(): total sum(range(1_000_000))第二种写法是用标准库的contextlib.contextmanager适合逻辑简单的场景。它把一个生成器函数包装成上下文管理器yield之前的部分就是__enter__之后的部分就是__exit__。注意如果生成器内部没有捕获异常异常会在yield的位置重新抛出相当于自动实现return False。from contextlib import contextmanager contextmanager def temporary_change_dir(path): import os old os.getcwd() os.chdir(path) try: yield finally: os.chdir(old)这两种写法到底用哪个我的经验是需要明确给外部返回资源对象且资源生命周期清晰时用类只想包一层前后逻辑、不需要返回对象时用contextmanager。不要为了炫技使用它但遇到进入前准备、退出后清理的场景能想到用上下文管理器就说明你已经脱离只会写线性代码的层级了。4. 数据持久化实战用JSON和CSV保存真实数据4.1 JSON的序列化、反序列化与漂亮输出day40到了后半段一定会把文件和结构化数据结合起来因为只读写纯文本的txt文件实际用处有限。JSON是理解最简单、调试最直观的结构化存储方案。序列化就是把Python的字典/列表转成JSON字符串并写入文件反序列化就是读文件再转回Python对象。常规写法是import json data { name: 某开发者, skills: [python, data_processing], projects: [ {id: 1, score: 95}, {id: 2, score: 88} ] } with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) with open(data.json, r, encodingutf-8) as f: loaded json.load(f)这里两个细节必须注意。一是ensure_asciiFalse不写的话中文字符会被保存成\u67d0这种转义序列文件可读性降为零交给同事或前端排查问题时非常崩溃。二是indent2是给人类看的如果文件只是程序内部对接不需要加缩进省一点体积。还有一个常被忽略的问题是json.dump与json.load的类型限制。字典的键如果包含元组json.dump会直接报TypeError因为JSON标准只支持字符串作为键。所以我在做序列化前往往先在内存里做一次数据规范化把键全部转成字符串宁可多写两行也比后期加各种default回调干净。4.2 CSV读写常见坑与DictWriter的正确用法CSV在业务工作中出现的频率远超想象。做数据处理的人经常要交付CSV给其他团队但新手写CSV时最容易犯的一个错误是以为它只是个用逗号分隔的文本直接自己用字符串.join拼接。我见过一个真实案例某同学用,拼接字段结果一位用户的昵称里有英文逗号整行数据列数错位后续程序把每一列都读崩了。修复这个问题的正道是用标准库csv模块它知道怎么处理引号包裹、转义和换行。推荐用csv.DictWriter代码意图清晰又不容易出现列取值不对称的问题import csv rows [ {id: 1, name: A同学, score: 92}, {id: 2, name: B同学, score: 87}, ] with open(scores.csv, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[id, name, score]) writer.writeheader() writer.writerows(rows)这里我特意写了encodingutf-8-sig是在Excel打开CSV不再乱码的关键。用纯utf-8写出来的CSV有些Excel版本会因缺少BOM头而不认中文加utf-8-sig相当于在文件开头写了一个不可见的BOM标志Excel就懂了。newline也是一个容易踩的坑。在Windows上不加它写CSV文件时行与行之间会多一个空行因为csv模块内嵌换行处理同时系统的行结束符又插了一次。经验就是凡是开发CSV写文件时newline是标准配置别问为什么不加就出问题先加为敬。提示读取CSV时如果某些字段是数值csv模块默认全部读成字符串。需要转换时自己写一个小函数做类型清洗别指望它自动推断类型。5. 大文件处理与增量读写方案5.1 逐行读入的内存控制思路到了day40你已经不是随便写个脚本跑着玩的新手了一定会在某个时刻遇到大文件。比如了几GB的日志、导出的数据全量备份。此时read()一次性加载是灾难正确思路是迭代式读取。最基础的行迭代原生就支持with open(huge_log.txt, r, encodingutf-8) as f: for line in f: process(line.rstrip(\n))这个for line in f看起来简单但它是惰性加载的一次只读一行内存占用非常低。如果你确实需要按块处理比如按固定字节数切割用read(size)循环更合适with open(huge_file.bin, rb) as f: while chunk : f.read(8192): process(chunk):是海象运算符第40天学到这个水平你可以开始在各种循环里大胆用它了。它的含义是读取一块数据并赋值给 chunk如果非空就继续进循环非常契合文件流的处理习惯。5.2 日志文件的实时追加与轮转思路day40的进阶应用里我最推荐的案例是日志写入。既要持续写入还要考虑单文件体积膨胀的问题。持续追加很简单用a模式def write_log(message: str, log_file: str app.log): with open(log_file, a, encodingutf-8) as f: f.write(f{time.strftime(%Y-%m-%d %H:%M:%S)} {message}\n)但这样写久了文件会越来越庞大打开慢、磁盘占用多。生产环境的常规做法是日志轮转当文件超过阈值时把当前日志重命名为带时间戳的历史文件再新建一个空文件继续写。用Python实现一个简易轮转也不复杂import os def safe_log(message: str, log_file: str app.log, max_size: int 10 * 1024 * 1024): if os.path.exists(log_file) and os.path.getsize(log_file) max_size: timestamp time.strftime(%Y%m%d_%H%M%S) os.replace(log_file, f{log_file}.{timestamp}) with open(log_file, a, encodingutf-8) as f: f.write(f{time.strftime(%Y-%m-%d %H:%M:%S)} {message}\n)这里用的os.replace不是os.rename原因我在实际项目中体验过Windows下如果目标文件已被某些程序占用os.rename可能抛权限错误os.replace的语义是原子替换在跨平台场景下更稳。6. 常见问题与排查技巧实录6.1 典型报错与解决办法速查表我盘点了一下写文件那几年高频出现的报错做成一张表。很多问题不是因为你笨而是环境、编码、模式纠缠在一起把一次简单操作变成大片现场。报错信息出现原因解决思路UnicodeDecodeError读取时按错误编码解码确认文件真实编码显式传encoding打开UnicodeEncodeError写入时目标编码无法表示字符统一用UTF-8少用Windows默认的GBKPermissionError文件被其他进程占用或无写入权限检查是否已在Excel中打开CSV关闭后重试FileNotFoundError路径不存在使用绝对路径或用Path.mkdir(parentsTrue, exist_okTrue)先建目录IsADirectoryError传的路径是文件夹不是文件打印调试路径确认目标类型TypeError: write() argument must be str, not bytes文本模式写入bytes要么改wb模式要么先decode()TypeError: Object of type set is not JSON serializable把不可序列化类型丢给json先转成列表或使用defaultstr兜底每一行我都配一个真实场景想一下比如PermissionError十次里有七次是你自己在Excel里开着这个CSV程序写完想关闭或覆盖Windows就会锁住文件。遇到这种问题第一时间不查代码先看有没有程序正开着目标文件。6.2 几个值得收藏的实操小习惯第一临时目录优先用系统临时目录。今天写中间文件明天清理可能忘了。标准库tempfile能生成随机文件名用完自动销毁比如from tempfile import NamedTemporaryFile with NamedTemporaryFile(deleteTrue, suffix.txt) as tmp: tmp.write(btemp data)第二写路径不要硬编码。用Path(__file__).resolve().parent来定位当前脚本所在目录再拼接相对路径这样换了机器、换了文件夹代码一样跑。from pathlib import Path base_dir Path(__file__).resolve().parent data_path base_dir / data / result.json第三写入重要文件时先写临时文件再替换。比如保存用户配置直接写原文件万一半路断电或异常原文件就坏了。更稳妥的做法是写一个config.json.tmp成功后用os.replace原子替换原文件。这个习惯帮我避免了好几次配置丢失的悲剧比事后懊悔强太多。7. 一些基于个人项目经验的进阶体会最后聊点课程之外的东西。day40的知识点拆开看无非是open、with、json、csv但组合起来的场景千变万化。我在自己的一段数据处理项目中用这套知识做了一个批量文件合并工具扫描某目录下所有同结构CSV逐行读入把符合条件的数据清洗后统一写入一个新的JSON文件整个过程完全用上下文管理器组织资源异常被记录到日志但没有中断整体流程。写完后我对文件操作的理解完全更新了——它不只是读写两个函数而是一种工程思维。以我个人的经验学到这一天非常建议你给自己布置一个真实型练习从网上下载一份公开数据或者拿自己手机导出的一批联系人写一个Python脚本读取、清洗、去重、统计再输出成一份结构良好的CSV。完成这个练习后你再看后面要学的网络请求、数据库操作会有完全不同的底气。多写、多踩坑多尝试在真实场景里应用。文件读写这门手艺光看课程记笔记是学不深、学不活的。希望这篇day40的总结能让你少走一点我当时走过的弯路。
阅读完成 · 觉得有帮助?