春江暮客

春江暮客的个人学习分享网站

Python filecmp 比较文件内容:避开浅比较与缓存误判

2026-11-03 技术
Python filecmp 比较文件内容:避开浅比较与缓存误判

备份文件和原文件的大小、修改时间完全相同,Python 却把内容不同的两个文件判成了一样。原因可能不是复制失败,而是比较函数默认只做了元数据快捷判断。

本文面向初级到中级读者,复现 filecmp.cmp() 的浅比较和缓存陷阱,再写一个只读的文件内容比较命令。它区分相同、不同和检查失败,适合接在本地备份或自动化任务后面,不删除或覆盖被检查的文件。

1. 方法:先区分元数据与内容

Python 官方 filecmp 文档说明,默认 shallow=True:文件类型、大小和修改时间一致时,可以直接判为相同。显式传入 shallow=False 才跳过这条“仅凭元数据判相同”的路径;大小不同时仍可直接判不同。

本教程只比较可信本地目录里、已经停止写入的普通文件:

  1. 比较字节,不解码文本,不忽略空格、行尾或 BOM;不比较文件权限、所有者和时间戳。
  2. 使用 shallow=False,并在每轮比较前清除该进程的 filecmp 缓存。
  3. 退出码约定为 0=内容相同,1=内容不同,2=参数或检查失败。
  4. 路径可以指向普通文件的符号链接;不把它当成防越界或对抗恶意路径的工具。

如果需要看清配置中改了哪几行,用文本配置差异工具。这里回答的是“两个文件的字节是否相同”,不是文本是否等价。

2. 准备:建立演示目录

使用已有的 Python 3.9 或更新版本,无需安装第三方包。示例在 Linux、Python 3.11.4 下验证,shell 命令适用于 bash、zsh。重复实验时换一个新的目录名。

python3 --version
mkdir filecmp-demo
cd filecmp-demo

将下一节代码保存为 demo_filecmp.py。它只创建并改写当前演示目录下的 left.bin、right.bin,不要在真实文件旁直接运行。

3. 复现:相同时间戳不等于相同内容

import filecmp
import os
from pathlib import Path

left = Path("left.bin")
right = Path("right.bin")
stamp = 1_700_000_000_000_000_000


def write_at_fixed_time(path, data):
    path.write_bytes(data)
    os.utime(path, ns=(stamp, stamp))


write_at_fixed_time(left, b"alpha\n")
write_at_fixed_time(right, b"omega\n")
filecmp.clear_cache()
print("default:", filecmp.cmp(left, right))
print("deep:", filecmp.cmp(left, right, shallow=False))

write_at_fixed_time(right, b"alpha\n")
filecmp.clear_cache()
assert filecmp.cmp(left, right, shallow=False) is True
write_at_fixed_time(right, b"omega\n")
print("cached deep:", filecmp.cmp(left, right, shallow=False))
filecmp.clear_cache()
print("cleared deep:", filecmp.cmp(left, right, shallow=False))

运行:

python3 demo_filecmp.py

预期输出:

default: True
deep: False
cached deep: True
cleared deep: False

这里故意用 os.utime(..., ns=...)固定时间戳,确保两份等长文件具有相同签名。真实环境中,保留修改时间的复制操作也可能遇到这种情况,不要依赖“再等一秒”来修复比较逻辑。

cached deep: True 展示另一个问题:这个进程先比较相同内容,再把右侧改成同大小、同修改时间的不同内容;旧的深比较结果仍可能被复用。官方的 clear_cache() 说明提供了清缓存入口。因此,shallow=False 不等于完全不使用缓存。

4. 实现:只读比较命令

保存为 compare_files.py:

"""Compare two stable local regular files by content. Python 3.9+."""
import argparse
import filecmp
from pathlib import Path
import sys


def same_content(left, right):
    for path in (left, right):
        if not path.is_file():
            raise ValueError(f"not a regular file: {path}")
    # Disable metadata-only equality and discard earlier comparison results.
    filecmp.clear_cache()
    return filecmp.cmp(left, right, shallow=False)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("left", type=Path)
    parser.add_argument("right", type=Path)
    args = parser.parse_args()
    try:
        equal = same_content(args.left, args.right)
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2
    print("SAME" if equal else "DIFFERENT")
    return 0 if equal else 1


if __name__ == "__main__":
    raise SystemExit(main())

same_content() 中真正改变判断方式的是 shallow=False;clear_cache() 避免复用此前的比较结果。清缓存影响整个进程的 filecmp 缓存,因此这个小工具适合独立运行,不建议把它当成多线程共享缓存管理方案。

普通文件检查会跟随符号链接。它能挡住常见的目录或缺失文件输入,但不是并发路径替换防护;请勿对不可信、不断变化的目录运行。

5. 使用与验证:检查输出,也检查退出码

在同一个演示目录执行。cp 只用于把演示右文件改成相同内容;比较工具本身不会修改两份文件。

python3 compare_files.py --help
python3 compare_files.py left.bin right.bin; printf 'exit=%s\n' "$?"
cp left.bin right.bin
python3 compare_files.py left.bin right.bin; printf 'exit=%s\n' "$?"
python3 compare_files.py left.bin missing.bin; printf 'exit=%s\n' "$?"

除 --help 外,依次得到以下结果;错误提示写到标准错误:

DIFFERENT
exit=1
SAME
exit=0
ERROR: not a regular file: missing.bin
exit=2

以上演示预期会返回 1 和 2,不要原样放进启用了 set -e 的脚本。接入自动化时应立即保存退出码,区分“不同”与“检查失败”:

status=0
python3 compare_files.py left.bin right.bin || status=$?
case "$status" in
  0) printf 'verified same content\n' ;;
  1) printf 'content differs; review before continuing\n' ;;
  *) printf 'comparison failed; stop the job\n' >&2 ;;
esac

这是演示分支处理的片段,不会自行终止外层任务;生产脚本应在失败分支明确 exit 或返回错误。若需要持续检查目录中新增、缺失和改变的文件,可进一步使用 SHA-256 数据清单。

再把下面保存为 check_compare.py,执行 python3 check_compare.py。它使用自动清理的临时目录,检查空文件、长度不同、同内容、同路径、非 UTF-8 字节、超过 1 MiB 文件的末尾差异、缺失文件和目录输入:

from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory

script = Path("compare_files.py").resolve()


def check(left, right, status, stdout):
    result = subprocess.run(
        [sys.executable, str(script), str(left), str(right)],
        capture_output=True, text=True, timeout=10,
    )
    assert result.returncode == status, result
    assert result.stdout == stdout, result
    if status == 2:
        assert result.stderr.startswith("ERROR:"), result
    else:
        assert result.stderr == "", result


with TemporaryDirectory(prefix="filecmp-check-") as name:
    root = Path(name)
    a, b = root / "a.bin", root / "b.bin"
    a.write_bytes(b"")
    b.write_bytes(b"")
    check(a, b, 0, "SAME\n")
    a.write_bytes(b"A")
    check(a, b, 1, "DIFFERENT\n")
    b.write_bytes(b"A")
    check(a, b, 0, "SAME\n")
    check(a, a, 0, "SAME\n")
    a.write_bytes(b"\x00\xff")
    b.write_bytes(b"\x00\xfe")
    check(a, b, 1, "DIFFERENT\n")
    a.write_bytes(b"x" * 1_048_576 + b"A")
    b.write_bytes(b"x" * 1_048_576 + b"B")
    check(a, b, 1, "DIFFERENT\n")
    b.write_bytes(a.read_bytes())
    check(a, b, 0, "SAME\n")
    check(a, root / "missing.bin", 2, "")
    check(a, root, 2, "")
print("OK: 9 CLI checks")

预期输出 OK: 9 CLI checks。第三节另外验证同大小同时间戳和同进程缓存场景;不要仅凭普通同内容用例认为已经覆盖这两类问题。

6. 排错:结果与预期不一致怎么办

现象 直接处理
默认比较返回 True,但内容不同 显式使用 shallow=False,不要用修改时间代替内容检查。
深比较仍返回旧结果 在下一轮比较前执行 filecmp.clear_cache();确认写入进程已经停止。
not a regular file 检查命令的工作目录和路径;本例不接受目录、断开的符号链接或缺失文件。
PermissionError 等读取错误 使用有权限的账户和正确路径,不要通过屏蔽异常把错误变成“相同”。
肉眼看相同的文本返回 DIFFERENT 字节比较会保留 CRLF、BOM 和末尾换行差异;需要文本规范化时改用第一节的文本工具。
文件还在写,结果时好时坏 先停止写入,或比较已完成的独立副本。清缓存不提供锁,也不生成一致性快照。

对于大目录中的候选重复项,可用只读重复文件报告先筛选,而不是手动对每两个路径运行本命令。读取内容可能更新访问时间;“只读”指不改写文件内容,并不承诺所有元数据不变。

7. 总结

比较文件内容时,先明确是否允许元数据快捷判断和缓存复用。本例用 shallow=False 加每轮清缓存修正这两种行为,再用 0、1、2 区分相同、不同与失败。比较前让文件停止变化;这个命令不替代并发控制、备份完整性方案或配置语义验证。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它