程序要动硬盘,第一件事是搞清楚「我现在在哪」。os.getcwd() 回答这个。
路径不能用加号拼。 Windows 两种斜杠都认,所以这个坑在这里看着不严重;
换到 Linux 上,一个反斜杠就能让整段路径失效。
os.walk 一次给你一层,外层 for 加内层 for 就遍历完了,不用自己写递归。
第 3 章讲的都是内存里的东西,程序一关就没了。这一章开始动硬盘。
教材第 4 章开头两节是【同步备份文件】和【使用 os 程序库操作文件】。
教材把「要做什么」写在前面、「用什么做」写在后面。
本篇照这个顺序:先看 os 能干什么,下一篇再把它们拼成那个备份脚本。
有一件事必须提前警告:shutil.rmtree 没有回收站。 本篇最后一节会讲,看到那里请慢一点。
一、先搞清楚「当前在哪」
os.getcwd() 告诉你当前目录。
所有相对路径都相对它算,所以百分之八十的「文件找不到」,答案都在这一行。
os.listdir(d) 列出目录里的名字。它有两个局限:只给名字不给完整路径,而且不区分文件和文件夹。
想区分,就再判断一次:os.path.isdir(名字) 是文件夹,os.path.isfile(名字) 是文件。
或者干脆用 os.scandir(),它给的对象自带 is_dir() 方法。
演示里那个目录只有一个 a.txt 和一个 sub。
listdir 返回 ['a.txt', 'sub'],光看这两个字符串,分不出哪个是文件夹。
二、路径不能用加号拼
演示第二段把两种拼法摆在一起看:加号拼 得到 …/m01/sub,os.path.join 得到 …\m01\sub。
两个字符串不相等:一个正斜杠,一个反斜杠。但两个都能打开——因为 Windows 两种都认。
这就是这个坑在这里看不太出来的原因。 换到 Linux 上,反斜杠不是分隔符,
整个路径会被当成一个奇怪的文件名。
真正会出事的还有两种情况。一是源目录末尾本来就带斜杠,src + "/" + 名字 拼出来变成两层斜杠;
二是路径里有中文或空格,拼错一个字符整段失效。
os.path.join 会自己处理分隔符,而且能一次接好几个参数:join(a, b, c)。永远用它,别用加号。
三、拆路径的四个零件
| 函数 | 给你什么 | 例 |
|---|---|---|
os.path.dirname(p) |
去掉最后一段的目录部分 | …\sub\deep |
os.path.basename(p) |
最后那一段 | c.txt |
os.path.splitext(p) |
名字和扩展名分开 | ('c', '.txt') |
os.path.exists(p) |
在不在 | True |
splitext 分的是「最后一个点」。文件名里带点也没关系,它只认最后那个。
所以 splitext('报告.2024.最终版.docx') 给的是 ('报告.2024.最终版', '.docx'),
不会在中间那个点上断。
判断文件类型别自己切字符串,用 splitext。
它处理了那些你想不到的情况,包括根本没有扩展名(返回第二段是空字符串)。
四、走一遍整棵树
os.walk(根目录) 一次给你三样:当前目录路径、子目录列表、文件列表。然后自动往下一层走,直到走完。
演示里那棵树有三层,walk 给出三次结果:根目录、sub、sub\deep。
注意它是自上而下,一层一层来的。
写法固定是两层 for:外层拿三个变量,内层走文件列表。
这就是「遍历一个文件夹里所有文件」的全部代码,不用自己写递归。
备份脚本全靠这个。想「所有文件」就两层 for 收完;
想跳过某个子目录,在遍历时把 dirs 里那个名字删掉就行,walk 就不会往下走它了。
五、大小、时间、权限
os.path.getsize(p) 给字节数。
os.path.getmtime(p) 给修改时间,但它是一个时间戳——从 1970 年 1 月 1 日算起的秒数,
直接打印是个十几亿的整数。
要给人看,得转一下:datetime.fromtimestamp(mtime)。下一篇会专门讲时间怎么处理。
这个时间戳是下一篇备份脚本的核心判据。 「谁新谁旧」全靠它比。
权限用 os.stat(p).st_mode 拿,但那是一串带类型信息的数字(显示成 100666 这样)。
要纯权限位,套一层 stat.S_IMODE(),得到 666。
666 是八进制,意思是不管谁都能读能写。
演示里这个值是 666,因为 Windows 上权限模型跟 Unix 不一样,Python 只能给个象征性的值。
真要管权限,得在 Linux 上做。
六、建、改、删,以及那条不能撤销的命令
os.makedirs(p, exist_ok=True) 建目录,会一路建上去。os.mkdir 只能建一层,父目录不存在就报错。
exist_ok=True 这个参数很关键。 不加它,目录已经存在时会抛 FileExistsError。
后果是:同一个脚本第二次跑就崩在「文件已存在」上。
改名用 os.rename(旧, 新),删文件用 os.remove(p),删空目录用 os.rmdir(p)。
要连内容一起删目录,是 shutil.rmtree(p)。这条命令没有回收站,删了就是删了。
写自动化脚本时,这条命令是唯一一个「跑错了没法撤回」的地方。
本书的做法是:涉及 rmtree 的脚本,先把要删的路径打印出来,加一个 --dry-run 只打印不动手,
确认过再真跑。下一篇的备份脚本就是这个套路。
七、动手 Demo:os:文件夹和路径
造一棵三层的目录树,然后从「我在哪」一路试到「怎么删干净」。
重点看两处:加号拼出来的路径和 os.path.join 拼出来的那个 False,
还有 os.walk 一层一层吐出什么。
import os, stat, shutil, tempfile
from datetime import datetime
root = tempfile.mkdtemp()
os.makedirs(os.path.join(root, 'sub', 'deep'))
for p in ('a.txt', 'sub/b.txt', 'sub/deep/c.txt'):
open(os.path.join(root, p), 'w').write('x')
print('① 当前在哪、里面有什么')
print(' os.getcwd() =', os.getcwd())
print(' os.listdir(root) =', os.listdir(root))
print('② 路径为什么不能用加号拼')
m01 = os.path.join(root, 'sub')
print(' 加号拼 ', m01 + '/deep')
print(' join ', os.path.join(m01, 'deep'))
print(' 两者字符串相等?', (m01 + '/deep') == os.path.join(m01, 'deep'))
print(' os.sep =', repr(os.sep))
print('③ 拆路径的四个零件')
p = os.path.join(root, 'sub', 'deep', 'c.txt')
print(' 完整路径 ', p)
print(' dirname ', os.path.dirname(p))
print(' basename ', os.path.basename(p))
print(' splitext ', os.path.splitext(p))
print(' exists ', os.path.exists(p))
print('④ 走一遍整棵树')
print(' os.walk 给出的三样:目录路径、子目录列表、文件列表')
for d, dirs, files in os.walk(root):
print(' %-24s 子目录 %s 文件 %s'
% (os.path.relpath(d, root), dirs, files))
print('⑤ 文件的大小和时间')
f = os.path.join(root, 'a.txt')
st = os.stat(f)
print(' getsize %d 字节' % os.path.getsize(f))
print(' getmtime %s' % datetime.fromtimestamp(st.st_mtime).strftime('%Y-%m-%d %H:%M:%S'))
print(' 权限位 %o(八进制,666 就是所有人可读可写)' % stat.S_IMODE(st.st_mode))
print('⑥ 建、改、删')
newd = os.path.join(root, 'backup')
os.makedirs(newd, exist_ok=True)
os.makedirs(newd, exist_ok=True)
print(' makedirs(exist_ok=True) 建成 True(跑两次都不报错)')
os.rename(f, os.path.join(root, 'a_renamed.txt'))
print(' rename → a.txt 还在?%s a_renamed 在?%s'
% (os.path.exists(f), os.path.exists(os.path.join(root, 'a_renamed.txt'))))
os.remove(os.path.join(root, 'a_renamed.txt'))
print(' remove 之后 a_renamed 在?%s' % os.path.exists(os.path.join(root, 'a_renamed.txt')))
shutil.rmtree(root)
print(' rmtree 之后 root 在?%s ← 这条没有回收站' % os.path.exists(root))
实跑输出:
--------------------------------------------------------------------
os:文件夹和路径
--------------------------------------------------------------------
① 当前在哪、里面有什么
--------------------------------------------------------------------
os.getcwd() = I:\Desktop\自我教学\.workbuddy\scripts\ch58_dev
os.listdir(d) = ['a.txt', 'sub']
listdir 只给名字,不给完整路径,而且不区分文件和文件夹。想区分就得再判断一次。
② 路径为什么不能用加号拼
--------------------------------------------------------------------
加号拼 ts\ch58_dev\_tmp58\m01/sub
join ts\ch58_dev\_tmp58\m01\sub
两者字符串相等?False
os.sep = '\\'
这次两个路径都能打开,但字符串并不相等:一个用正斜杠,一个用反斜杠。
Windows 两种都认,所以这个坑在这里看不太出来。
真正会出事的是三种情况:源目录末尾本来就带斜杠,拼出来变成两层;换到 Linux 上反斜杠不是分隔符;
路径里有中文或空格时拼接顺序一错就整段失效。join 会自己处理分隔符,也能接多个参数,永远用它。
③ 拆路径的四个零件
--------------------------------------------------------------------
完整路径 scripts\ch58_dev\_tmp58\m01\sub\deep\c.txt
dirname pts\ch58_dev\_tmp58\m01\sub\deep
basename c.txt
splitext ('c', '.txt')
exists True
splitext 分的是「最后那个点」,返回 (名字, 扩展名)。文件名里带点也没关系,它只认最后一个。
④ 走一遍整棵树
--------------------------------------------------------------------
os.walk 给出的三样:目录路径、子目录列表、文件列表
. 子目录 ['sub'] 文件 ['a.txt']
sub 子目录 ['deep'] 文件 ['b.txt']
sub\deep 子目录 [] 文件 ['c.txt']
walk 是自上而下递归,一次给你一层。
要统计「这个文件夹里所有文件」,外层 for 加上内层 for 就完了,不用自己写递归。
⑤ 文件的大小和时间
--------------------------------------------------------------------
getsize 1 字节
getmtime 2026-11-02 02:46:49
权限位 666(八进制,666 就是所有人可读可写)
修改时间是个时间戳(从 1970 年起的秒数),要给人看就得 fromtimestamp 转一下。
下一节的备份脚本全靠这个数判断新旧。
⑥ 建、改、删
--------------------------------------------------------------------
makedirs(exist_ok=True) 建成 True
rename → a.txt 还在?False a_renamed 在?True
remove 之后 a_renamed 在?False
exist_ok=True 很关键:目录已经存在时不报错。
不加这个参数,第二次跑同一个脚本就会崩在「文件已存在」上。
makedirs 会一路建上去,mkdir 只能建一层。
删目录用 rmdir(只能删空目录),要连内容一起删是 shutil.rmtree——这条命令没有回收站,
用之前想清楚。
第一段那个 listdir 只给了两个名字。光看字符串,分不出 a.txt 是文件、sub 是目录,得再判断一次。 第二段是本篇最重要的一处。两个路径都能打开,但字符串不相等:一个正斜杠一个反斜杠。 Windows 两种都认,所以这儿看不太出来,换到 Linux 就出事。 第三段 splitext 分的是最后一个点。文件名里带点它不管,只认最后那个。 第四段那三层结果要竖着看:每一行都是「一层」。 walk 是自上而下递归,一次给你一层,外层 for 加内层 for 就走完了。 第五段那个 666 是八进制,Windows 上只是象征性的值。真要管权限得在 Linux 上做。 第六段两次 makedirs 都不报错,靠的是 exist_ok=True。 最后那句 rmtree 是本篇唯一不可撤销的操作,写脚本时一定要加 dry-run 开关。
30 秒实验
- 把演示第二段的
m01 + '/deep'改成m01 + os.sep + 'deep',再看两个字符串相不相等。 - 把
os.walk(root)改成os.walk(root, topdown=False),看三层的顺序变成什么样。 - 在演示第五段加一行
print(os.stat(f).st_mode),跟stat.S_IMODE的结果比一比, 看多出来的那几位是什么。
常见误区
- 「路径用加号拼一下就行。」Windows 碰巧认,Linux 不认。
os.path.join会自己处理分隔符,而且能接多个参数。 永远用它。 - 「
listdir能分出文件和文件夹。」它只给名字。 要区分就再判断一次,用os.path.isdir或直接换os.scandir。 - 「删目录就是
shutil.rmtree。」它没有回收站。 这是自动化脚本里唯一一个跑错没法撤回的操作。先打印路径、加--dry-run确认,再真删。
自测三问(点开对答案)
1. `'…/m01' + '/deep'` 和 `os.path.join('…/m01', 'deep')`,两个路径都能打开,为什么还要用后者? —— 因为能打开只是 Windows 宽容,字符串本身已经错了:一个正斜杠一个反斜杠。 换到 Linux 上反斜杠不是分隔符,整段会失效;另外源目录末尾带斜杠时,加号会拼出两层斜杠。 `join` 把这些都处理了。 2. `os.makedirs(p, exist_ok=True)` 里那个 `exist_ok` 不加会怎样? —— 目录已经存在时会抛 `FileExistsError`。后果是同一个脚本第二次跑就崩。 加了它,存在就跳过,脚本可以反复跑——这一点在定时任务里是必须的。 3. `os.path.getmtime` 打出来是一串十几亿的整数,那是什么? —— 时间戳:从 1970 年 1 月 1 日算起过了多少秒。 要给人看得用 `datetime.fromtimestamp()` 转成本地时间。下一篇的备份脚本就靠比这个数判断新旧。下一篇把这些零件拼起来:一个只复制改过文件的备份脚本,再加上命令行参数。
- ☐ (本篇完,下一篇 4.2 argparse 处理命令行参数)