news 2026/10/1 12:53:36

Python文件读写入门:从open到with,掌握文件复制与工程化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python文件读写入门:从open到with,掌握文件复制与工程化实践

我最初接触《笨办法学python》这本书的时候,完全是被书名里的"笨办法"三个字吸引的。市面上的Python教程大多在讲语法、讲特性、讲框架,它却反着来,逼着读者把每一段练习亲手敲进电脑,敲错了就停下来对照检查,敲对了才允许翻到下一页。就这么一行一行地熬过十几个练习,到第17课出现时,我第一次产生了一种"我好像真的能用Python干点实际的事"的错觉。第17课的标题叫More Files,核心任务一句话就能说清:把A文件的内容复制到B文件。听起来简单到有点无聊,但它把open、read、write、close这一整套文件读写动作一次性塞进了你的手掌心,从此之后的配置读取、数据落盘、日志处理、结果导出,全都建立在这一课理解得够不够扎实上。这篇文章不打算复述教材,而是结合我自己当年实操、以及后来陪很多学员一起踩坑的经验,把第17课讲透,再往后多走两步,让它真正变成你的能力。

1. 第17课在整本书里的位置:一次从"会语法"到"会干活"的转折

1.1 这本书的路数:刻意重复、手脑并用

《笨办法学python》整本书的核心逻辑是"练习优先,解释靠后"。前16课里,你在反复练变量、函数、条件判断、循环、列表这些基本功,每个练习都要求手敲源码,而不是复制粘贴。这种做法看起来效率低,实际是在训练你的手指和眼睛:手指记住缩进和标点,眼睛记住报错信息长什么样。等这些内功练得差不多了,第17课才把你带到"程序与外部世界交互"的第一站——文件。

我经常跟刚入门的朋友打一个比方:前16课是让你在驾校场地里练打方向盘、踩离合,第17课则是第一次把车开上真实马路。文件就是程序与硬盘之间的马路,读写文件就是你第一次让程序在退出之后还能留下痕迹。没有这一步,你写出来的代码永远只是内存里转瞬即逝的临时计算,有了这一步,你才真正开始做"软件"该做的事。

1.2 第17课的核心任务:文件复制这个小练习到底在练什么

表面上是复制文件,实际上拆开看有四件事:打开源文件、读取内容、打开目标文件、写入内容。每个动作对应一个Python函数:open()负责打开,read()负责读取,write()负责写入,close()负责收尾。第17课把这四个动作全部串在一个程序里,而且故意用了一种"啰嗦"的写法——每一步都打印一行提示信息,让你清楚地看到程序正在做什么。

这种设计非常像训练运动员时的分解动作。真正的高手写文件复制会用shutil.copy()一行搞定,但新手必须先把底层动作重复练到形成肌肉记忆。等你知道read()之后有一个"当前读取位置"的概念,知道close()不及时会占用文件句柄,再去使用shutil这种高级工具,才算真正理解它替你做了什么。这个先后顺序不能颠倒,颠倒了你就是在背API,而不是在学编程。

2. 动手做一遍:第17课的代码长什么样,每一行在干嘛

2.1 原版练习的代码还原

如果手边没有英文原版,这里是我根据记忆以及对教材的理解复现出来的第17课练习代码,功能是从命令行接收两个文件名,把第一个文件的内容复制到第二个文件:

from sys import argv from os.path import exists script, from_file, to_file = argv print(f"正在从 {from_file} 复制到 {to_file}") # 打开源文件,读取全部内容 in_file = open(from_file) indata = in_file.read() print(f"源文件大小为 {len(indata)} 字节") print(f"目标文件是否已存在? {exists(to_file)}") print("按回车键继续,按 CTRL-C 中止。") input() out_file = open(to_file, 'w') out_file.write(indata) print("已完成复制。") out_file.close() in_file.close()

运行方式是在命令行里敲:

python ex17.py test.txt copy.txt

如果你自己写了一遍,会发现这段代码其实挺"话痨"的,几乎每一步都要打印状态。这正是练习的目的:让你看清数据是怎么从文件流进内存、再从内存流向另一个文件的。

2.2 逐行拆解:argv、exists、input()分别解决什么问题

第17课用到了三个之前课程不太常用的东西。第一个是sys.argv,它是命令行参数的入口。script, from_file, to_file = argv这一行,等于把"当前脚本名、源文件名、目标文件名"三个值依次解包给三个变量。这是文件处理脚本最常见的交互方式,比在代码里写死文件名要灵活得多,因为你不用为了复制别的文件去改代码。

第二个是from os.path import exists,exists()用来判断目标路径是否已存在。这个判断在真实场景中太重要了——如果目标文件已经存在,你直接open(to_file, 'w')的后果是瞬间清空原有内容。所以练习里特意把它单独拿出来打印,就是提醒你:"写入前看看脚下,别一脚踩碎别人的东西。"

第三个是input(),它在这里充当"人工确认闸门"。程序读到这行会停下来,等你按回车才继续。设计意图是让你有机会在覆盖前反悔。实际写脚本时,这个交互不一定每次都需要,但用来学习"写文件是破坏性操作"这个意识,效果立竿见影。

2.3 我习惯性改掉的写法:从open/close到with open

第17课里的写法是为了教学故意铺开的,但放到真实项目里,in_file.close()和out_file.close()很容易出问题。如果write()执行到一半抛出异常,close()这行代码根本不会执行,文件句柄就漏掉了。对Windows用户来说,后果就是你发现文件被进程占用,想删除都删不掉;在大量循环处理文件的场景下,还会逐渐耗尽系统文件描述符。

所以我现在教任何新手,都会建议在理解原版写法之后,立刻改用with语句:

from sys import argv script, from_file, to_file = argv with open(from_file, 'r', encoding='utf-8') as f_in: indata = f_in.read() with open(to_file, 'w', encoding='utf-8') as f_out: f_out.write(indata)

with的学名叫上下文管理器,它保证代码块无论正常结束还是异常退出,都会自动调用close()。你可以理解为:以前是"出门前要记得锁门",现在是"装了自动闭门器,人一走门就自己带上"。两种写法都要会——考试和看书能看懂老的,写代码和找工作用新的,这就是从笨办法走向专业做法的第一步。

2.4 我在练习时额外做的一件事:验证写入结果

教材不会主动教的一个好习惯是"写后校验"。复制文件这个操作,表面上看write()执行完就万事大吉,但硬盘写满、权限变化、编码转换异常都可能导致写入内容与源文件不一致。我自己练习时会在写完以后重新打开目标文件,比对字节数:

from os.path import getsize 源文件大小 = getsize(from_file) 目标文件大小 = getsize(to_file) if 源文件大小 == 目标文件大小: print("复制成功,文件大小一致") else: print("警告:文件大小不一致,请检查")

即便不用中文变量名(只是举例方便理解),核心思路是一样的——凡是涉及"数据迁移"的操作,都应该有结果校验。这是从完成练习迈向工程化的重要一步。

3. 新手必踩的坑:路径、编码、资源泄漏与权限问题

3.1 路径认知错误:相对路径到底相对谁

第17课让很多人卡住的第一关,不是代码写错,而是运行时提示FileNotFoundError。最常见的场景是:你在PyCharm里把代码和test.txt放在同一个文件夹,运行却报"找不到文件"。原因是相对路径是相对于"当前工作目录"的,而IDE的运行配置可能把工作目录指向了项目根目录,不是代码文件所在目录。

排查方法很简单,在脚本开头打印一下当前工作目录:

import os print(os.getcwd())

如果发现工作目录和你预想的不一致,要么用os.chdir()切换,要么干脆用绝对路径。我自己更推荐后者,但要注意硬编码绝对路径会导致脚本换台电脑就跑不了。折中方案是动态拼接:

from pathlib import Path 当前脚本目录 = Path(__file__).parent 源文件路径 = 当前脚本目录 / "source.txt"

__file__是Python内置的当前脚本路径变量,Path(__file__).parent拿到它的所在目录,然后再去组合文件名。这种方法既避开工作目录问题,又保留相对路径的可移植性,是我现在写小工具的首选。

3.2 编码问题:文本文件不是只有一种"文本"

第二个高频坑是编码。open()函数默认使用系统编码,Windows上经常是cp936(GBK的别名),macOS和Linux上是utf-8。同一份Python代码,在Windows上运行正常,丢到服务器上可能直接给你抛一个UnicodeDecodeError,或者更隐蔽的——读进来全是乱码还不知道错在哪。

尤其是用记事本在Windows上创建文件时,它可能默认以UTF-8 with BOM格式保存。BOM是文件开头多出的三个字节\xef\xbb\xbf,Python按utf-8读取能正确处理,但按utf-8-sig读取才能自动去掉BOM标记。反过来,有些旧文件是GBK编码,你按utf-8读就会报错。

我现在的习惯是一律显式声明编码,绝不留白:

with open(from_file, 'r', encoding='utf-8') as f_in: indata = f_in.read()

如果读取旧系统导出的文件,遇到编码报错,可以先用chardet或file命令检测编码,再决定用什么参数打开。花两分钟搞清楚编码,能省下两小时的乱码排查时间。

3.3 资源泄漏:为什么close()不能只写在纸面上

第17课特意在结尾写了两个close(),但在真实代码里,"打开文件后忘了关闭"几乎是最常见的资源管理错误。Python有垃圾回收机制,理论上文件对象没有引用时会被自动回收,但回收时机不确定。在Windows系统上,一个打开状态的文件会阻止你重命名、删除、覆盖它,如果你在循环里反复以写模式打开同一个文件,还会在几次迭代后收到Too many open files的报错。

这就是我在2.3节里坚持推荐with语法的原因。它不是花哨的语法糖,而是防呆设计。新人期踩一次"文件被占用"的坑,就明白为什么要花心思确保关闭。如果你还停留在"反正脚本跑完进程就退出了"的心态,等你自己写一个常驻服务、定时任务或者GUI程序时,就会付出代价。

3.4 权限与文件占用:报错信息里有半数的答案

还有一种情况是代码没问题、路径没问题、编码也没问题,但还是报错。大概率是权限:目录只读、目标路径在系统受保护目录、当前用户没有写入权限。PermissionError出现时,先别急着改代码,用文件管理器确认一下目标目录的权限,或者以管理员身份运行终端试试。

另一个容易忽略的是"文件正被其他程序占用"。如果你复制一个正在被Excel或编辑器打开的Excel文件,写入时就会报PermissionError: [Errno 13] Permission denied。这时候不是程序写错了,而是你得在业务逻辑上决定:提示用户关闭文件,还是把文件先复制成临时副本再操作。这些都是真实项目中会遇到的细节,教材不会写,但你必须知道。

3.5 input()的隐患:交互式确认在自动化任务里会卡死

第17课里的input()是为了确认覆盖操作,但如果你把这个脚本放到定时任务、批处理或者CI环境里,程序会永远停在"按回车键继续"这一步,因为根本没人按回车。脚本看似死了,其实是在等你一个永远不会来的输入。

我的建议是:学习阶段保留它,理解"覆盖需谨慎"的含义;进入真实脚本阶段,要么删掉input(),要么改用参数控制:

import sys if '--force' in sys.argv: print("强制执行,跳过确认") else: input("按回车键继续,按 CTRL-C 中止。")

这样既保留了安全闸门,又给自动化留了后门。这是我在实际工程里常用的模式——交互与自动化并存,而不是非此即彼。

4. 把文件复制练成肌肉记忆:三个能直接用的改造场景

4.1 场景一:批量备份配置文件

第17课的代码只能处理单个文件,稍微改造一下就能变成批量工具。比如你有一堆.conf配置文件散落在不同目录,定期备份时想给每个文件生成一个.bak副本。遍历目录用os.walk,复制部分复用文件读写逻辑:

import os import shutil 备份根目录 = "configs" 备份目标目录 = "backup" for root, dirs, files in os.walk(备份根目录): for name in files: if name.endswith(".conf"): src = os.path.join(root, name) dst_dir = os.path.join(备份目标目录, os.path.relpath(root, 备份根目录)) os.makedirs(dst_dir, exist_ok=True) dst = os.path.join(dst_dir, name + ".bak") shutil.copy(src, dst) print(f"已备份: {src} -> {dst}")

这里我用了shutil.copy而不是手写的open/read/write,因为文件复制属于高频基础操作,标准库已经帮你封装好了,没必要重复造轮子。但前提是你已经理解了底层原理,否则遇到shutil不支持的场景(比如只复制前10KB)会无从下手。备份场景的关键逻辑其实是第17课之外的:用os.makedirs保证目标目录存在,用os.path.relpath保留目录结构。这些组合起来才是工程化的备份脚本。

4.2 场景二:日志文件的追加与按大小分割

open()的第二个参数决定打开模式。第17课用'w'(写入,覆盖),后续你还会经常用到'a'(追加)和'r+'(读写)。比如一个运行日志,希望每次运行都往末尾追加而不是清空重来:

with open("run.log", "a", encoding="utf-8") as f: f.write(f"{当前时间} 任务开始\n")

追加模式的底层行为是:打开文件后,把写指针自动移到文件末尾,每次write()都从尾部插入。它比先读整个文件再写回的方式高效得多,尤其是日志文件涨到几百MB时,一次全量读写会拖垮程序。

更高级一点的需求是按大小切分日志。当一个文件超过10MB,你想把旧内容改名归档,再新建文件继续写。这时候就要用到os.path.getsize():

import os LOG_FILE = "app.log" MAX_SIZE = 10 * 1024 * 1024 # 10MB if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) >= MAX_SIZE: os.rename(LOG_FILE, LOG_FILE + ".1") print("日志已滚动")

虽然真实项目有专门的logging模块能实现RotatingFileHandler,但从第17课延伸出来的"文件大小判断+重命名+重建"思路,是理解那些高级模块的地基。你会发现有大量功能,本质上都是这几个基础动作的组合。

4.3 场景三:简易的文件搬运脚本

复制和移动是两件事,但很多人一开始容易混。复制是源文件保留,目标位置多一份;移动是源文件消失,目标位置出现。Python里移动文件有两种方式:os.rename只能在同一个文件系统内,跨盘符会报错;shutil.move能自动处理跨盘符情况,本质上是"复制+删除源文件"的组合。

写一个超简易的搬运脚本,可以练习目标目录的自动创建:

import shutil from pathlib import Path 来源目录 = Path("downloads") 目标目录 = Path("archive/2026") 目标目录.mkdir(parents=True, exist_ok=True) for file in 来源目录.iterdir(): if file.is_file() and file.suffix in {".zip", ".tar", ".gz"}: shutil.move(str(file), str(目标目录 / file.name)) print(f"已移动: {file} -> {目标目录 / file.name}")

Path.mkdir(parents=True, exist_ok=True)这行是重点——parents=True表示如果上级目录不存在就一并创建,exist_ok=True表示目录已存在时不报错。没有这两个参数,你会被FileNotFoundError教育很多次。这类"自动建目录"的细节,是让脚本从实验室走进真实环境的关键。

5. 往后走:从第17课到能处理真实文件任务的路

5.1 升级工具:pathlib比os.path更好用

如果你已经熟练掌握了第17课的写法,下一步建议认真学一下pathlib模块。Python 3.6及以上版本推荐使用它处理路径,因为它的API是面向对象的,读起来更符合人的思维习惯。一个典型对比:

# 传统写法 import os path = os.path.join("a", "b", "c.txt") print(os.path.exists(path)) print(os.path.basename(path)) # pathlib写法 from pathlib import Path path = Path("a") / "b" / "c.txt" print(path.exists()) print(path.name)

/运算符直接拼接路径,exists()、read_text()、write_text()这些常用方法直接挂在对象上。尤其是Path.read_text()和Path.write_text(),它们把第17课里的"打开-读取/写入-关闭"压缩成一行:

from pathlib import Path 内容 = Path("source.txt").read_text(encoding="utf-8") Path("copy.txt").write_text(内容, encoding="utf-8")

底层还是open/close那一套,但上层API清爽了太多。我推荐初学者在第17课之后立刻用pathlib重新写一遍文件复制练习,体会一下同样的功能、不同的抽象层级带来的体验差异。

5.2 从读文件到读数据:CSV与JSON是下一站

第17课处理的是纯文本文件,但现实世界中你复制的最多的其实是"结构化数据"。比如Excel导出的CSV、接口返回的JSON、爬虫抓下来的HTML。它们本质仍是文本文件,但你需要按规则解析。Python标准库里的csv和json模块,底层都建立在你已经学会的open/read之上。

举个例子,读取CSV:

import csv from pathlib import Path with Path("data.csv").open("r", encoding="utf-8") as f: reader = csv.reader(f) for row in reader: print(row)

读取JSON:

import json from pathlib import Path data = json.loads(Path("data.json").read_text(encoding="utf-8")) print(data["name"])

你能看出,所有这些代码的第0步都是"把文件内容读进内存"——正是第17课练到的东西。很多人在这一步卡住,不是不懂JSON语法,而是连"先把文件打开"这个基本动作都没形成条件反射。所以别觉得第17课简单,它是后面所有数据处理类任务共同的地基。

5.3 学习路线建议:先笨办法,再上工具

如果你现在刚读完第17课,我的建议是不要急着去看各种框架和爬虫实战。先拿一周时间,用open/read/write/close加pathlib加os模块,把你日常能碰到的文件操作全部写一遍:复制、移动、重命名、清空、追加、批量改扩展名、统计文件行数。每个小练习都用两种方式实现:一种是最底层的open写法,一种是pathlib/shutil的捷径写法。这个阶段看起来"笨",但它会在你将来读别人代码时发生奇效——你能一眼看穿高级API背后在做哪些底层操作,那是一种别人没法替代的掌控感。

我在带人的过程中发现一个规律:能默写出第17课核心逻辑的人,后面学数据分析、爬虫、自动化运维都顺很多;而跳过它直接去看"高级技巧"的人,遇到文件读写就发怵,分不清open模式里w和a的区别,也搞不懂为什么with下面要缩进。这些看似不起眼的细节,恰恰是区分"真的会写Python"和"只是看过教程"的分水岭。

写在实操之后的一些补充

如果只看动作,第17课就是复制一个文件;如果看本质,这一课是让你第一次理解什么叫"程序的状态可以越过运行时边界继续存在"。你写的变量会在程序退出后烟消云散,但写进文件的内容不会。这个"持久化"的概念,是后续所有有意思的Python项目——爬虫、数据分析、Web应用、自动办公脚本——的共同前提。

最后再分享一个我个人的小习惯:每学完类似第17课这样的小练习,我会逼自己写出一个"章节之外的小变体"。比如这一课,我就写了一个"复制文件并自动重命名备份"的小脚本;学到函数那一课,又回头把第17课改成让函数接收路径参数。这种"复习上一课、改造当前课"的做法,比一味往前刷章节更扎实。我至今都记得第一次把自己的脚本运行成功、文件真的被复制出来的瞬间——那种"代码指挥了硬盘"的兴奋感,是支撑我继续写下去的动力之一。希望你也能在敲完第17课之后,得到同款体验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:53:25

基于Spring Boot的血库管理系统设计与毕业设计实战

1. 毕业设计选题:为什么选血库管理系统而不是“烂大街”的商城或图书管理 每年到了毕业设计开题季,总有一批同学在“商城系统”“图书管理系统”“博客系统”这几个老面孔之间反复横跳。倒不是说这些题目不能做,而是它们已经被做了太多遍&…

作者头像 李华
网站建设 2026/10/1 12:53:02

JSP+Java校园二手平台开发实战指南

简介:这是一份面向计算机专业本科生的毕业设计与期末大作业实战资源,聚焦校园二手物品交易场景,提供基于Java Web技术栈的完整可运行系统。资源包含1305个文件,涵盖128个Java业务逻辑类、119个JSP页面、364个JS交互脚本、146个CSS…

作者头像 李华
网站建设 2026/10/1 12:53:01

2B模型跑赢4B?MiniCPM5-2B长上下文与工具调用实战解析

最近在盘点端侧和低算力环境下能跑的开源模型,我注意到OpenBMB放出的MiniCPM5-2B讨论度很高。这个小参数模型的卖点非常直接:2B参数却跑赢了不少4B级别的模型,在同等体量里做到了开源SOTA,还带131K长上下文和工具调用能力。这两个…

作者头像 李华
网站建设 2026/10/1 12:52:16

Vue项目接入外部JS的完整指南:从脚本加载到SDK生命周期管理

Vue项目里接外部JS,是很多人迟早要面对的事。小到页面里插一个统计脚本,大到对接腾讯地图、企业微信JS-SDK、播放器组件,都会涉及到“Vue和外部JS怎么配合”这个问题。我在实际开发里踩过不少坑,也慢慢整理出一套比较稳妥的做法&a…

作者头像 李华
网站建设 2026/10/1 12:52:07

教辅排版与例题设计:样式规范、解析编写实战指南

“样式及例题”,光看这个标题容易让人摸不着头脑,但这五个字恰恰戳中了所有做教辅、写讲义、出培训材料的人最头疼的两件事:排版得像样,题目得像样。很多刚入行的作者、教研新人甚至一线老师,写正文时思路顺畅&#xf…

作者头像 李华
网站建设 2026/10/1 12:51:54

短剧翻译流程全对却效果差?关键在字幕节奏与口语化细节

1. 短剧翻译为什么"流程全对,成品不对"做短剧翻译这行久了,你会发现一个特别普遍的现象:很多团队接单的时候流程表列得明明白白——翻译、审校、润色、质检、打轴、压制,一环不落。可交付的成品一看,要么观众…

作者头像 李华