Python: Uzyskaj ścieżkę względną z porównania dwóch ścieżek bezwzględnych

143

Powiedz, mam dwie absolutne ścieżki. Muszę sprawdzić, czy lokalizacja, do której odwołuje się jedna ze ścieżek, jest potomkiem drugiej. Jeśli to prawda, muszę znaleźć względną ścieżkę potomka przodka. Jaki jest dobry sposób na zaimplementowanie tego w Pythonie? Każda biblioteka, z której mogę skorzystać?

tamakisquare
źródło

Odpowiedzi:

167

os.path.commonprefix () i os.path.relpath () to Twoi przyjaciele:

>>> 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
'/'

Możesz więc sprawdzić, czy wspólny prefiks jest jedną ze ścieżek, tj. Czy jedna ze ścieżek jest wspólnym przodkiem:

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

Następnie możesz znaleźć ścieżki względne:

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

Za pomocą tej metody można nawet obsłużyć więcej niż dwie ścieżki i sprawdzić, czy wszystkie ścieżki znajdują się poniżej jednej z nich.

PS : w zależności od tego, jak wyglądają Twoje ścieżki, możesz najpierw zechcieć przeprowadzić pewną normalizację (jest to przydatne w sytuacjach, gdy nie wiadomo, czy zawsze kończą się znakiem „/”, czy też nie, lub jeśli niektóre ścieżki są względne). Odpowiednie funkcje obejmują os.path.abspath () i os.path.normpath () .

PPS : jak wspomniał w komentarzach Peter Briggs, proste podejście opisane powyżej może się nie powieść:

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

mimo że nie/usr/var jest to wspólny prefiks ścieżek. Wymuszenie przed wywołaniem wszystkich ścieżek kończących się znakiem „/” rozwiązuje ten (konkretny) problem.commonprefix()

PPPS : jak wspomniano w bluenote10, dodanie ukośnika nie rozwiązuje ogólnego problemu. Oto jego kolejne pytanie: Jak obejść błąd związany z os.path.commonprefix w języku Python?

PPPPS : począwszy od Pythona 3.4, mamy pathlib , moduł zapewniający bardziej rozsądne środowisko do manipulacji ścieżkami. Myślę, że wspólny prefiks zestawu ścieżek można uzyskać, pobierając wszystkie prefiksy każdej ścieżki (z PurePath.parents()), biorąc przecięcie wszystkich tych zestawów nadrzędnych i wybierając najdłuższy wspólny prefiks.

PPPPPS : Python 3.5 wprowadził poprawne rozwiązanie tego pytania :,os.path.commonpath() które zwraca prawidłową ścieżkę.

Eric O Lebigot
źródło
Dokładnie to, czego potrzebuję. Dziękuję za szybką odpowiedź. Zaakceptuje Twoją odpowiedź po zniesieniu ograniczenia czasowego.
tamakisquare
10
Uważaj commonprefix, ponieważ np. Wspólny prefiks for /usr/var/logi /usr/var2/logjest zwracany jako /usr/var- co prawdopodobnie nie jest tym, czego można się spodziewać. (Możliwe jest również zwrócenie ścieżek, które nie są prawidłowymi katalogami.)
Peter Briggs,
@PeterBriggs: Dzięki, to zastrzeżenie jest ważne. Dodałem PPS.
Eric O Lebigot
1
@EOL: Naprawdę nie wiem, jak rozwiązać problem, dodając ukośnik :(. A co, jeśli mamy ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10
1
@EOL: Ponieważ nie udało mi się znaleźć atrakcyjnego rozwiązania tego problemu, pomyślałem, że może być w porządku omówienie tego pod-zagadnienia w osobnym pytaniu .
bluenote10
86

os.path.relpath:

Zwróć względną ścieżkę do ścieżki z bieżącego katalogu lub z opcjonalnego punktu początkowego.

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

Tak więc, jeśli ścieżka względna zaczyna się od '..'- oznacza to, że druga ścieżka nie jest potomkiem pierwszej ścieżki.

W Pythonie3 możesz użyć 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
źródło
2
Sprawdzanie obecności os.pardirjest bardziej niezawodne niż sprawdzanie ..(uzgodniono, nie ma jednak wielu innych konwencji).
Eric O Lebigot
8
Czy się mylę, czy jest os.relpathsilniejszy, ponieważ obsługuje ..i PurePath.relative_to()nie obsługuje ? Czy coś mi brakuje?
Ray Salemi
15

Inną opcją jest

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

źródło
To zawsze zwraca ścieżkę względną; nie wskazuje to bezpośrednio, czy jedna ze ścieżek znajduje się nad drugą (można jednak sprawdzić obecność os.pardirprzed dwiema możliwymi wynikowymi ścieżkami względnymi).
Eric O Lebigot
8

Opis sugestii jme przy użyciu pathlib w Pythonie 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
źródło
Więc dir1.relative_to(dir2)da PosixPath ('.'), Jeśli są takie same. Kiedy używasz if dir2 in dir1.parents, wyklucza przypadek tożsamości. Jeśli ktoś porównuje ścieżki i chce uruchomić, relative_to()jeśli są one zgodne ze ścieżkami, lepszym rozwiązaniem może być if dir2 in (dir1 / 'x').parentslub if dir2 in dir1.parents or dir2 == dir1. Następnie uwzględniono wszystkie przypadki zgodności ścieżek.
ingyhere
3

Czysty Python2 bez dep:

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 "."
Jan Stürtz
źródło
Ten wygląda dobrze, ale jak się natknąłem, pojawia się problem, kiedy cwdi pathsą takie same. należy najpierw sprawdzić, czy te dwa są takie same i zwrot albo ""albo"."
Srđan Popić
1

Edycja: Zobacz odpowiedź jme na najlepszy sposób z Python3.

Korzystając z pathlib, masz następujące rozwiązanie:

Powiedzmy, że chcemy sprawdzić, czy sonjest potomkiem parenti oba są Pathobiektami. Możemy uzyskać listę części ścieżki za pomocą list(parent.parts). Następnie po prostu sprawdzamy, czy początek syna jest równy liście segmentów rodzica.

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

Jeśli chcesz zdobyć pozostałą część, możesz to zrobić

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

Jest to ciąg znaków, ale można go oczywiście użyć jako konstruktora innego obiektu Path.

Jeremy Cochoy
źródło
4
To jeszcze łatwiejsze: po prostu parent in son.parents, a jeśli tak, to resztę zdobądź son.relative_to(parent).
jme
@jme Twoja odpowiedź jest jeszcze lepsza, dlaczego jej nie opublikujesz?
Jeremy Cochoy