¿Cómo evitar la falacia de os.path.commonprefix de Python?

Mi problema es encontrar el prefijo de ruta común de un conjunto dado de archivos.

Literalmente esperaba que “os.path.commonprefix” hiciera exactamente eso. Desafortunadamente, el hecho de que el commonprefix esté ubicado en la path es bastante engañoso, ya que en realidad buscará los prefijos de cadena.

La pregunta para mí es, ¿cómo se puede resolver esto realmente por caminos? El problema se mencionó brevemente en esta respuesta (de calificación bastante alta) pero solo como una nota al margen y la solución propuesta (agregando barras a la entrada de prefijo común) Imho tiene problemas, ya que fallará por ejemplo para:

 os.path.commonprefix(['/usr/var1/log/', '/usr/var2/log/']) # returns /usr/var but it should be /usr 

Para evitar que otros caigan en la misma trampa, podría valer la pena discutir este problema en otra pregunta: ¿Existe una solución simple / portátil para este problema que no se base en verificaciones desagradables del sistema de archivos (es decir, acceda al resultado)? de commonprefix y compruebe si es un directorio y si no devuelve un os.path.dirname del resultado)?

Hace un tiempo me encontré con esto donde os.path.commonprefix es un prefijo de cadena y no un prefijo de ruta como se esperaría. Así que escribí lo siguiente:

 def commonprefix(l): # this unlike the os.path.commonprefix version # always returns path prefixes as it compares # path component wise cp = [] ls = [p.split('/') for p in l] ml = min( len(p) for p in ls ) for i in range(ml): s = set( p[i] for p in ls ) if len(s) != 1: break cp.append(s.pop()) return '/'.join(cp) 

podría hacerse más portátil reemplazando '/' con os.path.sep .

Parece que este problema se ha corregido en las versiones recientes de Python. Nueva en la versión 3.5 es la función os.path.commonpath() , que devuelve la ruta común en lugar del prefijo de cadena común.

Suponiendo que desea la ruta de directorio común, una forma es:

  1. Utilice solo las rutas de directorio como entrada. Si su valor de entrada es un nombre de archivo, llame a os.path.dirname(filename) para obtener su ruta de directorio.
  2. “Normalice” todos los caminos para que sean relativos a la misma cosa y no incluyan separadores dobles. La forma más sencilla de hacerlo es llamando a os.path.abspath( ) para obtener la ruta relativa a la raíz. (Es posible que también desee utilizar os.path.realpath( ) para eliminar enlaces simbólicos).
  3. Agregue un separador final (que se encuentra de forma portátil con os.path.sep o os.sep ) al final de todas las rutas de directorio normalizadas.
  4. Llame a os.path.dirname( ) en el resultado de os.path.commonprefix( ) .

En código (sin eliminar enlaces simbólicos):

 def common_path(directories): norm_paths = [os.path.abspath(p) + os.path.sep for p in directories] return os.path.dirname(os.path.commonprefix(norm_paths)) def common_path_of_filenames(filenames): return common_path([os.path.dirname(f) for f in filenames]) 

Un enfoque sólido es dividir la ruta en componentes individuales y luego encontrar el prefijo común más largo de las listas de componentes.

Aquí hay una implementación que es multiplataforma y se puede generalizar fácilmente a más de dos caminos:

 import os.path import itertools def components(path): ''' Returns the individual components of the given file path string (for the local operating system). The returned components, when joined with os.path.join(), point to the same location as the original path. ''' components = [] # The loop guarantees that the returned components can be # os.path.joined with the path separator and point to the same # location: while True: (new_path, tail) = os.path.split(path) # Works on any platform components.append(tail) if new_path == path: # Root (including drive, on Windows) reached break path = new_path components.append(new_path) components.reverse() # First component first return components def longest_prefix(iter0, iter1): ''' Returns the longest common prefix of the given two iterables. ''' longest_prefix = [] for (elmt0, elmt1) in itertools.izip(iter0, iter1): if elmt0 != elmt1: break longest_prefix.append(elmt0) return longest_prefix def common_prefix_path(path0, path1): return os.path.join(*longest_prefix(components(path0), components(path1))) # For Unix: assert common_prefix_path('/', '/usr') == '/' assert common_prefix_path('/usr/var1/log/', '/usr/var2/log/') == '/usr' assert common_prefix_path('/usr/var/log1/', '/usr/var/log2/') == '/usr/var' assert common_prefix_path('/usr/var/log', '/usr/var/log2') == '/usr/var' assert common_prefix_path('/usr/var/log', '/usr/var/log') == '/usr/var/log' # Only for Windows: # assert common_prefix_path(r'C:\Programs\Me', r'C:\Programs') == r'C:\Programs' 

Hice una pequeña commonpath común de commonpath para encontrar rutas comunes de una lista. Viene con algunas opciones agradables.

https://github.com/faph/Common-Path