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

143

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

tamakisquare
источник

Ответы:

168

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()возвращает правильный путь.

Эрик О Лебиго
источник
Именно то, что мне нужно. Спасибо за ваш быстрый ответ. Приму ваш ответ, как только будет снято ограничение по времени.
tamakisquare
10
Будьте внимательны commonprefix, например, с общим префиксом for /usr/var/logи /usr/var2/logвозвращаемым как /usr/var- что, вероятно, не то, что вы ожидаете. (Также возможно, чтобы он возвращал пути, которые не являются действительными каталогами.)
Питер Бриггс
@PeterBriggs: Спасибо, это предупреждение важно. Я добавил PPS.
Эрик О Лебиго
1
@EOL: Я действительно не вижу, как решить проблему, добавив косую черту :(. Что если у нас есть ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10
1
@EOL: Так как мне не удалось найти привлекательное решение для этой проблемы, я подумал, что можно обсудить этот вопрос в отдельном вопросе .
bluenote10
86

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'
warvariuc
источник
2
Проверка на наличие os.pardirболее надежна, чем проверка на наличие ..(хотя и не так много других соглашений).
Эрик О Лебиго
8
Я ошибаюсь или является os.relpathболее мощным, так как он обрабатывает ..и PurePath.relative_to()не делает? Я что-то упускаю?
Рэй Салем
15

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

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

источник
Это всегда возвращает относительный путь; это напрямую не указывает, находится ли один из путей над другим ( os.pardirхотя можно проверить наличие перед двумя возможными относительными путями).
Эрик О Лебиго
8

Описание предложения 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 or parent==son:
    print(son.relative_to(parent)) # returns Path object equivalent to 'c/d'
Tahlor
источник
Так dir1.relative_to(dir2)что даст PosixPath ('.'), Если они одинаковы. Когда вы используете, if dir2 in dir1.parentsэто исключает личность. Если кто-то сравнивает пути и хочет работать, relative_to()если они совместимы с путями, лучшим решением может быть if dir2 in (dir1 / 'x').parentsили if dir2 in dir1.parents or dir2 == dir1. Тогда все случаи совместимости пути покрыты.
ingyhere
3

Чистый 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 "."
Ян Штюрц
источник
Это выглядит хорошо, но, как я наткнулся, есть проблема, когда cwdи pathто же самое. он должен проверить первый , если те два одинаковы и возврат либо ""или"."
Srđan Popić
1

Изменить: см. Ответ 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.

Джереми Кохой
источник
4
Это даже проще, чем просто: parent in son.parentsи, если это так, получить остаток с помощью son.relative_to(parent).
JM
@jme Ты отвечаешь еще лучше, почему бы тебе не опубликовать это?
Джереми Кочой