fnmatch和使用`**`进行递归路径匹配

9

有没有内置的或直接的方法来使用双星号递归地匹配路径,例如zsh所做的那样?

例如,使用

path = 'foo/bar/ham/spam/eggs.py'

我可以使用fnmatch来进行测试

fnmatch(path, 'foo/bar/ham/*/*.py'

尽管如此,我想能够做到:
fnmatch(path, 'foo/**/*.py')

我知道 fnmatch 将其模式映射到正则表达式, 所以在这种情况下我可以使用额外的 ** 模式来制作自己的 fnmatch,但也许有更简单的方法。

类似于glob.glob的东西? - g.d.d.c
这是一个允许使用 fnmatch * 和 ** 的分支 https://pypi.python.org/pypi/pywildcard - Alberto Galera
您可能想查看我对类似问题的回答。它提供了一个稍微修改过的 fnmatch.translate() 版本,支持 ** 通配符,并防止 * 匹配跨目录边界。 - Mathew Wicks
4个回答

8

想要一个适用于路径的fnmatch变体,可以使用一个名为 wcmatch 的库,它实现了一个名为globmatch的函数,该函数使用与 glob 在文件系统中爬行相同的逻辑来匹配路径。您可以使用标志控制启用的功能,在本例中,我们启用了GLOBSTAR(使用**进行递归目录搜索)。

>>> from wcmatch import glob
>>> glob.globmatch('some/file/path/filename.txt', 'some/**/*.txt', flags=glob.GLOBSTAR)
True

谢谢!正是我想要的 :) - Toilal
3
很高兴看到人们发现这个库很有用 : )。我花了很多时间来开发它。 - facelessuser
继续保持好的工作,我也是一个开源作者/维护者 :) - Toilal

8

如果你仔细查看fnmatch源代码,它会将模式内部转换为正则表达式,并将*映射为.*(而不是[^/]*或类似),因此不需要处理目录分隔符/——与UNIX shell不同:

while i < n:
    c = pat[i]
    i = i+1
    if c == '*':
        res = res + '.*'
    elif c == '?':
        res = res + '.'
    elif c == '[':
        ...

因此
>>> fnmatch.fnmatch('a/b/d/c', 'a/*/c')
True
>>> fnmatch.fnmatch('a/b/d/c', 'a/*************c')
True

1
一个陷阱是 foo/**/bar 会匹配 foo/bar,但 foo/.*/bar 不会匹配它。你可以通过在原始 glob 中替换 **/,将其转换为正则表达式(fnmatch.translate),然后用 (.*/)* 替换占位符来解决这个问题,这似乎是 **/ 的语义。 - Masklinn

3
如果您可以不使用os.walk循环,尝试以下方法:

glob2

formic

我个人使用glob2:
import glob2
files = glob2.glob(r'C:\Users\**\iTunes\**\*.mp4')

附录:

从Python 3.5开始,原生的glob模块支持递归模式匹配:

import glob
files = glob.iglob(r'C:\Users\**\iTunes\**\*.mp4', recursive=True) 

这个问题涉及到处理路径(可能是虚拟的),而glob则是扫描实际文件系统。 - Victor Gavro

1
这个片段增加了关于 ** 的兼容性。
import re
from functools import lru_cache
from fnmatch import translate as fnmatch_translate


@lru_cache(maxsize=256, typed=True)
def _compile_fnmatch(pat):
    # fixes fnmatch for recursive ** (for compatibilty with Path.glob)
    pat = fnmatch_translate(pat)
    pat = pat.replace('(?s:.*.*/', '(?s:(^|.*/)')
    pat = pat.replace('/.*.*/', '.*/')
    return re.compile(pat).match


def fnmatch(name, pat):
    return _compile_fnmatch(str(pat))(str(name)) is not None

这在py3.9中似乎不起作用。看起来fnmatch现在会过滤掉连续的星号字符。 - Mouscellaneous

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接