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'