目录同步
# 目录同步
filecmp 比较目录差异、增量/全量同步、rsync 封装、排除模式。
# 同步的三种粒度
目录同步是一个经典的"比较—决策—执行"问题。根据比较粒度不同,有三种策略:
| 策略 | 比较方式 | 适用场景 | 准确性 |
|---|---|---|---|
| 时间戳 | st_mtime | 文件数量少、无跨时区 | ⚠️ 时钟偏差会导致误判 |
| 大小+时间戳 | st_size + st_mtime | 一般文件同步 | 较好,但内容相同时间不同的文件会重传 |
| 内容哈希 | MD5/SHA256 | 需要精确保证一致性 | 最准确,但计算成本高 |
stat结构体字段:每次调用os.stat()返回文件的元数据。关键字段:st_size(字节数)、st_mtime(最后修改时间——Unix 时间戳浮点数)、st_ctime(元数据变更时间/Windows 创建时间)。注意st_mtime的精度在 ext4 上是纳秒级,但 Pythonos.path.getmtime返回浮点秒——比较时应避免==,用<=容忍精度误差。
# 一、filecmp——目录差异分析
filecmp.dircmp 内部使用两级过滤:先比 os.stat() 元数据(类型+大小+时间),对同名且同大小的文件才计算 hashlib 浅哈希(shallow=True)进行比较。注意这不同于文件去重的"两级过滤"——这里的目的是找出差异,而非发现重复。
#!/usr/bin/env python3
import filecmp, os
def diff_dirs(dir1, dir2):
"""比较两个目录——filecmp 底层用 stat 做快速过滤"""
cmp = filecmp.dircmp(dir1, dir2)
for f in cmp.left_only:
print(f" 只在左: {f}")
for f in cmp.right_only:
print(f" 只在右: {f}")
for f in cmp.diff_files:
print(f" 内容不同: {f}")
print(f"\n 相同文件: {len(cmp.same_files)} 个")
for sub in cmp.subdirs:
print(f"\n--- 子目录: {sub} ---")
diff_dirs(os.path.join(dir1, sub), os.path.join(dir2, sub))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 二、shutil——双向同步
增量同步的原理:对源目录中每个文件,检查目标目录中是否存在同名文件且大小相同 + 修改时间不更新。满足条件则跳过(未变化),否则复制。这是一个启发式算法——无法检测文件内容被"还原"为旧版本但时间戳更新的情况,但覆盖了 99% 的实际场景。
shutil.copy2 相比 shutil.copy 额外保留 st_mode(权限)和 st_mtime(修改时间)——这对增量同步很关键:如果复制后时间戳变了,下次同步时会误判为"需要更新"。
#!/usr/bin/env python3
import shutil, os
from pathlib import Path
def sync_dirs(src, dst, delete_extra=False):
"""增量同步——只复制大小/时间不同的文件"""
src, dst = Path(src), Path(dst)
copied, skipped = 0, 0
for s in src.rglob('*'):
rel = s.relative_to(src)
d = dst / rel
if s.is_dir():
d.mkdir(parents=True, exist_ok=True)
else:
if d.exists():
ss, ds = s.stat(), d.stat()
# 大小相同且修改时间不更新 → 跳过
if ss.st_size == ds.st_size and ss.st_mtime <= ds.st_mtime:
skipped += 1; continue
d.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(s, d) # copy2 保留 mtime + permissions
copied += 1
# 镜像模式:删除目标中多余的文件(先处理新增,后删除——减少风险)
if delete_extra:
for d in dst.rglob('*'):
if not (src / d.relative_to(dst)).exists():
if d.is_file(): d.unlink()
elif d.is_dir(): shutil.rmtree(d)
print(f"✅ 复制 {copied} 个,跳过 {skipped} 个")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 三、rsync 封装
rsync 为什么快? 它的核心是 rolling checksum(滚动校验和) 算法:不直接比较两个完整文件的 MD5,而是将文件分成固定大小的块,逐块计算弱校验和(adler-32)+ 强校验和(MD5),只传输块哈希不匹配的部分。这意味着修改一个大文件中的一行——只传那个块,不重传整文件。这是 rsync 与其他同步工具的本质区别。
src.rstrip('/') + '/' 的含义:源路径末尾的 / 告诉 rsync 同步的是"内容"而非"目录本身"。没有 / 会在目标创建 src 子目录。
#!/usr/bin/env python3
import subprocess
def rsync_sync(src, dst, exclude=None, dry_run=False):
cmd = ['rsync', '-avz', '--delete']
if dry_run: cmd.append('--dry-run')
if exclude:
for pattern in exclude:
cmd.extend(['--exclude', pattern])
cmd.append(f"{src.rstrip('/')}/"); cmd.append(dst)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("✅ 同步完成")
else:
print(f"❌ 失败: {result.stderr}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 四、Shell 命令
#!/bin/bash
rsync -avz --delete /src/ /dst/ # 增量镜像(最常用)
rsync -avzn /src/ /dst/ # -n dry-run 预览变更
rsync -avz --ignore-existing /src/ /dst/ # 只复制新文件(不更新已有)
2
3
4
5