Как обойти ошибку Python os.path.commonprefix?

Моя проблема - найти префикс общего пути для заданного набора файлов.

Буквально я ожидал, что "os.path.commonprefix" сделает именно это. К сожалению, тот факт, что commonprefix находится в path, скорее вводит в заблуждение, так как он фактически будет искать строковые префиксы.

Вопрос для меня в том, как это можно реально решить для путей? Эта проблема была кратко упомянута в этом (довольно высоко оцененном) ответе, но только как побочная заметка и предлагаемое решение (добавление слэшей к вводу commonprefix). Imho имеет проблемы, поскольку он не будет работать, например, для:

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

Чтобы другие не попадали в одну и ту же ловушку, было бы целесообразно обсудить эту проблему в отдельном вопросе: есть ли простое/переносное решение для этой проблемы, которое не полагается на неприятные проверки файловой системы (т.е. получить доступ к результату commonprefix и проверить, является ли он каталогом, а если не возвращает os.path.dirname результата)?

Ответ 1

Некоторое время назад я столкнулся с этим, где os.path.commonprefix - это префикс строки, а не префикс пути, как и следовало ожидать. Поэтому я написал следующее:

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)

это можно сделать более переносимым, заменив '/' на os.path.sep.

Ответ 2

Похоже, что эта проблема была исправлена ​​в последних версиях Python. Новое в версии 3.5 - это функция os.path.commonpath(), которая возвращает общий путь вместо общего префикса строки.

Ответ 3

Предполагая, что вам нужен общий путь к каталогу, один из способов:

  • В качестве входных данных используйте только пути к каталогам. Если ваше входное значение является именем файла, вызовите os.path.dirname(filename), чтобы получить его путь к каталогу.
  • "Нормализовать" все пути, чтобы они относились к одной и той же вещи и не включали двойные разделители. Самый простой способ сделать это - вызвать os.path.abspath( ), чтобы получить путь относительно корня. (Вы также можете использовать os.path.realpath( ) для удаления символических ссылок.)
  • Добавьте окончательный разделитель (найденный с возможностью os.path.sep или os.sep) до конца всех нормализованных путей каталога.
  • Вызвать os.path.dirname( ) по результату os.path.commonprefix( ).

В коде (без удаления символических ссылок):

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

Ответ 4

Надежный подход состоит в том, чтобы разбить путь на отдельные компоненты, а затем найти самый длинный общий префикс списков компонентов.

Вот реализация, которая является кросс-платформенной и может быть легко обобщена на более чем два пути:

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'

Ответ 5

Я сделал небольшой пакет python commonpath, чтобы найти общие пути из списка. Поставляется с несколькими хорошими вариантами.

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