053、文件读写那些坑open 的模式、编码检测、大文件分块与上下文安全一个让我加班到凌晨两点的bug去年接手一个数据清洗项目客户给了一堆CSV文件说是“标准UTF-8编码”。我随手写了个循环读取本地测试一切正常。上线后第三天运维半夜打电话说程序崩了——某个文件读到一半直接抛出UnicodeDecodeError整条流水线中断数据丢失了将近两万条。我远程连上去一看那个文件开头几个字节是\xff\xfeBOM头标记的是UTF-16 LE。客户所谓的“标准UTF-8”其实是Excel另存为时默认的“带BOM的UTF-8”而中间混入了一个从老旧系统导出的UTF-16文件。更致命的是我用了with open(file, r, encodingutf-8)硬编码了编码方式遇到不匹配直接炸。从那以后我写文件读写代码都会多问自己一句这个文件真的像它看起来那样吗open 的模式你以为你懂其实你只懂一半open()的第二个参数大多数人只会用r、w、a。但实际项目中模式组合才是真正的坑。二进制模式与文本模式的混用# 别这样写——Windows下会出鬼withopen(data.bin,r)asf:dataf.read()Windows系统下文本模式会自动把\r\n转成\n。如果你读的是二进制文件图片、压缩包、pickle序列化数据这种转换会破坏数据完整性。正确的做法是# 二进制文件必须用b模式withopen(image.jpg,rb)asf:raw_bytesf.read()读写混合模式r、w、a这三个模式我见过太多人用错。简单记一个原则r文件必须存在指针在开头可以读也可以写。但写的时候会覆盖原有内容不是追加。w文件不存在就创建存在就清空。可以读但读到的内容是你刚写进去的。a文件不存在就创建指针在末尾。读的时候需要先seek(0)否则读不到任何东西。# 踩过坑的写法想用r在文件末尾追加withopen(log.txt,r)asf:f.write(new line\n)# 这行会写在文件开头覆盖原有内容正确的追加方式是用a或a或者先seek(0, 2)把指针移到末尾。容易被忽略的x模式x模式独占创建是我最近才养成习惯用的。它只在文件不存在时创建并写入如果文件已存在直接抛FileExistsError。这在多进程写日志、缓存文件生成时特别有用避免两个进程同时写同一个文件导致数据混乱。try:withopen(output.txt,x)asf:f.write(独占写入)exceptFileExistsError:# 这里可以处理冲突比如重命名或跳过pass编码检测别信文件名信字节回到开头的故事。硬编码编码方式就像在赌桌上押全部身家。正确的做法是检测文件的实际编码。chardet 库的正确用法chardet是Python生态里最常用的编码检测库但它有个坑检测小文件时准确率极低。importchardet# 错误示范只读前100字节就判断编码withopen(unknown.csv,rb)asf:rawf.read(100)resultchardet.detect(raw)encodingresult[encoding]# 这里大概率是ascii实际可能是utf-8正确的做法是读取足够多的样本至少几千字节defdetect_encoding(file_path,sample_size10000):withopen(file_path,rb)asf:rawf.read(sample_size)resultchardet.detect(raw)# chardet返回的confidence是0到1之间的置信度ifresult[confidence]0.8:# 置信度太低可能需要人工介入或尝试常见编码# 这里踩过坑有些文件混合了多种编码returnutf-8# 回退到最通用的编码returnresult[encoding]BOM头的处理Windows生成的UTF-8文件经常带BOM头\xef\xbb\xbf。Python的open()函数不会自动处理BOM需要手动跳过或使用utf-8-sig编码# 自动处理BOM头withopen(excel_export.csv,r,encodingutf-8-sig)asf:# BOM头会被自动忽略不会出现在读取的内容中contentf.read()utf-8-sig是Python特有的编码别名它会在读取时自动跳过BOM头写入时自动添加BOM头。如果你需要兼容Excel写入时用这个编码最省心。大文件分块别让内存爆炸处理几百MB甚至GB级别的文件时f.read()直接读取全部内容到内存是自杀行为。我见过一个同事用readlines()读2GB的日志文件服务器直接OOM被kill。逐行读取的陷阱# 看似安全的逐行读取其实有隐患withopen(huge_file.log,r)asf:forlineinf:process(line)这个写法本身没问题Python的文件对象是迭代器内部会按行缓冲读取。但问题在于如果某一行特别长比如一个JSON对象被压缩成一行这一行仍然会占用大量内存。# 更安全的做法按固定字节块读取defread_in_chunks(file_path,chunk_size1024*1024):withopen(file_path,rb)asf:whileTrue:chunkf.read(chunk_size)ifnotchunk:breakyieldchunk# 使用示例forchunkinread_in_chunks(huge_file.bin):process_chunk(chunk)处理超大文本文件时的行分割按块读取二进制文件简单但处理文本文件时一个块可能切断了某行。需要自己处理行边界defread_lines_in_chunks(file_path,chunk_size1024*1024):withopen(file_path,r,encodingutf-8)asf:bufferwhileTrue:chunkf.read(chunk_size)ifnotchunk:ifbuffer:yieldbufferbreakbufferchunk# 按换行符分割保留最后一个不完整的行linesbuffer.split(\n)forlineinlines[:-1]:yieldline\nbufferlines[-1]这个写法有个细节split(\n)会丢失换行符所以yield的时候要补回来。如果你需要保留原始换行符比如处理CSV时可以用splitlines(True)。上下文安全with 不是万能药with open()是Python最优雅的语法糖之一但它并不能解决所有资源管理问题。多个文件的上下文管理# 同时打开两个文件用with嵌套withopen(source.txt,r)assrc:withopen(dest.txt,w)asdst:forlineinsrc:dst.write(line)Python 3.1支持在一个with语句中打开多个文件# 更简洁的写法withopen(source.txt,r)assrc,open(dest.txt,w)asdst:forlineinsrc:dst.write(line)自定义上下文管理器有时候你需要管理的不是文件而是数据库连接、网络socket等资源。可以自己实现上下文管理器classManagedFile:def__init__(self,filename,mode):self.filenamefilename self.modemode self.fileNonedef__enter__(self):self.fileopen(self.filename,self.mode)returnself.filedef__exit__(self,exc_type,exc_val,exc_tb):ifself.file:self.file.close()# 返回False会传播异常返回True会抑制异常# 这里踩过坑不要轻易返回True会吞掉异常returnFalse异常处理与资源释放with语句保证即使发生异常__exit__也会被调用。但有个细节如果在__enter__中发生异常__exit__不会被调用。# 危险的写法try:withopen(可能不存在的文件.txt,r)asf:dataf.read()exceptFileNotFoundError:# 这里没问题with已经处理了资源释放pass但如果open()本身抛异常比如权限不足文件对象根本没创建也就不需要释放。with语句的设计已经考虑到了这一点。个人经验性建议永远不要信任文件扩展名和文件名。.csv文件可能是Excel导出的带BOM的UTF-16.txt文件可能是GBK编码。写代码时先检测编码或者提供一个可配置的编码参数。大文件处理时先估算内存占用。一个简单的公式文件大小 × 编码膨胀系数UTF-8中文约3倍≈ 内存占用。如果超过可用内存的30%考虑分块处理。写日志文件时用a模式而不是w。我见过太多人用w模式写日志每次重启程序就把之前的日志清空了。如果担心日志文件太大配合logging模块的RotatingFileHandler使用。测试文件读写时一定要测试边界情况空文件、只有一行、只有换行符、包含特殊字符如\x00、文件被其他进程锁定。这些情况在单元测试中很容易被忽略但生产环境一定会遇到。最后一条也是最重要的一条写文件时先写入临时文件再重命名。这样即使写入过程中程序崩溃也不会破坏原始文件。这个习惯救过我很多次。importosimporttempfiledefsafe_write(filename,content):# 先写入临时文件tmptempfile.NamedTemporaryFile(modew,deleteFalse,diros.path.dirname(filename),prefixtmp_,suffix.tmp)try:tmp.write(content)tmp.close()# 原子操作重命名os.replace(tmp.name,filename)except:os.unlink(tmp.name)raise文件读写看起来是Python最基础的操作但恰恰是这些基础操作在线上环境最容易出问题。希望这篇笔记能帮你少踩几个坑。
053、文件读写那些坑:open 的模式、编码检测、大文件分块与上下文安全
发布时间:2026/6/26 12:32:24
053、文件读写那些坑open 的模式、编码检测、大文件分块与上下文安全一个让我加班到凌晨两点的bug去年接手一个数据清洗项目客户给了一堆CSV文件说是“标准UTF-8编码”。我随手写了个循环读取本地测试一切正常。上线后第三天运维半夜打电话说程序崩了——某个文件读到一半直接抛出UnicodeDecodeError整条流水线中断数据丢失了将近两万条。我远程连上去一看那个文件开头几个字节是\xff\xfeBOM头标记的是UTF-16 LE。客户所谓的“标准UTF-8”其实是Excel另存为时默认的“带BOM的UTF-8”而中间混入了一个从老旧系统导出的UTF-16文件。更致命的是我用了with open(file, r, encodingutf-8)硬编码了编码方式遇到不匹配直接炸。从那以后我写文件读写代码都会多问自己一句这个文件真的像它看起来那样吗open 的模式你以为你懂其实你只懂一半open()的第二个参数大多数人只会用r、w、a。但实际项目中模式组合才是真正的坑。二进制模式与文本模式的混用# 别这样写——Windows下会出鬼withopen(data.bin,r)asf:dataf.read()Windows系统下文本模式会自动把\r\n转成\n。如果你读的是二进制文件图片、压缩包、pickle序列化数据这种转换会破坏数据完整性。正确的做法是# 二进制文件必须用b模式withopen(image.jpg,rb)asf:raw_bytesf.read()读写混合模式r、w、a这三个模式我见过太多人用错。简单记一个原则r文件必须存在指针在开头可以读也可以写。但写的时候会覆盖原有内容不是追加。w文件不存在就创建存在就清空。可以读但读到的内容是你刚写进去的。a文件不存在就创建指针在末尾。读的时候需要先seek(0)否则读不到任何东西。# 踩过坑的写法想用r在文件末尾追加withopen(log.txt,r)asf:f.write(new line\n)# 这行会写在文件开头覆盖原有内容正确的追加方式是用a或a或者先seek(0, 2)把指针移到末尾。容易被忽略的x模式x模式独占创建是我最近才养成习惯用的。它只在文件不存在时创建并写入如果文件已存在直接抛FileExistsError。这在多进程写日志、缓存文件生成时特别有用避免两个进程同时写同一个文件导致数据混乱。try:withopen(output.txt,x)asf:f.write(独占写入)exceptFileExistsError:# 这里可以处理冲突比如重命名或跳过pass编码检测别信文件名信字节回到开头的故事。硬编码编码方式就像在赌桌上押全部身家。正确的做法是检测文件的实际编码。chardet 库的正确用法chardet是Python生态里最常用的编码检测库但它有个坑检测小文件时准确率极低。importchardet# 错误示范只读前100字节就判断编码withopen(unknown.csv,rb)asf:rawf.read(100)resultchardet.detect(raw)encodingresult[encoding]# 这里大概率是ascii实际可能是utf-8正确的做法是读取足够多的样本至少几千字节defdetect_encoding(file_path,sample_size10000):withopen(file_path,rb)asf:rawf.read(sample_size)resultchardet.detect(raw)# chardet返回的confidence是0到1之间的置信度ifresult[confidence]0.8:# 置信度太低可能需要人工介入或尝试常见编码# 这里踩过坑有些文件混合了多种编码returnutf-8# 回退到最通用的编码returnresult[encoding]BOM头的处理Windows生成的UTF-8文件经常带BOM头\xef\xbb\xbf。Python的open()函数不会自动处理BOM需要手动跳过或使用utf-8-sig编码# 自动处理BOM头withopen(excel_export.csv,r,encodingutf-8-sig)asf:# BOM头会被自动忽略不会出现在读取的内容中contentf.read()utf-8-sig是Python特有的编码别名它会在读取时自动跳过BOM头写入时自动添加BOM头。如果你需要兼容Excel写入时用这个编码最省心。大文件分块别让内存爆炸处理几百MB甚至GB级别的文件时f.read()直接读取全部内容到内存是自杀行为。我见过一个同事用readlines()读2GB的日志文件服务器直接OOM被kill。逐行读取的陷阱# 看似安全的逐行读取其实有隐患withopen(huge_file.log,r)asf:forlineinf:process(line)这个写法本身没问题Python的文件对象是迭代器内部会按行缓冲读取。但问题在于如果某一行特别长比如一个JSON对象被压缩成一行这一行仍然会占用大量内存。# 更安全的做法按固定字节块读取defread_in_chunks(file_path,chunk_size1024*1024):withopen(file_path,rb)asf:whileTrue:chunkf.read(chunk_size)ifnotchunk:breakyieldchunk# 使用示例forchunkinread_in_chunks(huge_file.bin):process_chunk(chunk)处理超大文本文件时的行分割按块读取二进制文件简单但处理文本文件时一个块可能切断了某行。需要自己处理行边界defread_lines_in_chunks(file_path,chunk_size1024*1024):withopen(file_path,r,encodingutf-8)asf:bufferwhileTrue:chunkf.read(chunk_size)ifnotchunk:ifbuffer:yieldbufferbreakbufferchunk# 按换行符分割保留最后一个不完整的行linesbuffer.split(\n)forlineinlines[:-1]:yieldline\nbufferlines[-1]这个写法有个细节split(\n)会丢失换行符所以yield的时候要补回来。如果你需要保留原始换行符比如处理CSV时可以用splitlines(True)。上下文安全with 不是万能药with open()是Python最优雅的语法糖之一但它并不能解决所有资源管理问题。多个文件的上下文管理# 同时打开两个文件用with嵌套withopen(source.txt,r)assrc:withopen(dest.txt,w)asdst:forlineinsrc:dst.write(line)Python 3.1支持在一个with语句中打开多个文件# 更简洁的写法withopen(source.txt,r)assrc,open(dest.txt,w)asdst:forlineinsrc:dst.write(line)自定义上下文管理器有时候你需要管理的不是文件而是数据库连接、网络socket等资源。可以自己实现上下文管理器classManagedFile:def__init__(self,filename,mode):self.filenamefilename self.modemode self.fileNonedef__enter__(self):self.fileopen(self.filename,self.mode)returnself.filedef__exit__(self,exc_type,exc_val,exc_tb):ifself.file:self.file.close()# 返回False会传播异常返回True会抑制异常# 这里踩过坑不要轻易返回True会吞掉异常returnFalse异常处理与资源释放with语句保证即使发生异常__exit__也会被调用。但有个细节如果在__enter__中发生异常__exit__不会被调用。# 危险的写法try:withopen(可能不存在的文件.txt,r)asf:dataf.read()exceptFileNotFoundError:# 这里没问题with已经处理了资源释放pass但如果open()本身抛异常比如权限不足文件对象根本没创建也就不需要释放。with语句的设计已经考虑到了这一点。个人经验性建议永远不要信任文件扩展名和文件名。.csv文件可能是Excel导出的带BOM的UTF-16.txt文件可能是GBK编码。写代码时先检测编码或者提供一个可配置的编码参数。大文件处理时先估算内存占用。一个简单的公式文件大小 × 编码膨胀系数UTF-8中文约3倍≈ 内存占用。如果超过可用内存的30%考虑分块处理。写日志文件时用a模式而不是w。我见过太多人用w模式写日志每次重启程序就把之前的日志清空了。如果担心日志文件太大配合logging模块的RotatingFileHandler使用。测试文件读写时一定要测试边界情况空文件、只有一行、只有换行符、包含特殊字符如\x00、文件被其他进程锁定。这些情况在单元测试中很容易被忽略但生产环境一定会遇到。最后一条也是最重要的一条写文件时先写入临时文件再重命名。这样即使写入过程中程序崩溃也不会破坏原始文件。这个习惯救过我很多次。importosimporttempfiledefsafe_write(filename,content):# 先写入临时文件tmptempfile.NamedTemporaryFile(modew,deleteFalse,diros.path.dirname(filename),prefixtmp_,suffix.tmp)try:tmp.write(content)tmp.close()# 原子操作重命名os.replace(tmp.name,filename)except:os.unlink(tmp.name)raise文件读写看起来是Python最基础的操作但恰恰是这些基础操作在线上环境最容易出问题。希望这篇笔记能帮你少踩几个坑。