拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python文件对象完全指南:核心方法与实战避坑

1. 文件对象的核心机制与设计思路1.1 先从文件对象是什么说起很多人刚开始学Python文件操作一上来就照着文档抄open()、read()、write()但代码一跑就各种报错。我见过太多人卡在FileNotFoundError: [Errno 2] No such file or directory这行报错上大半天最后发现就是路径写错了个斜杠。这篇文章我想从最底层的东西讲起把Python File的方法掰开揉碎了说清楚最后再给一份实战问题速查表不管你是刚入门还是写了好几年脚本应该都能捞到点东西。首先你得明白一件事当你在Python里执行f open(data.txt, r)的时候Python并不是真的把整个文件“装”进了内存里而是向操作系统申请了一个文件描述符file descriptor。这个描述符就像一个门牌号Python通过它去调用操作系统的读写接口。open()返回的文件对象其实就是这个门牌号的封装它里面记录着文件当前的状态打开模式、编码方式、当前文件指针的位置等等。这里有个特别关键的概念——文件指针。你可以把它理解成看书时的手指头正指着某一行的位置。你读文件的时候手指头就往后移动你写文件的时候手指头也跟着移动。所以同一个文件对象你读一次read()之后再读一次read()第二次拿到的不会是文件开头的内容而是接着上一次的位置继续往后读。这一点没搞清楚的话后面学seek()和tell()的时候一定会绕晕。文件对象还有一个特点它的能力由打开模式决定。你用r模式打开的文件只能读不能写用w模式打开的文件只能写不能读。这不是Python故意限制你而是操作系统层面就不允许。所以在设计文件操作代码之前想清楚“我到底要对这个文件干什么”比急着写代码更重要。1.2 open() 的关键参数与模式选型open()是Python内置函数也是所有文件操作的总入口。它的完整签名是open(file, moder, buffering-1, encodingNone, errorsNone, newlineNone, closefdTrue, openerNone)日常开发中你真正需要经常关心的参数就三个file、mode、encoding。buffering、newline这些用到的时候再说但也不能完全不知道。mode参数是最核心的它决定了你对文件的操作权限。我见过不少新手把r和w搞混一运行发现文件被清空了那真是欲哭无泪。这里给你一张表建议收藏模式含义文件不存在时文件存在时文件指针位置r只读默认报错FileNotFoundError正常打开文件开头w只写覆盖创建新文件清空原内容文件开头a追加写创建新文件保留原内容文件结尾x排他创建写创建新文件报错FileExistsError文件开头r读写报错FileNotFoundError正常打开不覆盖文件开头w读写覆盖创建新文件清空原内容文件开头a读写追加创建新文件正常打开文件结尾这些模式还可以和b二进制、t文本组合比如rb表示以二进制只读方式打开wb表示以二进制写入方式打开。默认是文本模式也就是t。这里我特别想提醒一句r和w虽然都是读写但行为差别很大。r打开已存在的文件不会清空内容适合你想边读边改的场景w打开文件会先把文件清空如果你只是想读一下文件内容结果用了w文件就没了。建议默认情况下能读就不要加能不用w就不用w。关于模式选择的思路我一般是这样判断的如果只是想读取内容用r如果要把程序运行结果存下来且旧的不要了用w如果要在日志文件后面追加记录用a如果要防止误覆盖已有文件用x。二进制文件和文本文件的区别在于二进制模式操作的是字节串bytes文本模式操作的是字符串str如果你处理的是图片、视频、压缩包一定要用二进制模式否则会出各种奇怪的问题。1.3 编码与缓冲两个容易忽略的隐藏参数open()函数里还有一个特别容易踩坑的参数encoding。这个参数只在文本模式下有效作用是指定用哪种字符编码来解析文件内容。很多人在Windows上写代码读取文件时会遇到UnicodeDecodeError原因就是Windows默认编码是GBK而文件实际上是UTF-8编码存的。Python 3在Windows平台上如果你不指定encoding参数默认会使用系统locale的编码通常是GBK而Linux和macOS默认是UTF-8。所以同一份代码在Linux上跑得好好的拿到Windows上就报错十有八九是编码问题。解决方式很简单open的时候显式指定编码with open(data.txt, r, encodingutf-8) as f: content f.read()写入的时候同理统一指定utf-8避免跨平台乱码。再说说buffering参数。它控制文件的缓冲策略默认是-1表示使用系统默认的缓冲策略。文本模式下buffering1表示行缓冲也就是遇到换行符就把缓冲区的数据写出去buffering0表示无缓冲每次写入都直接写进磁盘。二进制模式下buffering0就是无缓冲buffering1表示缓冲区大小。看起来buffering很底层但它对性能的影响很大。如果你一行一行地往文件里写几千几万条数据并且每次write()之后马上flush()程序会慢得让你怀疑人生。正确的做法是攒一批数据再一次性写入让操作系统和Python的缓冲区去处理最后close()的时候统一刷到磁盘。后面讲写入方法的时候我还会专门提这个问题。2. 核心方法逐个拆解与实操要点2.1 读取方法read、readline、readlines 怎么选文件对象最常用的三个读取方法是read()、readline()、readlines()。名字很像但行为完全不同。read(size)是最直接的读取方式。不带参数时它会把整个文件内容一次性读成一个字符串文本模式或者字节串二进制模式。带参数时比如read(1024)它最多读取1024个字符文本模式或1024个字节二进制模式。注意文本模式下这个size是字符数不是字节数一个中文算一个字符。readline()每次读一行返回一个字符串行尾的换行符\n会保留。很多人第一次用readline()发现打印出来的内容有换行然后又print()又加了一个换行结果每行之间空了一行。如果你的文件恰好是Windows换行符\r\n还会看到\r残留这就是后面要单独处理的问题。readlines()则是把文件里所有行读进一个列表每一行是列表中的一个元素换行符同样会保留。这个方法的缺点是显而易见的如果文件很大比如几个GBreadlines()会一次性把所有行都加载进内存内存直接爆掉。那么正确的做法是什么用for循环直接遍历文件对象。看这段代码with open(big_file.txt, r, encodingutf-8) as f: for line in f: print(line.strip())为什么这个方式好因为文件对象本身就是一个迭代器for循环会一行一行地从磁盘读入用完一行就丢弃内存占用始终只有一行的大小。我处理几个GB的日志文件时都是用这种方式内存占用一直保持稳定。所以我的建议是小文件几MB以内用read()或readlines()都无所谓代码简单最重要大文件一定用for循环或者readline()配合while循环。至于readline()和for循环的区别for循环本质上也是调readline()但写起来更简洁还能少写一个判空逻辑。2.2 写入方法write、writelines 与刷新机制写入方法比读取简单一些核心就两个write()和writelines()。write(str)把一个字符串写进文件注意它不会自动在末尾加换行符。如果你需要换行得自己在字符串里带上\n。这个特性经常让人踩坑尤其是从别的语言转过来的朋友习惯了那种自带换行的写法到了Python这里老是忘记加换行。writelines(iterable)接收一个可迭代对象把里面的字符串逐个写入文件。注意它也不会自动在每个元素之间加换行符。所以如果你有一个字符串列表想每行一个元素地写进文件得这样lines [a, b, c] with open(out.txt, w, encodingutf-8) as f: f.writelines(line \n for line in lines)或者用列表推导式先加工好。总之换行符问题需要自己处理。说完两个方法我想重点讲讲写入的“刷新”机制。你调用write()的时候数据并不一定马上写进了磁盘而是先写到内存缓冲区里等缓冲区满了、或者你调用flush()、或者关闭文件的时候才会真正写入磁盘。这个机制是为了减少磁盘I/O次数提高性能。但是如果你写了几行数据马上想用另一个程序去读这个文件可能读不到内容因为数据还在缓冲区里。这时候你就要调用flush()把缓冲区的数据强制刷进磁盘。还有一点如果你忘记close()程序正常退出的时候Python会尝试帮你关闭文件并刷缓冲但如果程序中途崩溃了缓冲区的数据可能会丢失。所以用with语句是最稳妥的它保证代码块执行完一定会调用close()。2.3 文件定位tell 与 seek 的配合前面说了文件指针就像书签那么tell()就是告诉你书签当前在哪一页seek()就是让你把书签挪到指定的位置。这两个方法配合使用可以实现很多高级功能。tell()返回当前文件指针的位置。在文本模式下这个位置是一个不透明的数字表示从文件开头到当前位置的字符偏移量在二进制模式下它就是字节偏移量。seek(offset, whence)有三个基准模式whence0默认从文件开头算起offset必须是非负数。比如f.seek(0)就是把指针移到文件开头f.seek(5)就是移到第5个字符/字节的位置。whence1从当前位置算起offset可以为负数。比如f.seek(-3, 1)就是往前回退3个字符/字节。whence2从文件结尾算起offset通常为负数。比如f.seek(-10, 2)就是停在离文件末尾10个字符/字节的位置。一个很典型的应用场景是读取文件的最后几行或者实现类似tail -f的日志监控功能。在二进制模式下你可以轻松地跳过一段数据直接读取with open(data.bin, rb) as f: f.seek(0, 2) # 跳到文件末尾 file_size f.tell() # 获取文件大小字节数 f.seek(file_size - 100, 0) # 再回到倒数第100字节的位置 tail_data f.read(100)不过要注意在文本模式下Python不允许你随意使用seek()非零偏移量因为字符编码的长度不固定没法简单地从字节偏移量换算成字符位置。文本模式下你可以做的只有f.seek(0)、f.seek(0, 1)、f.seek(0, 2)想精确定位的话要么用二进制模式打开要么就老老实实一行行读。这点特别容易困惑我当初也在这里折腾了半天。2.4 文件属性与方法清单除了上面说的读写和定位方法文件对象还有一些属性和方法日常开发里用到的频率虽然不高但关键时刻能救命。常用属性f.name返回打开的文件路径字符串。f.mode返回打开模式的字符串比如r、wb。f.encoding文本模式下返回编码名称比如utf-8。f.closed文件是否已关闭True表示已关闭。f.buffer底层的缓冲二进制文件对象某些场景下需要直接操作它。常用方法f.flush()把缓冲区内容强制写入磁盘。f.fileno()返回底层的文件描述符编号少数情况下需要传给某些系统级API时有用。f.isatty()判断文件对象是否连接到一个终端设备写交互式命令行工具时可能用得上。f.seekable()判断文件是否支持seek操作。像标准输入stdin这类流对象通常不支持seek。f.readable()/f.writable()判断当前文件对象是否可读/可写可以用来检查模式是否用对了。f.truncate(sizeNone)把文件截断成指定大小不传参数就截断到当前文件指针位置。这个方法是清零重写日志的利器但使用时要特别小心因为它是直接修改文件长度。这些属性方法里我觉得最容易被忽略的是closed。很多老程序员在调试的时候习惯打印f.closed来判断文件是否正常关闭尤其在排查“文件被占用”问题时很管用。另外truncate()这个方法配合seek()可以实现“只保留文件前N个字节”的操作比如清理日志时保留最后50MBwith open(app.log, a, encodingutf-8) as f: if f.tell() 50 * 1024 * 1024: f.seek(-50 * 1024 * 1024, 2) f.truncate()3. 实操过程与核心环节实现3.1 完整示例读取CSV并统计前面讲了一堆方法可能比较抽象。这一节我拿出一个实战场景来把上面提到的方法串起来用一遍。假设我们有一个access.log文件里面记录了系统的访问日志每行开头是一条记录内容包括日志级别、时间戳、消息用逗号分隔。我们要统计一天内ERROR级别的日志有多少条并且把WARNING级别的日志单独写到一个新文件里。先看原始数据长什么样大概是这样的INFO,2024-11-20 09:00:00,服务启动成功 ERROR,2024-11-20 09:05:12,数据库连接超时 WARNING,2024-11-20 09:06:00,缓存命中率低于30% ERROR,2024-11-20 09:15:33,支付接口返回500实现代码import os source_file access.log warning_file warning.log error_count 0 warning_lines [] # 读取源文件并统计 with open(source_file, r, encodingutf-8) as f: for line in f: if ERROR in line: error_count 1 elif WARNING in line: warning_lines.append(line.rstrip(\n)) # 写入warning日志到新文件 with open(warning_file, w, encodingutf-8) as f: for line in warning_lines: f.write(line \n) print(fERROR日志条数: {error_count}) print(fWARNING日志已保存到: {os.path.abspath(warning_file)})这段代码里我用for循环逐行读取大文件避免了一次性加载全部内容导致内存暴涨。用with语句管理两个文件对象一个负责读一个负责写保证无论代码是否抛出异常文件都会被正确关闭。写完之后我还打印了文件的绝对路径。为什么因为新手最常见的问题之一就是文件明明生成了但找不到了。程序里用的相对路径指的是程序运行时的工作目录而你在VSCode或PyCharm里点运行按钮的时候工作目录不一定是.py文件所在目录这就导致文件被写到了别的地方。用os.path.abspath()打印出来你就知道到底写到哪了。这段代码还可以继续升级如果warning_lines列表太大就没必要把所有行都攒在内存里可以打开两个文件一边读一边写with open(source_file, r, encodingutf-8) as src, \ open(warning_file, w, encodingutf-8) as dst: error_count 0 for line in src: if ERROR in line: error_count 1 elif WARNING in line: dst.write(line)注意第二个版本里write(line)直接写原样字符串原来的换行符也一起写进去了所以不需要自己再加\n。这种写法更省内存也更高效。3.2 路径处理open 之前的事说一个很现实的问题很多人写文件操作代码报错不是方法用错了而是路径就错了。FileNotFoundError有个奇怪的特点它在Windows上经常显示“系统找不到指定的文件”但你自己看路径明明存在这就是相对路径和绝对路径的锅。相对路径是相对于“当前工作目录”的不是相对于你的.py文件所在目录。你在命令行里运行python script.py的时候当前工作目录就是命令行所在的目录你在VSCode里按F5调试当前工作目录是VSCode配置的工作区目录通常是项目根目录而不一定是script.py所在的目录。这就是为什么很多人在VSCode里写open(data.txt)总是找不到文件明明data.txt就在代码文件旁边。解决这个问题有几个思路其一用绝对路径。但在代码里写死绝对路径不好换台机器就废了。其二用__file__变量动态获取脚本所在目录再拼出绝对路径from pathlib import Path script_dir Path(__file__).parent data_path script_dir / data.txt with open(data_path, r, encodingutf-8) as f: content f.read()这里用到了pathlib.Path它是Python 3.4之后推荐的文件路径处理方式比原来os.path.join()要优雅得多/运算符直接拼接路径在Windows和Linux上都能正确工作。其三如果是在命令行里运行就用cd切到数据文件所在的目录再执行脚本让相对路径和你的预期一致。这个方法简单粗暴但有时候也会踩坑如果你脚本里用了相对路径写输出文件而你又在一个不小心cd错了目录的地方运行它输出文件就会被写到莫名其妙的路径下。我自己的习惯是所有涉及外部文件的脚本第一步就用os.getcwd()打印当前工作目录再决定用相对路径还是绝对路径。写文件操作之前先确认“我要操作的文件在哪”“程序认为我在哪”这两个问题搞清楚了路径问题基本不会再来困扰你。3.3 上下文管理器与资源释放的正确姿势我已经反复提到with语句现在来说说它背后的原理以及为什么它是文件操作的默认首选。with语句的全称叫“上下文管理器协议”核心是对象的__enter__()和__exit__()两个魔法方法。当执行with open(...) as f:的时候系统会调用open()返回的对象的__enter__()方法把返回值赋给f等代码块执行完无论有没有异常都会调用__exit__()方法在文件对象的实现里__exit__()内部就是调用close()。所以with语句本质上是替你保证了“一定会关闭文件”。如果你不用with而是这样写f open(data.txt, r, encodingutf-8) content f.read() f.close()这三行代码看起来没什么问题但一旦中间某行抛出异常close()就不会执行文件对象不会被释放。在Windows上这会导致文件被持续占用别的程序想读这个文件就会报权限错误。如果你在一个长循环里开文件不关可能还会耗尽文件描述符程序直接崩溃。所以我的建议非常明确文件操作一律用with。它不仅是语法糖更是资源管理的安全生产保障。除了标准文件对象contextlib模块还提供了很多实用的上下文管理器。比如你想临时切换工作目录可以用contextlib.chdir()如果想同时管理多个文件对象with语句后面可以跟多个表达式用逗号隔开、加个续行符像刚才那个读和写的例子那样。这些技巧在写正式项目时能省很多事。4. 常见问题与排查技巧实录4.1 高频报错速查表我在技术社区里回答过很多文件操作相关的问题把出现频率最高的几个报错整理成了下面的表格。建议你遇到问题时先对照这张表。报错信息原因解决方案FileNotFoundError文件路径不存在或相对路径的工作目录和你以为的不一致用os.path.exists()先确认路径打印os.getcwd()查看当前工作目录改用绝对路径PermissionError文件被其他程序占用或没有读取/写入权限关掉正在打开该文件的程序检查文件属性以管理员身份运行程序UnicodeDecodeError读取文件时指定的编码与文件实际编码不一致用encodingutf-8或errorsignore或者用二进制模式打开再decode()UnicodeEncodeError写入文件时字符串包含编码表之外的字符指定正确的encodingutf-8给字符编码前先处理io.UnsupportedOperation当前模式不允许执行该方法比如r模式下调用write()检查打开模式用w、a、r等支持写的模式IsADirectoryError传给open()的路径是一个目录检查路径确认打开的是文件而非文件夹ValueError: I/O operation on closed file文件已经关闭还在调用它的方法检查with语句的缩进范围不要在with外面引用f这张表里最常见的其实是FileNotFoundError和UnicodeDecodeError。FileNotFoundError的坑在于你看到报错的时候很少会怀疑“是不是程序跑的位置和我以为的位置不一样”但你打印一下os.getcwd()往往就真相大白了。UnicodeDecodeError则和文件编码强相关尤其是从网上或者Windows记事本里拿来的文件很可能不是纯UTF-8编码。4.2 实战避坑与独家经验最后这部分我把自己踩过的一些坑和解决经验整理出来。这些东西一般不会写在官方文档里但对实际开发来说特别关键。第一个坑Windows路径反斜杠。Windows的路径分隔符是反斜杠\但在Python字符串里反斜杠是转义字符所以写C:\Users\name\data.txt会出问题。三个解决方案用双反斜杠C:\\Users\\name\\data.txt、用原始字符串rC:\Users\name\data.txt、或者干脆统一用pathlib.Path处理它会自动处理好跨平台的分隔符问题。我推荐第三种。第二个坑换行符不统一。Linux/Unix系统用\n换行Windows用\r\n换行。在文本模式下打开文件Python的通用换行模式会帮你转换读的时候把\r\n转成\n写的时候把\n转成\r\n。但如果文件是二进制模式打开的换行符就是原样返回的这时候你自己处理字符串时就要考虑\r\n的存在否则你会写出很多\r\r\n这种混乱的行。有一个细节在文本模式下你写入的\n在Windows文件里会被自动扩展成\r\n所以如果你看到文件里出现双换行先检查是不是自己在字符串里多写了一个\n。第三个坑数据写到一半程序崩了。如果你的程序动态生成数据边生成边写入一旦中途崩溃文件里留下的就是半截数据。一个实用的办法是先写入一个临时文件全部写完后再用os.replace()原子地替换目标文件。这样即使程序崩了原文件还是完整的。第四个坑VSCode里Ctrl点击方法不跳转。这个和文件操作本身关系不大但问的人实在太多了。它通常是Python插件没装、或者VSCode没有选择正确的Python解释器导致的。装了Python插件的正确路径是在设置里选择解释器左下角会有解释器版本提示。文件操作出现问题时如果你定位不到方法定义排查会非常痛苦。同样的问题还有“环境变量没配好导致python命令找不到”这些基础环境问题建议在学文件操作之前先一步到位。第五个坑大日志文件清理。我前面讲过truncate()可以截断文件但如果你直接用open(app.log, w)去清空一个正在被进程写入的日志文件可能会遇到PermissionError或者日志继续写入但你看不到内容的情况。更稳妥的做法是用a模式打开然后配合seek()和truncate()截断到指定位置保留最近一段日志。第六个坑写完文件马上读读不到内容。这个前面提过是缓冲区的问题。写入的数据还在内存缓冲区里另一个程序甚至同一个程序的另一个文件对象去读这个文件读到的是旧数据。解决方法是write()之后立即flush()或者确保写文件的with语句先结束文件关闭后再读。我再强调一次文件操作里99%的“灵异事件”根源就三类路径不对、编码不对、资源没关好。遇到问题先往这三个方向排查基本能解决绝大多数情况。5. 从文件操作到常见应用场景的延伸5.1 文件复制、移动与删除的常规做法open()这些方法负责文件内容读写但文件本身的复制、移动、删除通常不直接用File方法而是借助os和shutil模块。这两个模块跟文件操作是黄金搭档很多场景必须组合使用。复制文件在Python 3.8之后可以用shutil.copyfile(src, dst)或shutil.copy(src, dst)。前者只复制文件内容后者还会复制权限位。区别很小日常用哪个都行。移动或重命名文件用os.replace()或shutil.move()。删除文件用os.remove()。这些操作同样会踩前面说的路径坑而且删除操作一旦路径写错删错文件就麻烦了。所以我在做删除操作之前一定会先打印出要删除的文件的绝对路径再动手。5.2 遍历目录os.walk 与 pathlib 的配合文件操作的更高级场景是遍历目录。比如你想批量重命名一个文件夹下的所有文件或者分批读取多个子目录下的日志这时候就要用到os.walk()或者pathlib的iterdir()、glob()。os.walk(top)会递归遍历一个目录每轮返回三个值当前目录路径、子目录列表、文件列表。配合open()就可以实现“读遍所有文件”的批量操作。举一个很基础的例子统计一个目录下所有.txt文件的总行数。import os total_lines 0 for root, dirs, files in os.walk(docs): for name in files: if name.endswith(.txt): file_path os.path.join(root, name) with open(file_path, r, encodingutf-8) as f: total_lines sum(1 for _ in f) print(total_lines)这段代码里sum(1 for _ in f)是个很漂亮的写法不用显式for循环一行就能统计文件行数。注意os.walk默认会递归到所有子目录如果你只想处理当前目录可以用next(os.walk(docs))取第一轮返回值或者直接改用Path(docs).iterdir()。5.3 大文件与实时日志的按需读取前面反复提了大文件要逐行读取但逐行读取也有讲究。如果你只想读文件里的第1000行用for循环从头读到第1000行那就白白浪费了前面的999次迭代。这时候可以用itertools.islice()跳过前N行效率会高很多from itertools import islice with open(access.log, r, encodingutf-8) as f: line_1000 next(islice(f, 999, 1000), None) print(line_1000)如果是实时增长的日志文件你想要的是“持续追加读取”效果相当于Linux的tail -f。这个在Python里可以用seek()配合循环实现把文件指针移到文件末尾然后循环判断文件大小有没有变化有新数据就读出来。这个场景我一般会写成一个小工具类核心逻辑就是不断seek到tell()的位置然后readline()没新行就休眠一会儿再试。import time def follow(filename): with open(filename, r, encodingutf-8) as f: f.seek(0, 2) # 跳到文件末尾 while True: line f.readline() if line: yield line else: time.sleep(0.5)这个生成器函数非常实用用在日志监控、数据采集脚本里都合适。它利用的正是前面讲的seek(0, 2)和tell()的知识点算是文件定位方法的一个高阶应用。5.4 与JSON、CSV等格式的组合实际项目中文件读写往往不是简单的纯文本而是JSON、CSV、YAML这类结构化数据。open()负责最底层的字节流读写再配合json、csv等模块做解析和序列化才能组成完整的功能。读写JSON文件的典型例子import json data {name: python, version: 3.12, features: [dynamic, readable]} # 写入 with open(config.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) # 读取 with open(config.json, r, encodingutf-8) as f: loaded json.load(f)注意ensure_asciiFalse这个参数很关键。如果不设中文字符会被转成\uXXXX这样的转义序列文件里全是一堆反斜杠既不美观也难调试。indent2则是让JSON输出有缩进方便人读。读写CSV文件则通常用csv模块import csv with open(data.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: print(row[name], row[age]) with open(out.csv, w, encodingutf-8-sig, newline) as f: fieldnames [name, age] writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerow({name: Alice, age: 18})这里有两个细节encodingutf-8-sig可以在读取带BOM的Excel导出CSV时自动去掉BOM头写入时指定newline可以防止Windows平台下csv模块写出多余的空行。这些坑我用得多了才慢慢摸清楚现在算是条件反射了。5.5 临时文件与内存文件的处理有时候你并不需要一个真实落盘的文件只是需要一个“假装是文件”的东西用来传给某个接收文件对象的函数。这时候可以用io.StringIO或io.BytesIO它们是内存中的文件对象支持read()、write()、seek()等绝大多数File方法但不会产生真实文件。在写单元测试、或者处理字符串格式的数据时特别有用。from io import StringIO buffer StringIO() buffer.write(hello\n) buffer.write(world\n) buffer.seek(0) print(buffer.read())如果你确实需要临时文件tempfile模块是更好的选择。tempfile.NamedTemporaryFile()可以创建一个带具体名称的临时文件用完自动清理而且在多线程环境下也能保证文件名不冲突。import tempfile with tempfile.NamedTemporaryFile(modew, suffix.txt, deleteTrue) as f: f.write(temporary data) temp_name f.name print(f临时文件路径: {temp_name}) # 退出with后临时文件自动删除这个模式用来做“先写临时文件、再原子替换正式文件”的落地操作非常顺手。把临时文件写在目标文件同一个目录下写完后用os.replace()覆盖目标文件就能做到原子更新进程崩溃也不会留下半截文件。6. 文件操作的项目实战从零构建一个日志管理器为了把前面讲的方法串成一个完整的项目这里我给出一个可以直接拿来改的“日志管理器”示例。它的功能是遍历指定目录下所有.log文件统计每个文件的ERROR条数把超过阈值的文件备份后截断并输出一份汇总报告。import os import shutil from datetime import datetime from pathlib import Path LOG_DIR Path(logs) BACKUP_DIR Path(backup) ERROR_THRESHOLD 100 def count_errors(file_path): count 0 with open(file_path, r, encodingutf-8, errorsignore) as f: for line in f: if ERROR in line: count 1 return count def backup_and_truncate(file_path): # 备份 backup_path BACKUP_DIR / f{file_path.name}.{datetime.now():%Y%m%d_%H%M%S} shutil.copyfile(file_path, backup_path) # 保留最后1000行的内容然后截断 with open(file_path, r, encodingutf-8, errorsignore) as f: lines f.readlines()[-1000:] with open(file_path, w, encodingutf-8, errorsignore) as f: f.writelines(lines) return backup_path def main(): BACKUP_DIR.mkdir(exist_okTrue) report [] for log_file in LOG_DIR.glob(*.log): errors count_errors(log_file) status OK if errors ERROR_THRESHOLD: status CLEANED backup_path backup_and_truncate(log_file) else: backup_path None report.append((log_file.name, errors, status, str(backup_path) if backup_path else )) # 输出汇总报告 with open(report.csv, w, encodingutf-8-sig, newline) as f: f.write(文件,ERROR数,状态,备份路径\n) for row in report
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门