Python glob but against a list of strings rather t

2020-05-29 13:24发布

I want to be able to match a pattern in glob format to a list of strings, rather than to actual files in the filesystem. Is there any way to do this, or convert a glob pattern easily to a regex?

7条回答
Luminary・发光体
2楼-- · 2020-05-29 13:30

I wanted to add support for recursive glob patterns, i.e. things/**/*.py and have relative path matching so example*.py doesn't match with folder/example_stuff.py.

Here is my approach:


from os import path
import re

def recursive_glob_filter(files, glob):
    # Convert to regex and add start of line match
    pattern_re = '^' + fnmatch_translate(glob)

    # fnmatch does not escape path separators so escape them
    if path.sep in pattern_re and not r'\{}'.format(path.sep) in pattern_re:
        pattern_re = pattern_re.replace('/', r'\/')

    # Replace `*` with one that ignores path separators
    sep_respecting_wildcard = '[^\{}]*'.format(path.sep)
    pattern_re = pattern_re.replace('.*', sep_respecting_wildcard)

    # And now for `**` we have `[^\/]*[^\/]*`, so replace that with `.*`
    # to match all patterns in-between
    pattern_re = pattern_re.replace(2 * sep_respecting_wildcard, '.*')
    compiled_re = re.compile(pattern_re)
    return filter(compiled_re.search, files)
查看更多
孤傲高冷的网名
3楼-- · 2020-05-29 13:31

While fnmatch.fnmatch can be used directly to check whether a pattern matches a filename or not, you can also use the fnmatch.translate method to generate the regex out of the given fnmatch pattern:

>>> import fnmatch
>>> fnmatch.translate('*.txt')
'.*\\.txt\\Z(?ms)'

From the documenation:

fnmatch.translate(pattern)

Return the shell-style pattern converted to a regular expression.

查看更多
再贱就再见
4楼-- · 2020-05-29 13:34

On Python 3.4+ you can just use PurePath.match.

pathlib.PurePath(path_string).match(pattern)

On Python 3.3 or earlier (including 2.x), get pathlib from PyPI.

Note that to get platform-independent results (which will depend on why you're running this) you'd want to explicitly state PurePosixPath or PureWindowsPath.

查看更多
Melony?
5楼-- · 2020-05-29 13:35

The glob module uses the fnmatch module for individual path elements.

That means the path is split into the directory name and the filename, and if the directory name contains meta characters (contains any of the characters [, * or ?) then these are expanded recursively.

If you have a list of strings that are simple filenames, then just using the fnmatch.filter() function is enough:

import fnmatch

matching = fnmatch.filter(filenames, pattern)

but if they contain full paths, you need to do more work as the regular expression generated doesn't take path segments into account (wildcards don't exclude the separators nor are they adjusted for cross-platform path matching).

You can construct a simple trie from the paths, then match your pattern against that:

import fnmatch
import glob
import os.path
from itertools import product


# Cross-Python dictionary views on the keys 
if hasattr(dict, 'viewkeys'):
    # Python 2
    def _viewkeys(d):
        return d.viewkeys()
else:
    # Python 3
    def _viewkeys(d):
        return d.keys()


def _in_trie(trie, path):
    """Determine if path is completely in trie"""
    current = trie
    for elem in path:
        try:
            current = current[elem]
        except KeyError:
            return False
    return None in current


def find_matching_paths(paths, pattern):
    """Produce a list of paths that match the pattern.

    * paths is a list of strings representing filesystem paths
    * pattern is a glob pattern as supported by the fnmatch module

    """
    if os.altsep:  # normalise
        pattern = pattern.replace(os.altsep, os.sep)
    pattern = pattern.split(os.sep)

    # build a trie out of path elements; efficiently search on prefixes
    path_trie = {}
    for path in paths:
        if os.altsep:  # normalise
            path = path.replace(os.altsep, os.sep)
        _, path = os.path.splitdrive(path)
        elems = path.split(os.sep)
        current = path_trie
        for elem in elems:
            current = current.setdefault(elem, {})
        current.setdefault(None, None)  # sentinel

    matching = []

    current_level = [path_trie]
    for subpattern in pattern:
        if not glob.has_magic(subpattern):
            # plain element, element must be in the trie or there are
            # 0 matches
            if not any(subpattern in d for d in current_level):
                return []
            matching.append([subpattern])
            current_level = [d[subpattern] for d in current_level if subpattern in d]
        else:
            # match all next levels in the trie that match the pattern
            matched_names = fnmatch.filter({k for d in current_level for k in d}, subpattern)
            if not matched_names:
                # nothing found
                return []
            matching.append(matched_names)
            current_level = [d[n] for d in current_level for n in _viewkeys(d) & set(matched_names)]

    return [os.sep.join(p) for p in product(*matching)
            if _in_trie(path_trie, p)]

This mouthful can quickly find matches using globs anywhere along the path:

>>> paths = ['/foo/bar/baz', '/spam/eggs/baz', '/foo/bar/bar']
>>> find_matching_paths(paths, '/foo/bar/*')
['/foo/bar/baz', '/foo/bar/bar']
>>> find_matching_paths(paths, '/*/bar/b*')
['/foo/bar/baz', '/foo/bar/bar']
>>> find_matching_paths(paths, '/*/[be]*/b*')
['/foo/bar/baz', '/foo/bar/bar', '/spam/eggs/baz']
查看更多
仙女界的扛把子
6楼-- · 2020-05-29 13:36

Good artists copy; great artists steal.

I stole ;)

fnmatch.translate translates globs ? and * to regex . and .* respectively. I tweaked it not to.

import re

def glob2re(pat):
    """Translate a shell PATTERN to a regular expression.

    There is no way to quote meta-characters.
    """

    i, n = 0, len(pat)
    res = ''
    while i < n:
        c = pat[i]
        i = i+1
        if c == '*':
            #res = res + '.*'
            res = res + '[^/]*'
        elif c == '?':
            #res = res + '.'
            res = res + '[^/]'
        elif c == '[':
            j = i
            if j < n and pat[j] == '!':
                j = j+1
            if j < n and pat[j] == ']':
                j = j+1
            while j < n and pat[j] != ']':
                j = j+1
            if j >= n:
                res = res + '\\['
            else:
                stuff = pat[i:j].replace('\\','\\\\')
                i = j+1
                if stuff[0] == '!':
                    stuff = '^' + stuff[1:]
                elif stuff[0] == '^':
                    stuff = '\\' + stuff
                res = '%s[%s]' % (res, stuff)
        else:
            res = res + re.escape(c)
    return res + '\Z(?ms)'

This one à la fnmatch.filter, both re.match and re.search work.

def glob_filter(names,pat):
    return (name for name in names if re.match(glob2re(pat),name))

Glob patterns and strings found on this page pass test.

pat_dict = {
            'a/b/*/f.txt': ['a/b/c/f.txt', 'a/b/q/f.txt', 'a/b/c/d/f.txt','a/b/c/d/e/f.txt'],
            '/foo/bar/*': ['/foo/bar/baz', '/spam/eggs/baz', '/foo/bar/bar'],
            '/*/bar/b*': ['/foo/bar/baz', '/foo/bar/bar'],
            '/*/[be]*/b*': ['/foo/bar/baz', '/foo/bar/bar'],
            '/foo*/bar': ['/foolicious/spamfantastic/bar', '/foolicious/bar']

        }
for pat in pat_dict:
    print('pattern :\t{}\nstrings :\t{}'.format(pat,pat_dict[pat]))
    print('matched :\t{}\n'.format(list(glob_filter(pat_dict[pat],pat))))
查看更多
做个烂人
7楼-- · 2020-05-29 13:38

never mind, I found it. I want the fnmatch module.

查看更多
登录 后发表回答