我会在你的主循环里加一个"内容去重"阶段:先给每个 PDF 计算稳定的指纹(优先用文件大小+前后若干 KB 的哈希,必要时再退到整文件哈希),发现重复就删掉并跳过后续处理。接下来我直接给你一段可粘贴的改动代码(含参数开关、日志提示、以及避免误删的安全策略)。
下面给你加了"内容去重,发现一样的 PDF 就删掉(或移动)"的功能,放的位置是在每个 PDF 开始处理时,在抽题名/写 Excel/改名之前就先判重,省时间省 token 💾✂️
你只需要按 3 步把代码粘进去即可。
1) 在 import 区加 2 个库
把下面两行加到你现有 import 里(随便放在 import os 下面也行):
python
import hashlib
import shutil
2) 在"基本配置"下面加一段【去重配置 + 工具函数】
建议放在 SIZE_LIMIT_BYTES = ... 后面、# ===== PDF 依赖 ===== 前面:
python
# ===== PDF 去重配置 =====
# 逻辑:用"文件大小 + 首尾采样哈希"做快速指纹;若指纹重复,再用全文件 SHA256 二次确认(更安全)。
DEDUP_ENABLED_DEFAULT = True
# 首尾各采样多少 MB 来做快速指纹(1~4MB 通常够用)
DEDUP_SAMPLE_MB_DEFAULT = 2
# 指纹相同后,是否再做全文件 SHA256 二次确认(强烈建议 True,几乎不会误删)
DEDUP_VERIFY_FULL_DEFAULT = True
# 重复文件处理方式:
# - "delete": 直接删除(你要的)
# - "move": 移动到同目录的 _DUPLICATES 文件夹(更安全)
DEDUP_MODE_DEFAULT = "delete"
DEDUP_DIRNAME_DEFAULT = "_DUPLICATES"
def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str:
"""流式计算全文件 SHA256(不会一次性读入内存)"""
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
b = f.read(chunk_size)
if not b:
break
h.update(b)
return h.hexdigest()
def fast_pdf_fingerprint(path: Path, sample_bytes: int) -> str:
"""
快速指纹:size + sha256(head_sample) + sha256(tail_sample)
- 若文件较小(<= 2*sample_bytes),直接用全文件 sha256 做指纹(更稳)。
"""
size = path.stat().st_size
if size <= 2 * sample_bytes:
full = sha256_file(path)
return f"FULL:{size}:{full}"
with open(path, "rb") as f:
head = f.read(sample_bytes)
f.seek(max(0, size - sample_bytes))
tail = f.read(sample_bytes)
head_h = hashlib.sha256(head).hexdigest()
tail_h = hashlib.sha256(tail).hexdigest()
return f"FAST:{size}:{sample_bytes}:{head_h}:{tail_h}"
def handle_duplicate(path: Path, mode: str, dup_dirname: str) -> Tuple[bool, str]:
"""
删除/移动重复文件。返回 (success, action_text)
- Windows 上文件被占用时删除可能失败,会自动回退为 move。
"""
try:
if mode == "delete":
path.unlink()
return True, "删除"
else:
dup_dir = path.parent / dup_dirname
dup_dir.mkdir(parents=True, exist_ok=True)
dst = unique_path(dup_dir, sanitize_filename(path.stem), path.suffix)
shutil.move(str(path), str(dst))
return True, f"移动到 {dup_dirname}"
except PermissionError:
# 常见:PDF 正在被打开占用,删除失败就移动
try:
dup_dir = path.parent / dup_dirname
dup_dir.mkdir(parents=True, exist_ok=True)
dst = unique_path(dup_dir, sanitize_filename(path.stem), path.suffix)
shutil.move(str(path), str(dst))
return True, f"(占用无法删)移动到 {dup_dirname}"
except Exception as e:
return False, f"失败:{e}"
except Exception as e:
return False, f"失败:{e}"
3) 在 main() 里加参数 + 在循环里插入"去重段"
3.1 给 argparse 增加 4 个参数
在 parser.add_argument("--recursive"... 那一段下面加:
python
# ===== 去重参数 =====
parser.add_argument("--no-dedupe", action="store_true", help="关闭PDF内容去重")
parser.add_argument("--dedupe-mode", choices=["delete", "move"], default=DEDUP_MODE_DEFAULT,
help="重复PDF处理:delete直接删除;move移动到 _DUPLICATES")
parser.add_argument("--dedupe-sample-mb", type=int, default=DEDUP_SAMPLE_MB_DEFAULT,
help="快速指纹:首尾采样大小(MB)")
parser.add_argument("--no-dedupe-verify-full", action="store_true",
help="指纹相同后不做全文件SHA256二次确认(更快但更不安全)")
3.2 在读取 pdfs 之后、进入 for 循环之前,建立"已见指纹表"
在这里:
python
pdfs = list(folder.rglob("*.pdf") if args.recursive else folder.glob("*.pdf"))
下面紧接着加:
python
# ===== 去重状态表 =====
dedupe_enabled = (not args.no_dedupe) and DEDUP_ENABLED_DEFAULT
dedupe_mode = args.dedupe_mode
dedupe_sample_bytes = max(1, args.dedupe_sample_mb) * 1024 * 1024
dedupe_verify_full = (not args.no_dedupe_verify_full) and DEDUP_VERIFY_FULL_DEFAULT
seen_fp: dict[str, Path] = {} # fast fingerprint -> kept file path
seen_full: dict[str, str] = {} # fast fingerprint -> kept file full sha256(用于二次确认缓存)
dup_removed = dup_failed = 0
3.3 在 for idx, pdf in enumerate(pdfs, 1): 循环里插入去重段
把下面这段插到你循环里 print(f"\n[{idx}/{total}] ...") 后面、if size > SIZE_LIMIT_BYTES: 之前(也就是大文件跳过之前,这样大文件也能去重):
python
# 0) ===== 去重:内容相同的 PDF 直接删/移并跳过 =====
if dedupe_enabled:
try:
fp = fast_pdf_fingerprint(pdf, dedupe_sample_bytes)
if fp in seen_fp:
kept = seen_fp[fp]
is_dup = True
# 二次确认:全文件 SHA256
if dedupe_verify_full:
cur_full = sha256_file(pdf)
kept_full = seen_full.get(fp)
if not kept_full:
kept_full = sha256_file(kept)
seen_full[fp] = kept_full
is_dup = (cur_full == kept_full)
if is_dup:
ok, action = handle_duplicate(pdf, dedupe_mode, DEDUP_DIRNAME_DEFAULT)
if ok:
dup_removed += 1
print(f" - 去重:与 {kept.name} 内容相同,已{action},跳过后续处理")
else:
dup_failed += 1
print(f" - 去重:疑似重复,但处理{action},继续按非重复处理")
if ok:
continue
else:
# 极低概率:快速指纹碰撞,给它升格为 FULL key,避免误判
fp = f"{fp}|FULL:{sha256_file(pdf)}"
# 记录"保留者"(后续如果被改名了,你会更新 pdf 变量,但内容不变,所以不影响 fp)
seen_fp[fp] = pdf
except Exception as e:
print(f" [去重异常] {e}(忽略,继续处理)")
3.4 结尾统计信息里加上去重结果(可选)
你最后的汇总 print 改成这样更直观(把原来的那行替换一下):
python
print("\n" + "=" * 80)
print(f"完成:改名 {renamed},写入单元格 {filled},已是目标名跳过 {skipped_same},"
f"大文件跳过 {skipped_big},去重移除 {dup_removed}(失败 {dup_failed}),错误 {errors},总文件 {total}")
print("=" * 80)
使用建议(很实用)
- 默认就是 delete,符合你"检查到一样的PDF就删掉"的要求。
- 如果你担心误删,跑第一次可以用更安全模式:
bash
python your_script.py --dedupe-mode move
会把重复的丢到同目录 _DUPLICATES,确认无误再删。
如果你愿意,我也可以顺手把你 glob("*.pdf") 扩成同时匹配 *.PDF、以及把"同题名不同内容"的覆盖风险(改名冲突)也一并兜住。