- 桌面应用
【免费下载链接】dupeguru
Find duplicate files
hscommon/conflict.py是 dupeGuru 项目中专门处理"目标位置文件名冲突"的通用工具模块:它以[000]形式的方括号编号前缀为核心协议,对外提供get_conflicted_name、get_unconflicted_name、is_conflicted三个命名工具函数,以及smart_move/smart_copy两个带冲突消解的移动/复制操作。本文结合 hscommon/conflict.py、hscommon/tests/conflict_test.py 以及它在 core/app.py、core/results.py 中的真实调用点,系统讲解其命名规则、边界行为、底层实现原理与在 dupeGuru 去重工作流中的实战位置。
一、模块定位:dupeGuru 中"名字冲突"的统一解决方案
在 dupeGuru 的实际使用场景中,"把重复文件移动/复制到某个目标文件夹"是最核心的操作之一。当目标目录里已经存在同名文件时,直接覆盖会造成数据丢失,直接报错则会让批处理流程中断。hscommon.conflict模块给出的答案是:不覆盖、不报错,而是自动为目标文件名加上一个[000]形式的方括号编号前缀,从而让每个文件都能落盘。
模块文档字符串(hscommon/conflict.py)给出了这一设计的官方定义:
"When you have to deal with names that have to be unique and can conflict together, you can use this module that deals with conflicts by prepending unique numbers in
[]brackets to the name."
即:当你需要处理"必须唯一、但可能相互冲突"的名字时,可以借助本模块,通过向名字前置[]方括号内的唯一编号来消解冲突。
这一设计有两个关键特点:
- 幂等可逆:
get_conflicted_name生成的前缀可以通过get_unconflicted_name干净地剥除,恢复到原始文件名; - 对用户友好:
[000]前缀直观表明了这是一个冲突副本,且按[000]、[001]……递增,用户在文件管理器中一眼就能看出冲突顺序。
二、核心命名协议:[三位数字]前缀与正则定义
冲突命名协议由模块顶部的正则常量re_conflict定义(hscommon/conflict.py):
re_conflict = re.compile(r"^\[\d{3}\d*\] ")这条正则的含义与约束是:
| 正则片段 | 含义 | 约束说明 |
|---|---|---|
^ | 锚定字符串开头 | 只有出现在名字最开头的方括号编号才被识别为冲突前缀 |
\[\d{3}\d*\] | 方括号内至少 3 位数字 | 匹配[123],但不匹配[12];\d*允许 4 位及以上(如[1000]) |
| (尾随空格) | 前缀与文件名之间必须有一个空格 | [000]foo不算冲突名 |
也就是说,is_conflicted("[000] foobar")返回True,而is_conflicted("[000]foobar")(无空格)、is_conflicted("[000a] foobar")(含字母)、is_conflicted("foo [000] bar")(编号不在开头)都返回False。对应断言可在 hscommon/tests/conflict_test.py 中查到。
选择"至少 3 位数字"的原因从测试中可以看出:冲突计数可能超过 99(如[1000] bar),三位起步保证了编号的稳定递增空间,同时避免把[12]这类普通文件名误判为冲突名。
三、三个命名工具函数:生成、剥离与判定
3.1get_conflicted_name(other_names, name):生成不冲突的名字
签名(hscommon/conflict.py):
def get_conflicted_name(other_names: List[str], name: str) -> str:行为逻辑分三步:
- 自动去冲突:先对
name调用get_unconflicted_name剥离已有前缀——即使传入的名字本身就是[001] bar,也会先还原成bar再重新编号,避免前缀叠加; - 查重:如果剥离后的名字不在
other_names中,直接原样返回; - 递增编号:否则从
i = 0开始,依次尝试"[%03d] %s" % (i, name),即[000] name、[001] name……直到找到一个不在other_names中的名字。
%03d格式说明编号至少 3 位、不足补零,而超过 999 时自然扩展为 4 位(如[1000]),这也与正则\d{3}\d*的允许多位设计相呼应。
测试用例验证了以下边界(hscommon/tests/conflict_test.py):
get_conflicted_name(["bar"], "bar") # -> "[000] bar" get_conflicted_name(["bar", "[000] bar"], "bar") # -> "[001] bar" get_conflicted_name(["bar"], "foobar") # -> "foobar"(无冲突) get_conflicted_name([], "[000] foobar") # -> "foobar"(自动去冲突) get_conflicted_name(["bar"], "[001] bar") # -> "[000] bar"(先还原再编号)第 4 位数字的极端场景(1000 个冲突同名文件)也有专门测试覆盖(hscommon/tests/conflict_test.py),测试注释也坦承这种情形"几乎没有机会发生",但从协议上保证了正确性。
3.2get_unconflicted_name(name):剥离冲突前缀
签名(hscommon/conflict.py):
def get_unconflicted_name(name: str) -> str: return re_conflict.sub("", name, 1)通过re_conflict.sub("", name, 1)把名字开头匹配到的方括号编号前缀(含尾随空格)替换为空,且最多替换一次。其行为要点:
[000] foobar→foobar;[9999] foobar→foobar(多位编号同样剥离);[000]foobar→ 原样返回(无空格,不匹配协议);[000a] foobar→ 原样返回(编号含非数字);foo [000] bar→ 原样返回(前缀不在开头);foobar→ 原样返回。
这保证了只有严格符合冲突协议的名字才会被还原,普通文件名即使包含方括号也不会被误改。对应断言见 hscommon/tests/conflict_test.py。
3.3is_conflicted(name):判定是否为冲突名
签名(hscommon/conflict.py):
def is_conflicted(name: str) -> bool: return re_conflict.match(name) is not None本质就是"正则match是否命中",match天然从字符串开头尝试匹配,配合正则中的^双重锚定。True/False的判定用例与get_unconflicted_name完全对称,见 hscommon/tests/conflict_test.py。
四、智能移动与复制:smart_move/smart_copy与底层_smart_move_or_copy
命名工具之外,模块还提供了两个"开箱即用"的文件操作:smart_move(移动)与smart_copy(复制)。二者的公共逻辑集中在私有函数_smart_move_or_copy(hscommon/conflict.py):
def _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None: if dest_path.is_dir() and not source_path.is_dir(): dest_path = dest_path.joinpath(source_path.name) if dest_path.exists(): filename = dest_path.name dest_dir_path = dest_path.parent newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename) dest_path = dest_dir_path.joinpath(newname) operation(str(source_path), str(dest_path))核心逻辑分三步:
- 目录归一:如果目标是目录而源是文件,则把目标路径改写为
目标目录/源文件名(这正是"把文件移入目录"场景); - 冲突消解:如果目标路径已存在,则以目标目录的现有条目列表为
other_names,调用get_conflicted_name生成新名字,拼接出新的目标路径; - 执行操作:把最终的源/目标路径交给传入的
operation回调执行。
4.1smart_move与smart_copy的区别
smart_move(source_path, dest_path)(hscommon/conflict.py):回调为shutil.move,移动后源文件不再存在;smart_copy(source_path, dest_path)(hscommon/conflict.py):回调为shutil.copy,但带递归目录复制与降级重试——若shutil.copy抛出OSError且errno为EISDIR(Linux/macOS 的错误码 21)或EACCES(Windows 的错误码 13),说明源是目录,则改用shutil.copytree重新执行_smart_move_or_copy;其他错误则原样向上抛出。这也是 hscommon/tests/conflict_test.py 中smart_copy对文件夹同样有效的原理所在。
4.2 实战行为矩阵(来自测试验证)
以测试夹具(hscommon/tests/conflict_test.py:临时目录下有foo、bar两个文件和一个dir目录)为基准,各场景结果如下:
| 调用 | 结果 |
|---|---|
smart_move(foo, baz) | baz存在,foo消失(无冲突移动) |
smart_copy(foo, baz) | baz与foo同时存在(无冲突复制) |
smart_move(foo, dir) | dir/foo存在,foo消失(目标为目录时并入其中) |
smart_move(foo, bar) | [000] bar存在,foo消失(目标文件已存在时生成冲突名) |
连续两次把foo移入dir | dir/foo与dir/[000] foo并存(同名冲突编号递增) |
上述断言分别位于 hscommon/tests/conflict_test.py。特别值得注意的是第二次同名移动得到[000] foo而非[001] foo:因为此时目标目录中只有foo而无冲突名,编号从 0 重新开始,这与"编号相对当前目录现有条目而言"的设计一致。
五、在 dupeGuru 中的真实调用链
hscommon.conflict不是孤立工具,它支撑着 dupeGuru 两大核心流程:标记重复项的移动/复制与结果保存时的文件覆盖防护。
5.1 移动/复制重复文件(core/app.py)
core/app.py顶部导入smart_move, smart_copy(core/app.py),在copy_or_move方法中根据用户选择分发(core/app.py):
if copy: smart_copy(source_path, dest_path) else: smart_move(source_path, dest_path) self.clean_empty_dirs(source_path.parent)该方法的调用链为:用户在界面选择"移动/复制标记项" →copy_or_move_marked(core/app.py)启动异步任务并对每个被标记的重复项调用copy_or_move。目标路径的三种形态由 core/app.py 的DestType枚举决定:DIRECT(直接目标目录)、RELATIVE(重建相对路径)、ABSOLUTE(重建绝对路径)。目标目录里已存在同名文件时,冲突名由smart_copy/smart_move在底层自动解决——这正是 help/en/preferences.rst 中"dupeGuru 通过向目标文件名前置编号来优雅处理命名冲突"这一用户文档描述的实现支撑。
另外,hscommon/tests/conflict_test.py 之外,core/tests/app_test.py 还通过monkeypatch把app.smart_copy替换为hscommon.conflict.smart_copy并统计调用次数,验证了 UI 层触发与底层冲突处理模块之间的真实耦合。
5.2 结果导出文件覆盖防护(core/results.py)
core/results.py导入get_conflicted_name(core/results.py),在导出结果 XML 时,如果目标路径恰好已是一个目录导致写入失败(EISDIR/EACCES),就通过get_conflicted_name(otherfiles, basename)生成冲突名后重试(core/results.py)。这说明命名工具函数同样服务于"文件写入目标不可用时自动改名重试"这一通用场景。
六、设计要点与使用建议
- 先剥离再编号:
get_conflicted_name内部强制先get_unconflicted_name,保证了重复调用不会产生[000] [000] foo这类叠加前缀,幂等性有测试背书; - 协议识别靠正则:
is_conflicted/get_unconflicted_name都严格依赖re_conflict,任何不满足"开头 + 方括号 + ≥3 位数字 + 空格"的名字都不会被误伤; - 目录与文件统一处理:
smart_move/smart_copy对"目标为目录"自动拼接源文件名,对"源为目录"由smart_copy通过EISDIR/EACCES降级到copytree,跨平台错误码差异(Linux/macOS 的 21 与 Windows 的 13)已在源码注释中说明(hscommon/conflict.py); - 编号相对目录现状:冲突编号从 0 重新计数,只保证"与目标目录当前条目不重复",不保证全局单调,理解这一点有助于预测批量操作结果。
对于需要在自有 Python 项目中复用此模式的开发者,可直接将 hscommon/conflict.py 的三个命名函数与smart_move/smart_copy作为参考实现——它们依赖的标准库仅为re、os、shutil、errno与pathlib,无任何第三方依赖,且配套的 hscommon/tests/conflict_test.py 提供了完整的边界测试样例可直接移植。
- 桌面应用
【免费下载链接】dupeguru
Find duplicate files
相关推荐
从崩溃到修复:ImHex计算器模块括号表达式解析全解析
从崩溃到修复:ImHex计算器模块括号表达式解析全解析 你是否在使用ImHex计算器时遇到过括号表达式导致的崩溃问题?本文将深入分析这一技术难题,带你了解问题根
桌面应用开发工具逆向工程PHP-CS-Fixer `no_unneeded_braces` 规则完全指南:清理多余花括号与括号式命名空间
PHP CS Fixer no_unneeded_braces 规则完全指南:清理多余花括号与括号式命名空间 no_unneeded_braces 是 PHP
开发工具代码质量静态分析Lint格式化解决Alacritty终端Ctrl+C冲突:从复制文本到信号处理的完美配置
解决Alacritty终端Ctrl+C冲突:从复制文本到信号处理的完美配置 你是否也曾在Alacritty终端中遭遇这样的尴尬:想复制选中的文本,按下 Ctrl
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考