
1. 从一道测验题看透文件操作的核心逻辑很多人第一次接触 Python 的文件读写都是在类似“测验7文件和数据格式化”这样的练习里。题目本身可能只是让你统计一个文本里的词频、把字典写进 CSV、或者把一列数据格式化成对齐的输出。但我带了这么多期学习者之后发现一个规律真正卡住大家的从来不是语法而是对“数据在磁盘和内存之间怎么流动”这件事没有建立起直觉。这篇内容我就借这道测验的壳把文件操作和数据格式化这两块讲透。它适合刚学完 Python 基础语法、正准备跨过“能写脚本”这道坎的人也适合那些写过一些代码但每次遇到编码报错、路径找不到、格式对不齐就靠搜索引擎救场的人。核心关键词就三个文件读写、数据格式化、异常处理。把这三个吃下来你后面处理日志、做数据清洗、写自动化脚本基本都是一路平推。先说清楚这道测验通常考什么。根据我见过的多套同类题目它的考点集中在几个方向用open()正确打开文件并选择模式、用read/readline/readlines或迭代器读取内容、把处理结果写回文件、用format或 f-string 做对齐和精度控制、以及用csv或json模块做结构化数据的序列化。看起来零散其实背后是一条完整的数据流水线读入 → 解析 → 处理 → 格式化 → 写出。你只要把这条线在脑子里立起来题目怎么变都不慌。我特别想强调一点文件操作是 Python 里少数几个“错误代价很真实”的领域。你在内存里算错一个数顶多结果不对但你要是用错了模式打开文件可能直接把源数据覆盖没了。所以这篇我会花不少篇幅讲模式选择、编码处理和异常兜底这些都是文档里一笔带过、但实际会咬人的地方。2. 文件读写的地基模式、编码与路径2.1 open 的模式到底怎么选open()的第二个参数是模式这是所有文件操作的起点。很多人背过那张表但真到用的时候还是会犹豫。我把常见的几种按“会不会破坏原文件”分成两类这样记更符合直觉。模式含义文件不存在时是否清空原内容典型场景r只读报错否读取配置、日志分析w只写创建是生成新结果文件a追加创建否持续写日志r读写报错否原地修改x独占创建创建不适用防止覆盖已有文件我踩过最疼的一个坑就是调试阶段用w打开了一个数据文件想看看内容结果一运行文件直接空了。因为w在打开的那一刻就把文件截断了跟你后面写不写没关系。所以我的习惯是——只要不是明确要生成新文件一律先用r打开确认能读再决定下一步。真要覆盖也先备份一份。还有一个细节文本模式默认和二进制模式加b的区别。文本模式会自动做换行符转换和编码解码二进制模式则是原样字节。处理图片、压缩包、序列化后的二进制数据时必须用rb/wb否则会报解码错误。测验里如果让你读一个非纯文本文件八成就是在考这个。2.2 编码问题为什么你的中文变成了乱码编码是文件操作里最高频的翻车点。默认情况下open()用的是系统默认编码在有些环境是 GBK在有些是 UTF-8。你在一台机器上写好的脚本换台机器读同一个文件中文就可能变成一堆问号或者方块。解决办法很直接显式指定encodingutf-8。这应该成为你的肌肉记忆。with open(data.txt, r, encodingutf-8) as f: content f.read()写文件同理也要指定编码否则在某些平台上写出的中文文件在别的软件里打开就是乱码。with open(output.txt, w, encodingutf-8) as f: f.write(处理完成)注意如果你读的文件本身不是 UTF-8 编码比如从某些旧系统导出的 GBK 文件强行用 UTF-8 读会抛UnicodeDecodeError。这时候要么用encodinggbk要么用errorsignore跳过无法解码的字节。但errorsignore会静默丢数据只建议在确认可以容忍丢失时使用。我个人的经验是处理来源不明的文本文件时先写个小脚本探测编码比盲目试要快。可以用chardet这类库也可以简单地 try 几种常见编码哪个不报错用哪个。2.3 路径这件事别再用字符串硬拼新手最容易犯的错是用字符串拼接路径比如data/ filename。这在 Windows 上会因为反斜杠和正斜杠的差异出问题。正确做法是用os.path.join或者更现代的pathlib。from pathlib import Path base Path(data) file_path base / input.txt if file_path.exists(): text file_path.read_text(encodingutf-8)pathlib的好处是路径就是一个对象拼接用/运算符判断存在用.exists()读文本用.read_text()写用.write_text()代码短且跨平台。测验里如果涉及多级目录用pathlib会省掉很多os.path的样板代码。另外提醒一句相对路径是相对于你运行脚本时所在的目录不是脚本文件所在的目录。这是另一个高频困惑点。如果你在 A 目录运行 B 目录下的脚本脚本里的相对路径会以 A 为基准。稳妥的做法是用Path(__file__).parent拿到脚本所在目录再基于它构造路径。3. 读取方式的选择一次读完还是逐行处理3.1 三种读法的适用边界Python 提供了read()、readline()、readlines()三种主要读法加上直接迭代文件对象一共四种。选哪种取决于文件大小和你后续要做什么。read()一次性把整个文件读成一个字符串。适合小文件比如配置文件、几百 KB 的文本。文件大了会吃光内存。readline()每次读一行。适合你只想读前几行或者需要精细控制读取节奏的场景。readlines()一次性读成列表每个元素是一行。适合文件不大、且你需要反复按索引访问行的场景。直接迭代for line in f:这是最 Pythonic 的逐行读法内存友好适合大文件。我实测过一个 500MB 的日志文件用readlines()直接把内存吃到几个 G机器开始卡换成for line in f:之后内存占用稳定在几十 MB。所以处理大文件时迭代是默认选择。with open(big_log.txt, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue process(line)3.2 换行符的处理细节不管用哪种读法每行末尾通常带着\n。如果你直接拿去做比较或拼接很容易出问题。比如abc\n abc是 False。所以读到行之后第一件事往往是strip()或rstrip(\n)。但要注意strip()会把行首行尾的所有空白都去掉包括空格和制表符。如果你的数据本身行首有意义的空格比如缩进敏感的格式就得用rstrip(\n)只去换行。还有一个坑文件最后一行可能没有换行符。用readlines()时最后一行不会带\n用迭代时也一样。如果你的逻辑依赖每行都有换行符就会在最后一行出错。稳妥的写法是统一strip()后再判断。3.3 用 with 管理资源别手动 closewith open(...) as f:这个写法不只是省事它保证了即使中间抛异常文件也会被正确关闭。手动f.close()的问题是一旦close()之前出了异常文件句柄就泄漏了。在长时间运行的程序里句柄泄漏最终会导致“打开文件过多”的错误。# 不推荐 f open(data.txt, r, encodingutf-8) content f.read() f.close() # 推荐 with open(data.txt, r, encodingutf-8) as f: content f.read()测验里如果考文件操作用with基本是标准答案。养成习惯之后你几乎不会再写手动 close。4. 数据格式化让输出既好看又能被程序读4.1 字符串格式化的三种武器Python 的字符串格式化经历了三代%格式化、str.format()、f-string。现在写新代码f-string 是首选因为它最直观、性能也最好。但读老代码时三种都会遇到所以都得认识。name 张三 score 92.5 # % 格式化老式 print(%s 的成绩是 %.1f % (name, score)) # str.format() print({} 的成绩是 {:.1f}.format(name, score)) # f-string推荐 print(f{name} 的成绩是 {score:.1f})f-string 里可以直接写表达式比如f{score * 1.1:.2f}这在拼装复杂输出时非常方便。4.2 对齐与宽度控制测验里经常要求输出对齐的表格比如左对齐的姓名列、右对齐的分数列。这靠格式说明符里的对齐符号和宽度数字实现。符号含义示例效果左对齐f{name:10}姓名后补空格到 10 宽右对齐f{score:8.1f}分数前补空格到 8 宽^居中f{title:^20}标题居中占 20 宽0补零f{num:05d}数字补零到 5 位students [(张三, 92.5), (李四, 88.0), (王五, 100.0)] for name, score in students: print(f{name:8}{score:8.1f})输出会是整齐的两列。这里的关键是宽度要统一否则列会错位。中文和英文的显示宽度不一样中文一个字通常占两个英文字符的宽度所以纯用字符数对齐时中英混排会歪。如果对美观要求高得按显示宽度计算或者干脆用制表符分隔。4.3 CSV 和 JSON结构化的两条路当数据不是给人看而是给程序读时就要用结构化格式。CSV 适合表格型数据JSON 适合嵌套的字典列表。写 CSV 用csv模块别自己拼逗号。因为字段里如果有逗号或引号自己拼会破坏格式。import csv rows [[姓名, 分数], [张三, 92.5], [李四, 88.0]] with open(scores.csv, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerows(rows)注意newline这个参数。在 Windows 上不加它写出的 CSV 每行之间会多一个空行。这是csv模块的官方建议写法。读 CSV 同理with open(scores.csv, r, encodingutf-8, newline) as f: reader csv.reader(f) for row in reader: print(row)JSON 用json模块dumps/loads处理字符串dump/load处理文件。import json data {name: 张三, scores: [92.5, 88.0]} with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)ensure_asciiFalse让中文原样输出而不是转成\uXXXXindent2让格式带缩进便于阅读。这两个参数在实际项目里几乎必加。5. 完整实操一个词频统计与格式化输出的脚本5.1 需求拆解假设测验要求读一个英文文本文件统计每个单词出现的次数按次数从高到低输出前 10 个格式化成对齐的表格同时把结果写进一个 CSV 文件。这个需求把读取、处理、格式化、写出全串起来了。5.2 分步实现第一步读取并清洗。单词统计要先统一大小写、去掉标点。import re from collections import Counter from pathlib import Path import csv def load_words(path): text Path(path).read_text(encodingutf-8) text text.lower() words re.findall(r[a-z], text) return words这里用正则[a-z]提取单词允许撇号是为了处理dont这类词。Counter是统计频次的利器一行搞定。第二步统计并排序。def top_words(words, n10): counter Counter(words) return counter.most_common(n)most_common(n)直接返回按频次降序的前 n 个内部做了排序不用自己写。第三步格式化输出。def print_table(items): print(f{排名:6}{单词:15}{次数:6}) print(- * 27) for rank, (word, count) in enumerate(items, 1): print(f{rank:6}{word:15}{count:6})第四步写出 CSV。def save_csv(items, path): with open(path, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([排名, 单词, 次数]) for rank, (word, count) in enumerate(items, 1): writer.writerow([rank, word, count])第五步串起来。if __name__ __main__: words load_words(input.txt) items top_words(words, 10) print_table(items) save_csv(items, result.csv)5.3 参数与细节说明enumerate(items, 1)的第二个参数是起始值默认从 0 开始这里从 1 开始更符合“排名”的语义。most_common在频次相同时的顺序是不保证的如果需要稳定排序得自己用sorted加 key。sorted_items sorted(counter.items(), keylambda x: (-x[1], x[0]))[:10]这个写法先按频次降序频次相同再按单词字母升序结果可复现。测验里如果对顺序有要求用这个更稳。6. 常见问题与排查技巧实录6.1 报错速查表报错常见原因解决方向FileNotFoundError路径错、文件不存在检查路径用Path.exists()确认UnicodeDecodeError编码不匹配指定正确 encoding 或探测编码PermissionError文件被占用或无权限关闭占用程序检查权限IsADirectoryError把目录当文件打开确认路径指向文件ValueError: I/O operation on closed file在 with 外使用文件对象把操作放进 with 块内6.2 几个文档里不写的经验第一写文件时先写临时文件再重命名。如果你要覆盖一个重要文件直接w打开有风险——写到一半程序崩了原文件已经没了。稳妥做法是写到xxx.tmp写完后os.replace覆盖原文件。os.replace在大多数平台上是原子操作要么成功要么原文件不变。第二读大文件时注意内存。前面说过迭代优于readlines。但如果你需要随机访问行可以考虑用linecache模块它按需缓存比全读进内存省。第三CSV 的编码陷阱。用 Excel 打开 UTF-8 的 CSV 有时会乱码因为 Excel 默认按本地编码解析。解决办法是写 CSV 时加 BOM即用encodingutf-8-sig。这样 Excel 能正确识别其他程序读也没问题。第四JSON 不支持所有 Python 类型。datetime、set、自定义对象都不能直接json.dump。要么先转成字符串或列表要么写自定义的default函数。import json from datetime import datetime def default_handler(obj): if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, set): return list(obj) raise TypeError(f无法序列化 {type(obj)}) json.dumps({time: datetime.now()}, defaultdefault_handler)第五格式化时注意浮点精度。f{0.1 0.2:.2f}得到0.30但0.1 0.2本身是0.30000000000000004。涉及金额计算时用decimal.Decimal而不是 float避免累积误差。6.3 调试文件操作的小技巧当你怀疑文件内容不对时别急着改代码先打印出来看。但直接print(content)可能因为内容太长刷屏。我的习惯是打印长度和前几百个字符。print(f长度: {len(content)}) print(f前200字符: {content[:200]!r})!r会显示转义字符能让你看到\n、\t这些不可见字符排查格式问题特别有用。另一个技巧是用repr()看每一行的真实内容尤其是怀疑有隐藏字符时。for i, line in enumerate(f): if i 5: print(repr(line))这样能立刻发现行尾是不是多了\r或者有没有不可见的 BOM 字符。7. 把测验变成能力后续可以怎么练做完这道测验如果你只是把答案提交了就关掉那收获有限。我建议做三件事把它变成真正的能力。第一把脚本改成能处理命令行参数。用argparse让输入文件和输出文件可配置而不是写死在代码里。这一步跨过去你的脚本就从“练习”变成了“工具”。import argparse parser argparse.ArgumentParser() parser.add_argument(input, help输入文件路径) parser.add_argument(-o, --output, defaultresult.csv) parser.add_argument(-n, typeint, default10) args parser.parse_args()第二加上异常处理让脚本在文件不存在、编码错误时给出友好提示而不是一堆 traceback。这对实际使用体验提升很大。第三把处理逻辑拆成函数每个函数只做一件事。这样你下次遇到类似需求可以直接复用其中某几个函数而不是从头写。我个人在实际操作中的体会是文件操作和数据格式化这两块看十遍不如动手写一遍。但写的时候一定要带着“如果这个文件很大怎么办”“如果编码不对怎么办”“如果写到一半崩了怎么办”这三个问题去写。带着问题写出来的代码和照着答案抄出来的代码差距在下一个项目里就会显现出来。