是否有可能将fnmatch.translate(“** / * .py”)转换为语义正确的正则表达式?

时间:2015-03-30 18:58:48

标签: python regex

有没有简单的方法(或可能)将fnmatch.translate的输出转换为匹配零个或多个目录的正则表达式?

>>> import fnmatch
>>> fnmatch.translate("**/*.py")
'.*.*\\/.*\\.py\\Z(?ms)'

我想要的是将___放在

中的正则表达式片段
dirs = ['a.py', 'b/a.py', 'b/c/a.py', 'b/c/d/a.py']
r = fnmatch.translate("**/*.py").replace(".*.*", "___")

这样

[d for d in dirs if re.match(r, d)] == dirs

这种尝试在前两个中并不匹配:

fnmatch.translate("**/*.py").replace('.*.*', "(.*/.*)*")

这与第一个匹配:

fnmatch.translate("**/*.py").replace('.*.*', "(.*/?)*")

这使解释器挂起:

fnmatch.translate("**/*.py").replace('.*.*', "(.*|(.*/.*))*")

我很满意答案,解释为什么它也不可能......

更新:用空字符串替换**/并不起作用:

dirs = ['a.py', 'b/a.py', 'b/c/a.py', 'b/c/d/a.py']

def translate(pat, repl=None):
    r = fnmatch.translate(pat)
    if repl:
        r = r.replace(".*.*", repl)
    r = re.compile(r)
    return [d for d in dirs if r.match(d)]

>>> print translate("**/*.py".replace("**/", ""))
['a.py', 'b/a.py', 'b/c/a.py', 'b/c/d/a.py']  # correct
>>> print translate("b/**/*.py".replace("**/", ""))
['b/a.py', 'b/c/a.py', 'b/c/d/a.py']  # correct
>>> print translate("b/**/b.py".replace("**/", ""))
['b/a.py']  # incorrect

2 个答案:

答案 0 :(得分:2)

实际上 - 您根本不需要更改.*.*,只需更改一个.*;相反,您需要将单一,未配对的 .*更改为[^/]*

因此,所需的实际翻译表是 - 根本不使用fnmatch.translate(),而是自己滚动:

`?` -> `.`
`.` -> `[.]`
`**/` -> `(?:.*/)?`
`*` -> `[^/]*`

正确处理[!foo]及其同类最好从上游translate代码中复制逻辑。


应用这些规则会将**/*.py转换为(?:.*/)?[^/]*[.]py,它会与您问题中的所有四个名称相匹配:

>>> import re
>>> dirs = ['a.py', 'b/a.py', 'b/c/a.py', 'b/c/d/a.py']
>>> py_re = re.compile('(?:.*/)?[^/]*[.]py')
>>> [ 'Matches' if py_re.match(x) else 'Non-Matching' for x in dirs ]
['Matches', 'Matches', 'Matches', 'Matches']

变换的实现:

def build_re(glob_str):
    opts = re.compile('([.]|[*][*]/|[*]|[?])|(.)')
    out = ''
    for (pattern_match, literal_text) in opts.findall(glob_str):
        if pattern_match == '.':
            out += '[.]'
        elif pattern_match == '**/':
            out += '(?:.*/)?'
        elif pattern_match == '*':
            out += '[^/]*'
        elif pattern_match == '?':
            out += '.'
        elif literal_text:
            out += literal_text
    return out

答案 1 :(得分:2)

这是Charles'的非正则表达式实现。翻译算法。

update1:​​此版本处理前导!作为整个模式的否定,类似于node.js' minimatch(https://github.com/isaacs/minimatch#comparisons-to-other-fnmatchglob-implementations)。

def charles(pat):
    r = ""
    negate = int(pat.startswith('!'))
    i = negate

    while i < len(pat):
        if pat[i:].startswith('**/'):
            r += "(?:.*/)?"
            i += 3
        elif pat[i] == "*":
            r += "[^/]*"
            i += 1
        elif pat[i] == ".":
            r += "[.]"
            i += 1
        elif pat[i] == "?":
            r += "."
            i += 1
        else:
            r += pat[i]
            i += 1

    def match(d):
        m = re.match(r, d)
        return not m if negate else m

    return [d for d in dirs if match(d)]

print charles("**/*.py")
print charles("b/**/*.py")
print charles("b/**/a.py")
print charles("!b/**/a.py")

打印

# (?:.*/)?[^/]*[.]py
['a.py', 'b/a.py', 'b/c/a.py', 'b/c/d/a.py']
# b/(?:.*/)?[^/]*[.]py
['b/a.py', 'b/c/a.py', 'b/c/d/a.py']
# b/(?:.*/)?a[.]py
['b/a.py', 'b/c/a.py', 'b/c/d/a.py']
# b/(?:.*/)?a[.]py
['a.py']

update2:处理否定fnmatch的处理方式有点复杂(基本上fnmatch对待[!...]类似于正则表达式[^...]) :

def translate(pat):
    r = ""
    i = 0
    L = len(pat)

    while i < L:
        if pat[i:].startswith('**/'):
            r += "(?:.*/)?"
            i += 3
        elif pat[i] == "*":
            r += "[^/]*"
            i += 1
        elif pat[i] == ".":
            r += "[.]"
            i += 1
        elif pat[i] == "?":
            r += "."
            i += 1
        elif pat[i] == '[':
            i += 1
            j = i
            if j < L and pat[j] == '!':
                j += 1
            if j < L and pat[j] == ']':  # make sure [!] => \[\!\]
                j += 1
            while j < L and pat[j] != ']':
                j += 1
            if j >= L:
                r += '\\['  # didn't find a closing ']', backtracking
            else:
                stuff = pat[i:j].replace('\\', '\\\\')
                i = j+1
                if stuff[0] == '!':
                    stuff = '^' + stuff[1:]  # translate negation
                elif stuff[0] == '^':
                    stuff = '\\' + stuff     # quote ^ character
                r = '%s[%s]' % (r, stuff)
        else:
            r += re.escape(pat[i])
            #r += pat[i]
            i += 1
    r += '\\Z(?ms)'
    return r

(否定代码几乎逐字逐句地从fnmatch中偷走了。)