Python glob pero contra una lista de cadenas en lugar del sistema de archivos

12 minutos de lectura

Quiero ser capaz de hacer coincidir un patrón en glob formato a una lista de cadenas, en lugar de archivos reales en el sistema de archivos. ¿Hay alguna manera de hacer esto, o convertir un glob patrón fácilmente a una expresión regular?

  • No sé si estoy haciendo algo mal, pero creo que el autor quería una solución para hacer coincidir cualquier cadena, no solo nombres de archivo, y las soluciones aquí no pueden extraer ni siquiera una cadena simple como max_volume de [Parsed_volumedetect_0 @ 0x7fbf12004080] max_volume: -9.3 dB. estoy tratando de extraer {max,mean}_volume desde la salida ffmpeg

    – bóveda

    27 de diciembre de 2022 a las 15:34


Avatar de usuario de Martijn Pieters
Martijn Pieters

El glob módulo utiliza el fnmatch módulo para elementos de ruta individuales.

Eso significa que la ruta se divide en el nombre del directorio y el nombre del archivo, y si el nombre del directorio contiene metacaracteres (contiene alguno de los caracteres [, * o ?) entonces estos se expanden recursivamente.

Si tiene una lista de cadenas que son nombres de archivo simples, simplemente use el fnmatch.filter() función es suficiente:

import fnmatch

matching = fnmatch.filter(filenames, pattern)

pero si contienen rutas completas, debe trabajar más, ya que la expresión regular generada no tiene en cuenta los segmentos de la ruta (los comodines no excluyen los separadores ni se ajustan para la coincidencia de rutas multiplataforma).

Puedes construir un sencillo probar de los caminos, luego haga coincidir su patrón con eso:

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)]

Este bocado puede encontrar coincidencias rápidamente usando globos en cualquier lugar a lo largo del camino:

>>> 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']

Avatar de usuario de Veedrac
Veedrac

En Python 3.4+ solo puedes usar PurePath.match.

pathlib.PurePath(path_string).match(pattern)

En Python 3.3 o anterior (incluido 2.x), obtenga pathlib de PyPI.

Tenga en cuenta que para obtener resultados independientes de la plataforma (que dependerán de por qué está ejecutando esto) le gustaría indicar explícitamente PurePosixPath o PureWindowsPath.

  • Un beneficio de este enfoque es que no requiere que especifique la sintaxis global (**/*) si no es necesario. Por ejemplo, si solo está tratando de encontrar una ruta basada en un nombre de archivo.

    – Esteban

    12 abr 2019 a las 17:24

  • Esto solo funciona en una sola cadena. Si bien es útil, no responde completamente la pregunta de OP de “glob formato a un lista de cuerdas”.

    – Numes Sanguis

    1 de noviembre de 2019 a las 2:55

  • Encontré una manera de extender esta respuesta con comprensión de lista, vea mi respuesta.

    – Numes Sanguis

    1 de noviembre de 2019 a las 4:15

  • @Esteban Para algunos casos de uso, también es una debilidad. Si está buscando explícitamente a.py con *.py y luego devuelve resultados recursivos. Por ejemplo, esto devuelve verdadero pathlib.PurePath("a/b/c/abc.py").match("*.py") mientras que creo que solo debería volver verdadero para **/*.py. Pero la solución de Mathew a continuación resuelve eso.

    – Jeppe

    2 oct 2022 a las 8:40

  • ¿Por qué no coincide lo siguiente?pathlib.PurePath("virt/kvm/devices/vm.rst").match("virt/*") # False con fnmatch funciona… fnmatch.filter(["virt/kvm/devices/vm.rst"], "virt/*") # ['virt/kvm/devices/vm.rst']

    – Schirrmacher

    4 de mayo a las 9:09


Avatar de usuario de Nizam Mohamed
Nizam Mohamed

Los buenos artistas copian; grandes artistas robar.

Robé 😉

fnmatch.translate traduce globos ? y * a la expresión regular . y .* respectivamente. Lo modifiqué para que no lo hiciera.

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)'

Este a la fnmatch.filterambos re.match y re.search trabajar.

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

Los patrones glob y las cadenas que se encuentran en esta página pasan la prueba.

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))))

  • ¡Gran primicia! Sí, traducir el patrón a uno que ignore los separadores de ruta es una gran idea. Tenga en cuenta que no maneja os.sep o os.altsep sin embargo, pero debería ser bastante fácil ajustarse a eso.

    – Martijn Pieters

    24/04/2015 a las 15:30

  • Por lo general, solo canonicalizo las rutas para usar barras diagonales primero antes de cualquier procesamiento.

    – Jason S.

    24 de abril de 2015 a las 18:26

  • Esta solución permitirá incorrectamente [^abc] para que coincida con un separador de directorio como /. Vea mi solución para ver un ejemplo que soluciona esto y también permite ** comodines.

    – Mathew mechas

    27 de mayo de 2022 a las 4:03


Avatar de usuario de Anshul Goyal
anshul goyal

Mientras fnmatch.fnmatch se puede usar directamente para verificar si un patrón coincide con un nombre de archivo o no, también puede usar el fnmatch.translate método para generar la expresión regular a partir de lo dado fnmatch patrón:

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

Desde el documentacion:

fnmatch.translate(pattern)

Devuelve el patrón de estilo shell convertido en una expresión regular.

Avatar de usuario de Mathew Wicks
Mechas Mathew

Mi solución es similar a la de Nizam pero con algunos cambios:

  1. Apoyo para ** comodines
  2. Previene patrones como [^abc] de emparejar /
  3. Actualizado para usar fnmatch.translate() de pitón 3.8.13 como base

ADVERTENCIA:

Hay algunas ligeras diferencias a glob.glob() que sufre esta solución (junto con la mayoría de las otras soluciones), no dude en sugerir cambios en los comentarios si sabe cómo solucionarlos:

  1. * y ? no debe coincidir con los nombres de archivo que comienzan con .
  2. ** también debe coincidir con 0 carpetas cuando se usa como /**/

Código:

import re

def glob_to_re(pat: str) -> str:
    """Translate a shell PATTERN to a regular expression.

    Derived from `fnmatch.translate()` of Python version 3.8.13
    SOURCE: https://github.com/python/cpython/blob/v3.8.13/Lib/fnmatch.py#L74-L128
    """

    i, n = 0, len(pat)
    res=""
    while i < n:
        c = pat[i]
        i = i+1
        if c == '*':
            # -------- CHANGE START --------
            # prevent '*' matching directory boundaries, but allow '**' to match them
            j = i
            if j < n and pat[j] == '*':
                res = res + '.*'
                i = j+1
            else:
                res = res + '[^/]*'
            # -------- CHANGE END ----------
        elif c == '?':
            # -------- CHANGE START --------
            # prevent '?' matching directory boundaries
            res = res + '[^/]'
            # -------- CHANGE END ----------
        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]
                if '--' not in stuff:
                    stuff = stuff.replace('\\', r'\\')
                else:
                    chunks = []
                    k = i+2 if pat[i] == '!' else i+1
                    while True:
                        k = pat.find('-', k, j)
                        if k < 0:
                            break
                        chunks.append(pat[i:k])
                        i = k+1
                        k = k+3
                    chunks.append(pat[i:j])
                    # Escape backslashes and hyphens for set difference (--).
                    # Hyphens that create ranges shouldn't be escaped.
                    stuff="-".join(s.replace('\\', r'\\').replace('-', r'\-')
                                     for s in chunks)
                # Escape set operations (&&, ~~ and ||).
                stuff = re.sub(r'([&~|])', r'\\\1', stuff)
                i = j+1
                if stuff[0] == '!':
                    # -------- CHANGE START --------
                    # ensure sequence negations don't match directory boundaries
                    stuff="^/" + stuff[1:]
                    # -------- CHANGE END ----------
                elif stuff[0] in ('^', '['):
                    stuff="\\" + stuff
                res="%s[%s]" % (res, stuff)
        else:
            res = res + re.escape(c)
    return r'(?s:%s)\Z' % res

Casos de prueba:

Aquí hay algunos casos de prueba que comparan el integrado fnmatch.translate() a lo anterior glob_to_re().

import fnmatch

test_cases = [
    # path, pattern, old_should_match, new_should_match
    ("/path/to/foo", "*", True, False),
    ("/path/to/foo", "**", True, True),
    ("/path/to/foo", "/path/*", True, False),
    ("/path/to/foo", "/path/**", True, True),
    ("/path/to/foo", "/path/to/*", True, True),
    ("/path/to", "/path?to", True, False),
    ("/path/to", "/path[!abc]to", True, False),
]

for path, pattern, old_should_match, new_should_match in test_cases:

    old_re = re.compile(fnmatch.translate(pattern))
    old_match = bool(old_re.match(path))
    if old_match is not old_should_match:
        raise AssertionError(
            f"regex from `fnmatch.translate()` should match path "
            f"'{path}' when given pattern: {pattern}"
        )

    new_re = re.compile(glob_to_re(pattern))
    new_match = bool(new_re.match(path))
    if new_match is not new_should_match:
        raise AssertionError(
            f"regex from `glob_to_re()` should match path "
            f"'{path}' when given pattern: {pattern}"
        )

Ejemplo:

Aquí hay un ejemplo que usa glob_to_re() con una lista de cadenas.

glob_pattern = "/path/to/*.txt"
glob_re = re.compile(glob_to_re(glob_pattern))

input_paths = [
    "/path/to/file_1.txt",
    "/path/to/file_2.txt",
    "/path/to/folder/file_3.txt",
    "/path/to/folder/file_4.txt",
]

filtered_paths = [path for path in input_paths if glob_re.match(path)]
# filtered_paths = ["/path/to/file_1.txt", "/path/to/file_2.txt"]

  • ¡Buen trabajo! Supongo que esta es la advertencia de la que estás hablando, que este patrón: "/a/b/c/**/test.py" no coincide con esta entrada: "/a/b/c/test.py". Todavía estoy probando, pero cambiando res = res + '.*' a res = res + '.*/?' y la línea de abajo para i = j + 2 parece funcionar, no estoy seguro de cuán robusto es, pero funciona para mis propósitos. 🙂

    – Jeppe

    2 oct 2022 a las 12:28

Avatar de usuario de Jason S.
jason s

no importa lo encontré. Quiero el partido fn módulo.

  • ¡Buen trabajo! Supongo que esta es la advertencia de la que estás hablando, que este patrón: "/a/b/c/**/test.py" no coincide con esta entrada: "/a/b/c/test.py". Todavía estoy probando, pero cambiando res = res + '.*' a res = res + '.*/?' y la línea de abajo para i = j + 2 parece funcionar, no estoy seguro de cuán robusto es, pero funciona para mis propósitos. 🙂

    – Jeppe

    2 oct 2022 a las 12:28

Avatar de usuario de NumesSanguis
NumesSanguis

Una extensión de @Veedrac PurePath.match respuesta que se puede aplicar a una lista de cadenas:

# Python 3.4+
from pathlib import Path

path_list = ["foo/bar.txt", "spam/bar.txt", "foo/eggs.txt"]
# convert string to pathlib.PosixPath / .WindowsPath, then apply PurePath.match to list
print([p for p in path_list if Path(p).match("ba*")])  # "*ba*" also works
# output: ['foo/bar.txt', 'spam/bar.txt']

print([p for p in path_list if Path(p).match("*o/ba*")])
# output: ['foo/bar.txt']

Es preferible utilizar pathlib.Path() encima pathlib.PurePath()porque entonces no tiene que preocuparse por el sistema de archivos subyacente.

¿Ha sido útil esta solución?