77人参与 • 2026-10-11 • Python
在 python 中,fnmatch 模块常被用来做文件名匹配。它实现了 unix shell 风格的通配符规则,允许你用 *.txt、data_?.csv 这样的模式来筛选文件名。与 glob 不同,fnmatch 不负责遍历目录,它只做纯粹的字符串匹配,因此更加灵活,也更容易被误用。很多开发者在使用 fnmatch 时都会遇到诡异的“漏匹配”或“过度匹配”:明明写了 *.py,却把 file.pyc 也算了进去;模式中的方括号被当成了普通字符,而不是字符集;在 windows 上能匹配的模式,在 linux 上却匹配不到。更令人困惑的是,fnmatch 到底区分大小写吗?它的行为为什么在不同平台上不一样?
这些问题的根源,在于 fnmatch 所遵循的匹配规则继承自操作系统的文件名匹配惯例,以及它内置的“大小写规范化”行为。今天,我们就来彻底解剖 fnmatch 的匹配机制,弄清它与 glob、re 的关系,并教你如何安全、精确地进行文件名匹配。

import fnmatch
print(fnmatch.fnmatch('file.pyc', '*.py'))
# 输出:false(正常)
等等,这看起来没问题。但如果你在 windows 上使用 fnmatch,结果可能不同?实际上,fnmatch 的 * 匹配任意字符,包括扩展名中的多余字符。*.py 只会匹配以 .py 结尾的字符串。所以 file.pyc 不会匹配。真正的问题往往出在模式本身写错,比如 *py 会匹配 file.pyc。
import fnmatch
print(fnmatch.fnmatch('文件.txt', '??.txt'))
# 输出:false,因为 '文件' 是两个字符,但 '?' 在 python 3 中匹配一个 unicode 字符
# 实际上在 python 3 中,'?' 匹配一个 unicode 码点,所以 '??.txt' 可以匹配 '文件.txt'
print(fnmatch.fnmatch('文件.txt', '??.txt')) # true
这里的问题在于,很多人以为 ? 匹配一个字节,但在 python 3 中它匹配一个字符(unicode 码点)。对于中文字符,通常一个汉字就是一个码点,所以 ?? 可以匹配两个汉字。
import fnmatch
print(fnmatch.fnmatch('d.txt', '[!abc].txt')) # true
print(fnmatch.fnmatch('a.txt', '[!abc].txt')) # false
这看起来正常。但如果模式中包含了 ] 或 - 等特殊字符,需要小心转义。
import fnmatch
print(fnmatch.fnmatch('file.txt', '*.txt'))
# 在 linux 上:false
# 在 windows 上:true
这是 fnmatch 最令人困惑的行为之一。在 windows 上,fnmatch 默认使用 os.path.normcase 对文件名和模式进行规范化,将大写转为小写,因为 windows 文件系统不区分大小写。在 linux 上则不进行转换,因此匹配是大小写敏感的。这导致同一段代码在不同平台上结果不同。
import fnmatch
import glob
# fnmatch 只匹配字符串,不访问文件系统
print(fnmatch.fnmatch('data/file.txt', '*.txt')) # true,因为 * 匹配包括 / 在内的任意字符
# 注意:fnmatch 的 * 会匹配路径分隔符,而 glob 的 * 不会
fnmatch 中的 * 会匹配任意字符,包括路径分隔符。因此 fnmatch('data/file.txt', '*.txt') 返回 true。这常常导致误匹配。而 glob 的 * 不匹配路径分隔符,因此 glob.glob('*.txt') 不会匹配 data/file.txt。
import fnmatch
print(fnmatch.fnmatch('file.txt', '*.txt')) # 平台相关
print(fnmatch.fnmatchcase('file.txt', '*.txt')) # 总是 false(区分大小写)
fnmatchcase 强制区分大小写,不受平台影响。如果你需要跨平台一致的行为,应该使用 fnmatchcase 或手动规范化。
fnmatch.filter(names, pattern) 是 [n for n in names if fnmatch(n, pattern)] 的优化版本,但很多人不知道它也会受平台大小写规则影响。fnmatch.translate(pattern) 返回一个正则表达式字符串,用于自定义匹配,但转义规则可能出乎意料。
fnmatch 支持以下通配符:
| 通配符 | 含义 |
|---|---|
* | 匹配任意数量的字符,包括零个,包括路径分隔符 |
? | 匹配任意单个字符(unicode 码点) |
[seq] | 匹配 seq 中的任意一个字符,支持范围 [a-z] |
[!seq] | 匹配不在 seq 中的任意字符 |
fnmatch.fnmatch(name, pattern) 在匹配前会对 name 和 pattern 都调用 os.path.normcase()。在 unix 上,normcase 返回原字符串;在 windows 上,它将所有字符转为小写,并将 / 转为 \。因此,在 windows 上,fnmatch 是大小写不敏感的,且路径分隔符被统一。
fnmatch.fnmatchcase(name, pattern) 不做任何规范化,直接进行大小写敏感的匹配。
fnmatch.translate(pattern) 将通配符模式转换为正则表达式。例如:
fnmatch.translate('*.txt')
# '(?s:.*\\.txt)\\z'
注意 * 被转换为 .*,并使用了 (?s:) 使 . 匹配换行符。\z 表示字符串末尾。这使得 * 可以匹配包括 / 在内的任何字符。
glob 模块内部使用 fnmatch 的 translate 和 filter 来匹配文件名。但 glob 在遍历目录时,会将模式按路径分隔符拆分成多个部分,分别匹配每一级,从而使得 * 不跨越分隔符。而直接使用 fnmatch 时,* 会跨越分隔符,这是两者的关键区别。
[!abc] 是排除字符集,等价于正则中的 [^abc]。[]] 匹配 ] 本身。[a-z] 匹配小写字母。- 出现在字符集开头或结尾,它被当作字面量。[、]、!)需要小心处理。fnmatch 没有提供内置的转义机制。如果你想匹配字面量 *、?、[,需要将它们放在字符集中,例如 [*] 匹配 *,[?] 匹配 ?,[[] 匹配 [。
fnmatch.fnmatch('/home/user/file.txt', '*.txt') # true!因为 * 匹配了 /
如果你只想匹配文件名部分,应该先用 os.path.basename() 提取文件名:
fnmatch.fnmatch(os.path.basename('/home/user/file.txt'), '*.txt') # true
# 但对于 '/home/user/file.txt',basename 是 'file.txt',所以匹配
# 如果要避免跨越路径,应确保模式中不包含路径分隔符,或对路径进行拆分
# 在 linux 上
fnmatch.fnmatch('readme.md', 'readme.*') # false
# 在 windows 上
fnmatch.fnmatch('readme.md', 'readme.*') # true
跨平台代码应使用 fnmatchcase 并显式处理大小写,或者统一规范化。
names = ['file.txt', 'file.txt'] fnmatch.filter(names, '*.txt') # 在 windows 上返回两个,在 linux 上只返回第二个
fnmatch.fnmatch('file[1].txt', 'file[1].txt') # false,因为 [1] 被解释为字符集
# 正确写法:
fnmatch.fnmatch('file[1].txt', 'file[[]1].txt') # 匹配字面量
在 python 3 中,? 匹配一个 unicode 码点,因此对于组合字符(如 é 可能由 e + 重音符号组成),? 可能只匹配其中的一个码点,导致匹配失败。如果需要按字素簇匹配,需要更复杂的处理。
fnmatch.translate('[!a]')
不同 python 版本的输出可能略有不同,不要依赖其精确格式。
fnmatch 本身不关心隐藏文件规则,它只是字符串匹配。* 可以匹配以 . 开头的文件名。但在 glob 中,* 不匹配隐藏文件,这是因为 glob 在目录遍历时过滤了隐藏文件。因此,直接使用 fnmatch 匹配文件名时,* 会匹配 .hidden。这是另一个 fnmatch 与 glob 的重要区别。
basename。glob 或 pathlib,而不是 fnmatch。import os
import fnmatch
filename = os.path.basename(path)
if fnmatch.fnmatch(filename, '*.py'):
...
import fnmatch
import os
def match_case_insensitive(name, pattern):
return fnmatch.fnmatchcase(name.lower(), pattern.lower())
# 或者统一使用 fnmatchcase,并在需要时手动规范化
fnmatch.fnmatchcase(os.path.normcase(name), os.path.normcase(pattern))
def escape_fn_pattern(s):
return s.replace('*', '[*]').replace('?', '[?]').replace('[', '[[]')
pattern = escape_fn_pattern('file[1].txt')
fnmatch.fnmatch('file[1].txt', pattern) # true
import fnmatch files = ['a.py', 'b.txt', 'c.pyc'] py_files = fnmatch.filter(files, '*.py') # ['a.py']
注意平台大小写差异。
import fnmatch
import re
regex = re.compile(fnmatch.translate('*.txt'))
if regex.match('file.txt'):
...
这允许你结合正则的其他功能。
如果模式不应匹配路径分隔符,可以使用 pathlib 的 purepath.match:
from pathlib import purepath
p = purepath('/home/user/file.txt')
print(p.match('*.txt')) # true,匹配文件名部分
print(p.match('*/*.txt')) # true
purepath.match 从右向左匹配,且 * 不跨越路径分隔符。
如果你需要确定的行为,始终使用 fnmatchcase,并在需要大小写不敏感时自己处理。
fnmatch 不是为遍历设计的,遍历应使用 glob、pathlib 或 os.walk。
打印 fnmatch.translate(pattern),查看实际使用的正则表达式,理解匹配逻辑。
在不同平台上测试:在 linux 和 windows 上运行相同的匹配,观察差异。
使用 fnmatchcase 验证大小写影响。
对特殊字符进行单元测试:包含 [、]、*、? 的文件名。
比较 fnmatch 与 glob 的结果:理解路径分隔符的处理差异。
使用 pathlib.purepath.match 作为现代替代。
fnmatch 用于纯字符串匹配,不应用于路径遍历。os.path.basename()。fnmatchcase 并显式处理大小写,避免依赖 fnmatch 的平台规范化。* 会匹配路径分隔符,除非你使用的是 pathlib.purepath.match。[*]、[?]、[[] 转义特殊字符。fnmatch.filter 是批量筛选的便捷方式,但受平台大小写影响。pathlib.purepath.match。fnmatch 做路径校验,因为它不感知目录边界。fnmatch 是一个小巧而实用的模块,它把 unix shell 的通配符规则带到了 python 的字符串世界。但它也继承了 shell 的“脾性”:大小写规范化因平台而异,* 不区分路径分隔符,隐藏文件没有特殊待遇。理解这些特性,你就能在文件名匹配时做出正确的选择:是使用 fnmatch 的便捷,还是转向 pathlib 的现代与精确。从今天起,当你需要匹配文件名时,请先问自己:我要匹配的是文件名还是路径?需要跨平台一致吗?* 应该跨越目录吗?回答这些问题,你就能选出最合适的工具,让每一次匹配都精准无误,不再被“漏网之鱼”或“误捕之虾”困扰。
以上就是python fnmatch模块文件名匹配的隐形规则与常见陷阱详解的详细内容,更多关于python fnmatch文件名匹配的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论