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?
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']
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 verdaderopathlib.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
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.filter
ambos 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
oos.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
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.
Mechas Mathew
Mi solución es similar a la de Nizam pero con algunos cambios:
- Apoyo para
**
comodines - Previene patrones como
[^abc]
de emparejar/
- Actualizado para usar
fnmatch.translate()
de pitón3.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:
*
y?
no debe coincidir con los nombres de archivo que comienzan con.
**
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 cambiandores = res + '.*'
ares = res + '.*/?'
y la línea de abajo parai = 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
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 cambiandores = res + '.*'
ares = res + '.*/?'
y la línea de abajo parai = 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
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.
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