资讯详情 Python文件操作避坑指南:从open()到csv读写与编码实战
📅 2026/10/20 9:17:19
最近帮一位刚入门Python的朋友调脚本他照着网上的教程写文件读写结果光一个“中文乱码”就折腾了一晚上紧接着又踩了“文件不存在”和“路径找不到”的连环坑。这事让我觉得文件读写虽然看起来是Python最基础的一课但实际写起来新手该踩的坑一个都不会少。这正好也回答了一个很常见的问题Python文件操作到底应该怎么学、怎么用才能既看懂又能拿来干活这篇文章我就以实际写代码的方式把读写txt文件和csv文件的完整路径走一遍。从最常用的open()函数讲起到中文编码、文件路径、csv模块的读写、大文件按行处理再到真机运行时的报错排查和几个实用小技巧。文章里出现的坑基本都是新手最容易遇到的代码也都可以直接复制去跑。适合刚学Python语法、准备处理本地数据文件的人参考也适合作为快速翻查的操作笔记。1. txt文件读取open()函数的模式选择与with语句的规矩1.1 先搞清楚open()的几个基本参数Python读写文件的入口百分之九十的场景都是open()这个内置函数。新手最容易忽略的是它的第二个参数——打开模式。我先把最常用的几个模式列出来后面写代码的时候你会一直用到它们。# 读取模式默认文件不存在会直接报错 f open(data.txt, r) # 写入模式文件存在会清空重写不存在会新建 f open(data.txt, w) # 追加模式文件存在则在末尾追加不存在会新建 f open(data.txt, a) # 读写模式可读可写不会清空文件 f open(data.txt, r)这几个模式的区别我用生活中的例子解释一下r就像你去图书馆借书书不存在你当然借不到w像拿着新本子写笔记翻开第一页之前先把旧内容全撕掉a像在便签本末尾持续加新页前面的内容不动r则像在草稿纸上边看边改。读模式还有一个比较隐蔽的细节文件打开后默认是按照文本格式读的如果你要操作图片、压缩包、二进制数据必须加一个b后缀变成rb、wb。处理txt和csv用普通文本模式就够了但遇到二进制文件时千万别忘了b否则读出来的内容会是一个乱掉的字符串而不是干净的字节数据。1.2 为什么我坚持用with语句管理文件很多老教程会这么写f open(data.txt, r, encodingutf-8) content f.read() f.close()这种写法本身没有语法错误但它有一个隐患如果f.read()执行过程中抛出异常程序就会跳过后面的f.close()文件句柄一直占着不释放。在Windows上你会发现文件被占用想删都删不掉。而with语句能保证异常发生时也自动关闭文件省心不少。with open(data.txt, r, encodingutf-8) as f: content f.read() # 到这里文件已经自动关闭了这段代码看起来只是把open()放进with里实际含义是“进入一个上下文管理块出了这个块文件自动关闭”。你可以把它理解成进房间之前先登记出房间后系统会自动帮你锁门哪怕房间里突然着火抛异常门也会被强制关上。推荐的写法就是with这也几乎是现在所有正经Python项目里的通用约定。1.3 文件读取的三种方式read、readline、readlines拿到文件对象后读取内容主要有三个方法read()一次全部读成一个大字符串适合小文件readline()每次只读一行适合边读边处理readlines()一次读成列表每行是一个元素。with open(data.txt, r, encodingutf-8) as f: # 1. 一次性读取全部内容 content f.read() print(content) with open(data.txt, r, encodingutf-8) as f: # 2. 只读第一行 first_line f.readline() print(first_line) with open(data.txt, r, encodingutf-8) as f: # 3. 读取所有行为列表 lines f.readlines() print(lines[:3])很多人一开始会把readlines()和readline()搞混其实只要记住一个规律带s的是复数返回列表不带s的是单数返回一行字符串。对于新手来说读小文件直接用read()最方便但要处理几十MB甚至几百MB的日志文件时read()会把整个文件塞进内存极容易把电脑卡死。遇到这种情况正确的做法是用for line in f来逐行迭代这部分我放到后面的“大文件处理”里专门说。2. 中文乱码与文件路径新手最容易踩的两个隐形坑2.1 乱码问题背后的编码真相中文乱码几乎可以说是Python文件操作新手村的第一大Boss。你明明用记事本写好了name: 张三程序读出来却变成name: 寮犱笁或者干脆报一个UnicodeDecodeError。这不是Python本身的问题而是读文件时用的编码和写文件时用的编码不一致。在Windows上记事本默认保存txt文件的编码是GBK部分新版系统默认UTF-8而Python的open()如果不指定encoding参数在Windows下会用系统默认编码GBK在Linux和macOS下会用UTF-8。这就导致一个很尴尬的情况你在Windows上写好的代码放到服务器上跑读同一个文件结果不一样。解决方案很简单读写文件时统一显式指定编码绝大多数场景用UTF-8就够了with open(data.txt, r, encodingutf-8) as f: content f.read()如果源文件确实是GBK编码而你又必须读它就指定encodinggbk。如果你不确定文件是什么编码用chardet库去探测是一种办法但说实话日常处理自己生成的文件直接用UTF-8规范写、规范读可以避免掉九成以上的乱码问题。2.2 文件路径绝对路径、相对路径和当前工作目录另一个经典事故代码明明写在桌面文件也在桌面代码里写的是open(data.txt, r)结果运行时报FileNotFoundError。原因是Python默认从当前工作目录找文件而不是从代码文件所在的目录找。如果你用某个编辑器直接运行脚本工作目录是命令行当前目录如果你用集成开发环境运行工作目录可能是项目配置的根目录不一定就是代码文件所在目录。排查这个问题第一件事就是打印当前工作目录import os print(os.getcwd())打印出来的路径才是open(data.txt, r)真正查找文件的起始目录。想让代码稳定工作一个比较省心的方案是用绝对路径或者用pathlib基于代码文件位置构造路径from pathlib import Path # 获取当前代码文件所在目录 base_dir Path(__file__).parent file_path base_dir / data / data.txt with open(file_path, r, encodingutf-8) as f: content f.read()这样不管在哪个目录启动程序Path(__file__).parent都能定位到当前文件所在目录再往下拼接“data.txt”就有了确定性。对于经常被文件路径折磨的朋友我强烈建议从一开始就养成用Path对象写路径的习惯它比一堆os.path.join()更直观也天然兼容Windows和Linux的路径风格。2.3 路径分隔符的隐藏问题Windows路径分隔符是反斜杠\Linux和macOS是正斜杠/。在Python字符串里反斜杠又是转义符所以新手经常写出C:\Users\name\data.txt这种代码运行后直接报错或者路径被截断。解决办法有几种用正斜杠C:/Users/name/data.txt、用原始字符串rC:\Users\name\data.txt或者干脆用pathlib.Path自动处理。我推荐最后一种因为Path会自动适配当前系统代码跨平台不用改一行。3. 写入txt文件write与writelines的差异以及换行符的那些事3.1 写入模式选择与基本写法写入文件看起来简单但有两个细节经常让新手翻车。第一个是模式选择。w模式每次打开都会清空原文件如果你只是想追加内容却用了w之前的数据就全没了。想追加要用a模式。举个典型场景写日志程序每次运行往同一个日志文件里加一条记录就应该用a。with open(log.txt, a, encodingutf-8) as f: f.write(2025-05-01 10:00:00 用户登录成功\n)第二个是换行符问题。write()不会帮你在末尾加换行需要手动写\n。你可能会想那writelines()总该自动换行了吧答案是“并不会”。writelines()只是把列表里的字符串依次写入文件不会主动插入换行符每个字符串必须在设计时就自带\nlines [第一行\n, 第二行\n, 第三行\n] with open(output.txt, w, encodingutf-8) as f: f.writelines(lines)如果列表里的字符串没有换行符最好用列表推导式统一补上。这个坑极隐蔽因为writelines这个函数名太容易让人误会它“按行写入、自动换行”实际上它就是个批量write。3.2 用join还是循环写性能与可读性的平衡写入多行时有人喜欢用\n.join(lines)拼成一个字符串再write也有人喜欢用循环一行行write。对于少量数据两者没差别但数据量上万时一次写入比循环多次写入性能更好因为减少了系统调用次数。lines [fuser_{i} for i in range(10000)] # 方式一一次性写入 with open(users.txt, w, encodingutf-8) as f: f.write(\n.join(lines) \n) # 方式二循环写入 with open(users.txt, w, encodingutf-8) as f: for line in lines: f.write(line \n)我不建议普通脚本在这种场景里过度纠结性能但“能一次写完就绝不循环写”这个习惯在处理大量数据时确实能省下可观时间。你可以把它当成一个默认选择。3.3 写入后立刻读取缓冲区与flush的机制还有一个容易让新手困惑的现象代码里写文件后紧接着用另一个程序打开文件看发现内容是空的。这是因为Python的文件写入有缓冲区数据先攒在内存里只有缓冲区满了、文件关闭或手动调用flush()时才会真正写到磁盘。使用with语句时退出语句块会自动关闭文件并刷写缓冲所以代码执行完再看文件内容肯定是完整的。如果你在写文件的中途需要强制把当前数据写到磁盘就调用f.flush()。在长时间运行的脚本里比如一个持续写日志的程序定期flush()很重要否则进程突然崩溃缓冲区里的日志全丢了。这点放到实战中体会更深。4. csv模块的正确打开方式读取与写入的完整套路4.1 csv不是简单的字符串拆分为什么别用split处理很多新手拿到csv文件第一反应是f.read()之后用split(,)去切分。对简单数据可能凑合能行但一旦字段内容里出现逗号比如张三, 产品经理程序员就会输出错误分列。csv格式本身有一套转义规则字段含逗号、换行、引号时会用双引号包裹并把这个双引号写成两个连续的引号。要正确解析这种规则靠手工split是不行的必须用Python标准库里的csv模块。import csv with open(users.csv, r, encodingutf-8, newline) as f: reader csv.reader(f) for row in reader: print(row)这里有个细节特别提醒用csv.reader读取文件时open()的newline参数要设置为空字符串。如果不设置文件里正常的换行可能被自动翻译导致读出多余空行。这是官方文档明确建议的写法虽然原因涉及行结束符的自动处理机制但新手只要记住“读写csv文件时newline是标配”就行了。4.2 用DictReader和DictWriter处理带表头的数据更爽实际工作中大部分csv文件都有表头比如第一行是id,name,age。用csv.DictReader可以把每一行读取成一个字典键就是表头这样后续按字段名取值代码可读性大大提高。with open(students.csv, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: print(row[id], row[name], row[age])写csv文件时也有对应的csv.DictWriter它会根据你指定的字段名自动写入表头和字典的值with open(output.csv, w, encodingutf-8, newline) as f: fieldnames [id, name, age] writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerow({id: 1, name: 张三, age: 20}) writer.writerow({id: 2, name: 李四, age: 25})这种写法的好处是思路清晰先定字段再写表头最后按字典写数据不会出现字段对应错位的问题。如果你的数据本来就是一个字典列表用DictWriter几乎是一行一行地搬进去非常顺。4.3 Excel打开csv中文乱码utf-8-sig的用途用Python写出的csv文件用记事本打开一切正常但用Excel打开时中文全都变成乱码。这是因为Excel在打开csv文件时默认按系统本地编码来解析而Windows中文系统的本地编码是GBK不认UTF-8。解决办法是写文件时用带BOM的UTF-8编码也就是utf-8-sigwith open(output.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([姓名, 年龄]) writer.writerow([张三, 20])在utf-8-sig模式下Python会在文件开头写入一个不可见的字节序标记Excel看到它就知道这个文件是UTF-8编码中文就能正常显示了。这是我在实际项目中处理“给非技术同事发csv”时必用的一招你如果以后需要把数据交给运营或财务同学没有这行编码设置对方大概率会打来电话问“这个文件怎么是乱码”。4.4 分隔符不只是逗号制表符、分号与自定义分隔符不是所有的csv文件都用逗号做分隔符。有些旧系统导出的文件用分号;还有些数据文件用制表符\t。csv模块允许你指定任意分隔符with open(data.tsv, r, encodingutf-8, newline) as f: reader csv.reader(f, delimiter\t) for row in reader: print(row)这里也解释了一个多年困惑为什么叫“csv”却还能读制表符分隔的“tsv”。因为delimiter参数就是用来干这个的你不传就是默认逗号传了就能处理所有类似格式。判断一个文件的实际分隔符我通常用记事本打开文件数一下每行有几个连续空格或者符号再决定delimiter传什么这个办法笨但有效。5. 大文件与性能逐行读取、统计与流式处理的实战经验5.1 一次性read()在多大文件时会出问题很多人写文件读取习惯都是read()一把梭。当文件是几千行时完全没问题到了几万行勉强也能跑但当你面对一个几个GB的日志文件时read()会一次性把整个文件加载到内存电脑内存小的直接卡死或者报MemoryError。我自己处理过一批第三方接口返回的报表文件单文件就有1.2GB当时用集成开发环境直接跑f.read()内存占用飙到接近3GB机器风扇狂转吓得我立刻关掉了进程。从那以后我判断是否可以用read()的标准很简单如果文件大小小于100MB随便用超过这个量老老实实按行处理。5.2 三种逐行读取方式的性能对比逐行读取有几种写法最容易理解的是readline()配合循环更推荐的写法是直接for line in f因为文件对象本身就是一个迭代器Python会在内部做缓冲读取代码更简洁性能也更好。# 方式一readline循环 with open(big.txt, r, encodingutf-8) as f: while True: line f.readline() if not line: break print(line.strip()) # 方式二for迭代推荐 with open(big.txt, r, encodingutf-8) as f: for line in f: print(line.strip())第二种写法的内存占用始终保持在单行大小附近无论文件多大都不会把全部内容载入内存。如果你还希望一次读取指定行数可以用islice但这属于进阶操作新手暂时用不上。核心记忆点就一句话loop over the file object, not over readlines。很多老手写大文件处理时默认就是这个写法。5.3 实操案例统计一个csv文件的行数、某列求和、筛选合格记录把几个知识串起来我写一个典型的大文件处理案例统计一个csv文件里所有用户的年龄总和并筛选出年龄大于30的用户。这个需求不复杂但很能说明逐行处理的套路。import csv total_age 0 count 0 qualified [] with open(users.csv, r, encodingutf-8, newline) as f: reader csv.reader(f) header next(reader) # 跳过表头 for row in reader: # 假设姓名在第1列年龄在第2列 name row[0] age int(row[1]) total_age age count 1 if age 30: qualified.append(name) print(f总人数: {count}, 平均年龄: {total_age / count:.1f}) print(f年龄大于30的用户: {qualified})同样处理几百万行数据时这份代码不会把整个表格装进内存reader每次迭代只生成一行数据内存占用量基本恒定。如果你要写一个批处理脚本去清洗数据这个模式足够应对绝大多数场景。6. 常见报错与排查思路FileNotFoundError、UnicodeDecodeError以及几个实用小技巧6.1 FileNotFoundError的排查链路这是我见过最多新手的报错。遇到FileNotFoundError先冷静三秒按下面的顺序排查打印当前工作目录确认你的脚本到底在哪个目录下运行用Path(文件路径).exists()检查目标文件是否存在检查文件名是否大小写写错Windows不区分大小写但Linux区分检查路径里是否有中文或空格这类情况建议换英文目录名。from pathlib import Path path Path(data/data.txt) print(当前目录:, Path.cwd()) print(文件存在?, path.exists()) if path.exists(): with open(path, r, encodingutf-8) as f: print(f.read()) else: print(文件路径不正确请检查目录结构)排查这类问题最忌讳的就是对着报错猜来猜去把这三个信息打印出来基本能锁定问题所在。6.2 UnicodeDecodeError与UnicodeEncodeError的分辨UnicodeDecodeError是读文件时编码不匹配UnicodeEncodeError是写文件时编码不支持某个字符。比如你用UTF-8读一个GBK编码的中文文件就可能因为某个中文字符无法被正确解码而报UnicodeDecodeError。反过来写入时如果指定的编码不支持某些特殊字符比如试图用ASCII编码写中文就会报UnicodeEncodeError。处理这类问题时你只需要确认两个信息文件实际是什么编码创建、你要用什么编码读取。如果你控制不了文件来源比如别人从Excel导出的csv大概率是GBK那你读取时就指定encodinggbk如果读出来乱码换成encodinggbk再试一次往往就能解决。自己写文件则统一指定utf-8需要发给Excel用户的用utf-8-sig。6.3 csv读写时常见的Surprising空行问题如果你写csv后发现每两行之间多了一个空行十有八九是open()时没加newline。这个问题很多初学者会以为是数据的问题折腾半天其实核心就是参数没写对。把它和encodingutf-8一样当成写csv文件时不可省略的标配即可。6.4 实战技巧临时文件、文件存在性检查和追加写日志最后分享几个我日常写脚本时一直在用的小技巧也算不上高深但确实省过不少时间。第一个是用临时文件保护数据。如果你要修改一个重要的csv文件不要直接在原文件上写而是先写到一个临时文件全部写完后用os.replace()原子替换原文件这样即使进程中途崩溃也不会把原文件写坏。import os import csv input_file data.csv tmp_file data.tmp.csv with open(input_file, r, encodingutf-8, newline) as fin, \ open(tmp_file, w, encodingutf-8, newline) as fout: reader csv.reader(fin) writer csv.writer(fout) for row in reader: if row: # 跳过空行 writer.writerow(row) os.replace(tmp_file, input_file)第二个是统一封装一个读写函数。当你发现自己反复写with open(..., encodingutf-8)时可以封装成一个小函数把编码处理细节收进去让业务代码更清爽。这个见仁见智但对新手来说先直接写with open反而能加深印象封装不必太早。第三个是日志文件的追加写入。写日志时用a模式加时间戳并记得写入后调用flush()保证日志能及时落盘。这些细节在调试线上问题时能救命。6.5 从入门到能干活你只需要记住这四件事文件操作在Python里是一个覆盖面很大、但核心思路很固定的领域。反过来说想让代码稳定运行你需要记住的其实就四件事读写文件时显式指定编码使用with语句管理文件生命周期处理csv使用标准库csv模块而不是手工split处理大文件用迭代而不是一次性读入。这四件事做对百分之九十五的日常文件处理任务都不会出大问题。另外还有一个经验分享给纠结“为什么我学了很多却写不出程序”的朋友文件操作这类基础能力不要追求一次性看完所有细节而是带着任务去写。比如今天就写一个“把csv里的数据按某列排序并输出新文件”明天写一个“读取日志统计error出现的行数”。当你为了完成具体目标去翻文档、去调试时那些坑才会真正变成你的经验。最后送上一个我用了很久的小习惯写完文件处理脚本后先拿一个几百行的小文件测试再拿真实大文件跑同时观察内存占用和耗时。没事就在代码里打印几行关键位置的进度信息方便定位到底是哪一行卡住了。遇到看不懂的报错把完整错误信息复制出来拆开看是哪一行、什么类型、什么原因这个习惯比背任何教程都有用。