Python fnmatch 的路径斜杠:只写一个星号,为什么连子目录和点文件都匹配了

前天 3阅读

给日志清单加筛选条件logs/*.txt,本来只想保留logs目录下可见的文本文件,结果logs/archive/app.txt和logs/.secret.txt都通过了。fnmatch接收的是字符串,斜杠与开头的点在这个模块中并没有目录边界或隐藏文件的特殊地位。

下面在CPython 3.12.14实跑,固定使用fnmatchcase保证大小写敏感,不依赖当前系统的大小写规范化。保存为demo.py,运行python3 demo.py。样本只是内存里的逻辑路径名称,不访问真实文件,也不会打开目录。

Python fnmatch 的路径斜杠:只写一个星号,为什么连子目录和点文件都匹配了

AI模型生成概念插图:一个大匹配环处理整条路径带,多个小环分别处理目录片段;用来说明筛选粒度,不是文件管理器截图。

完整程序与本地结果

完整可运行程序

from fnmatch import fnmatchcase

names = ["logs/app.txt", "logs/archive/app.txt", "logs/.secret.txt", "logs/app.TXT"]
pattern = "logs/*.txt"
raw = [name for name in names if fnmatchcase(name, pattern)]
assert raw == names[:3]
print("whole string:", raw)

def visible_path_match(name, pattern):
    names = name.split("/")
    patterns = pattern.split("/")
    if any(part in ("", ".", "..") for part in names):
        return False
    if len(names) != len(patterns):
        return False
    return all(
        not (part.startswith(".") and not rule.startswith("."))
        and fnmatchcase(part, rule)
        for part, rule in zip(names, patterns)
    )

scoped = [name for name in names if visible_path_match(name, pattern)]
assert scoped == ["logs/app.txt"]
assert visible_path_match("logs/.secret.txt", "logs/.*.txt")
assert not visible_path_match("logs/../app.txt", "logs/*/*.txt")
print("per segment, visible only:", scoped)
print("explicit dot pattern:", visible_path_match("logs/.secret.txt", "logs/.*.txt"))
print("case sensitive:", fnmatchcase("app.TXT", "*.txt"))

本地实际输出(以下内容为程序结果)

whole string: ['logs/app.txt', 'logs/archive/app.txt', 'logs/.secret.txt']
per segment, visible only: ['logs/app.txt']
explicit dot pattern: True
case sensitive: False

星号处理的是整串字符

whole string保留了前三个名称。对于fnmatchcase,星号可以覆盖archive/app这一段,其中的斜杠并不会阻断匹配;.secret开头的点也可以被星号覆盖。第四项扩展名为大写TXT,因此在明确的大小写敏感规则下没有通过。

这不代表fnmatch执行了递归搜索。程序把四个字符串逐个传给它,它只判断每个字符串是否符合模式。输入清单来自哪里、是否存在对应文件,以及是否遍历子目录,都是调用者或其他库负责的工作。

把目录层级写成显式规则

visible_path_match先按斜杠拆开名称和模式,要求两边片段数量相同,再一段对一段比较。logs/archive/app.txt有三段,logs/*.txt只有两段,于是在通配符判断之前就被拒绝。这个检查让本例的星号只承担单层名称匹配。

函数还规定:若名称片段以点开头,对应模式也必须显式以点开头。因此logs/.secret.txt不会通过普通星号,却能通过logs/.*.txt。这里的点文件政策是我们添加的应用规则,不是fnmatchcase暗中启用的选项。

只实现已经约定的逻辑路径格式

程序拒绝空片段、单点和双点,并仅把正斜杠作为分隔符。它适合这样的受控逻辑名称:相对路径、没有重复斜杠、不带父目录跳转、大小写需要精确比较。代码没有实现双星号递归、Windows盘符或所有shell通配细节。

若需求是按真实文件系统搜索目录,应选择相应的路径遍历工具,并独立核对其隐藏文件与递归规则。若只是对已有清单作显示筛选,显式逐段匹配更容易解释。不要在两类需求之间直接替换函数,却保留同一套未经验证的假设。

匹配通过不能当作访问授权

即使某个名称通过这个函数,也没有检查符号链接、实际解析位置、租户归属或调用者权限。真实读写接口仍需要对应的路径解析和授权设计;黑名单或后缀模式都无法独立证明某个文件可以安全打开。

排错时至少准备普通文件、深层目录、点文件、大写扩展名及父目录片段。本例输出与断言把它们分别列清楚,便于将来修改规则时发现行为变化。还应在接口说明中交代筛选针对全路径还是文件名,避免同一个星号在不同入口被理解成不同层级。

补齐容易遗漏的验收条件

fnmatch与fnmatchcase的另一处区别是大小写处理。前者会经当前平台的normcase规范化名称和模式,后者直接执行区分大小写的比较。对于来自跨平台清单的逻辑名称,明确选择后者能让本例在不同机器上保留相同判断;这仍不是Unicode的任意语言大小写折叠。

名称中如果真的包含问号或星号,还要考虑模式中的字面匹配写法。例如用[?]表达一个问号,避免让它变成任意单字符规则。模式是规则输入,文件名是被匹配内容;把用户文件名直接拼到模式里,可能意外扩大选择范围。

函数中的拒绝策略同样属于接口契约:带连续斜杠的名称会被拒绝,而不是先自动合并。若上游允许其他表示,应在指定的一层统一处理并保留测试,避免先匹配后再次改写路径含义。展示筛选越接近权限或删除等动作,越需要把两者的职责分开验收。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。