Python: Отримайте відносний шлях порівняння двох абсолютних шляхів


143

Скажіть, у мене два абсолютні шляхи. Мені потрібно перевірити, чи місцезнаходження, на яке посилається одна із стежок, є нащадком іншого. Якщо це правда, мені потрібно з’ясувати відносний шлях нащадка від предка. Який хороший спосіб реалізувати це в Python? Будь-яка бібліотека, якою я можу скористатися?

Відповіді:


167

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, додавання косої риски не вирішує загальної проблеми. Ось його наступне питання: Як обійти помилковість Python's os.path.commonprefix?

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

PPPPPS : Python 3.5 представив правильне рішення цього питання:, os.path.commonpath()який повертає дійсний шлях.


Саме те, що мені потрібно. Дякуємо за вашу швидку відповідь. Після прийому обмеження часу ваша відповідь прийме.
tamakisquare

10
Будьте уважні commonprefix, як, наприклад, загальний префікс /usr/var/logі /usr/var2/logповертається як /usr/var- що, мабуть, не те, що ви очікували. (Також можливе повернення шляхів, які не є дійсними каталогами.)
Пітер Бріггс,

@PeterBriggs: Дякую, цей застереження є важливим. Я додав PPS.
Ерік О Лебігот

1
@EOL: Я не знаю, як виправити проблему, додавши косу ['/usr/var1/log/', '/usr/var2/log/']
рису

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'

2
Перевірка на наявність os.pardirє більш надійною, ніж перевірка на наявність ..(погоджено, хоча інших конвенцій не існує).
Eric O Lebigot

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'

Так dir1.relative_to(dir2)дасть PosixPath ('.'), Якщо вони однакові. Якщо ви користуєтесь, if dir2 in dir1.parentsто це виключає випадок посвідчення особи. Якщо хтось порівнює Paths і хоче запустити, 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вони однакові. спершу слід перевірити, чи ці двоє однакові, і повернутись ""або"."
Серджан Попич,

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).
jme

@jme Ви відповідаєте ще краще, чому б не опублікувати його?
Джеремі Кохой
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.