Python: получить относительный путь от сравнения двух абсолютных путей

Скажем, у меня есть два абсолютных пути. Мне нужно проверить, является ли место, относящееся к одному из путей, потомком другого. Если это правда, мне нужно выяснить относительный путь потомка от предка. Какой хороший способ реализовать это в Python? Любая библиотека, с которой я могу воспользоваться?

Ответ 1

os.path.commonprefix() и os.path.relpath() являются вашими друзьями:

>>> print os.path.commonprefix(['/usr/var/log', '/usr/var/security'])
'/usr/var'
>>> print os.path.commonprefix(['/tmp', '/usr/var'])  # No common prefix: the root is the common prefix
'/'

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

paths = […, …, …]
common_prefix = os.path.commonprefix(list_of_paths)
if common_prefix in paths:
    …

Затем вы можете найти относительные пути:

relative_paths = [os.path.relpath(path, common_prefix) for path in paths]

Вы можете обрабатывать более двух путей с помощью этого метода и проверять, все ли все пути ниже одного из них.

PS: в зависимости от того, как выглядят ваши пути, вы можете сначала выполнить некоторую нормализацию (это полезно в ситуациях, когда никто не знает, всегда ли они заканчиваются на "/" или нет, или если некоторые из путей являются относительными). Релевантные функции включают os.path.abspath() и os.path.normpath().

PPS: как отметил Питер Бриггс в комментариях, простой способ, описанный выше, может выйти из строя:

>>> os.path.commonprefix(['/usr/var', '/usr/var2/log'])
'/usr/var'

хотя /usr/var не является общим префиксом путей. Принуждение всех путей к завершению с '/' перед вызовом commonprefix() решает эту (конкретную) проблему.

PPPS: как упоминалось bluenote10, добавление косой черты не решает общей проблемы. Вот его следующий вопрос: Как обойти ошибочность os.path.commonprefix Python?

PPPPS: начиная с Python 3.4, pathlib, модуль, который обеспечивает более эффективную манипуляцию Окружающая среда. Я предполагаю, что общий префикс набора путей можно получить, получив все префиксы каждого пути (с PurePath.parents()), взяв пересечение всех этих родительских множеств и выбор самого длинного общего префикса.

PPPPPS: Python 3.5 представил правильное решение этого вопроса: os.path.commonpath(), который возвращает допустимый путь.

Ответ 2

os.path.relpath:

Возвращает относительный путь к пути к пути из текущего каталога или из необязательной начальной точки.

>>> from os.path import relpath
>>> relpath('/usr/var/log/', '/usr/var')
'log'
>>> relpath('/usr/var/log/', '/usr/var/sad/')
'../log'

Итак, если относительный путь начинается с '..' - это означает, что второй путь не является потомком первого пути.

В Python3 вы можете использовать PurePath.relative_to:

Python 3.5.1 (default, Jan 22 2016, 08:54:32)
>>> from pathlib import Path

>>> Path('/usr/var/log').relative_to('/usr/var/log/')
PosixPath('.')

>>> Path('/usr/var/log').relative_to('/usr/var/')
PosixPath('log')

>>> Path('/usr/var/log').relative_to('/etc/')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/pathlib.py", line 851, in relative_to
    .format(str(self), str(formatted)))
ValueError: '/usr/var/log' does not start with '/etc'

Ответ 3

Другой вариант:

>>> print os.path.relpath('/usr/var/log/', '/usr/var')
log

Ответ 4

Чистый Python2 без депо:

def relpath(cwd, path):
    """Create a relative path for path from cwd, if possible"""
    if sys.platform == "win32":
        cwd = cwd.lower()
        path = path.lower()
    _cwd = os.path.abspath(cwd).split(os.path.sep)
    _path = os.path.abspath(path).split(os.path.sep)
    eq_until_pos = None
    for i in xrange(min(len(_cwd), len(_path))):
        if _cwd[i] == _path[i]:
            eq_until_pos = i
        else:
            break
    if eq_until_pos is None:
        return path
    newpath = [".." for i in xrange(len(_cwd[eq_until_pos+1:]))]
    newpath.extend(_path[eq_until_pos+1:])
    return os.path.join(*newpath) if newpath else "."

Ответ 5

Изменить: См. ответ jme для наилучшего способа с Python3.

Используя pathlib, у вас есть следующее решение:

Скажем, мы хотим проверить, является ли son потомком parent, и оба являются объектами Path. Мы можем получить список частей в пути с помощью list(parent.parts). Затем мы просто проверяем, что начало сына равно списку сегментов родителя.

>>> lparent = list(parent.parts)
>>> lson = list(son.parts)
>>> if lson[:len(lparent)] == lparent:
>>> ... #parent is a parent of son :)

Если вы хотите получить оставшуюся часть, вы можете просто сделать

>>> ''.join(lson[len(lparent):])

Это строка, но вы можете, конечно, использовать ее как конструктор другого объекта Path.

Ответ 6

Описание предложения jme с использованием pathlib в Python 3.

from pathlib import Path
parent = Path(r'/a/b')
son = Path(r'/a/b/c/d')            
​
if parent in son.parents:
    print(son.relative_to(parent)) # returns Path object equivalent to 'c/d'