Python: Ermitteln Sie den relativen Pfad, indem Sie zwei absolute Pfade vergleichen

143

Sagen wir, ich habe zwei absolute Wege. Ich muss überprüfen, ob der Ort, auf den sich einer der Pfade bezieht, ein Nachkomme des anderen ist. Wenn dies zutrifft, muss ich den relativen Pfad des Nachkommen vom Vorfahren herausfinden. Was ist ein guter Weg, um dies in Python zu implementieren? Gibt es eine Bibliothek, von der ich profitieren kann?

Tamakisquare
quelle

Antworten:

167

os.path.commonprefix () und os.path.relpath () sind deine Freunde:

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

Sie können also testen, ob das gemeinsame Präfix einer der Pfade ist, dh ob einer der Pfade ein gemeinsamer Vorfahr ist:

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

Sie können dann die relativen Pfade finden:

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

Mit dieser Methode können Sie sogar mehr als zwei Pfade verarbeiten und testen, ob alle Pfade unter einem von ihnen liegen.

PS : Je nachdem, wie Ihre Pfade aussehen, möchten Sie möglicherweise zuerst eine Normalisierung durchführen (dies ist in Situationen nützlich, in denen nicht bekannt ist, ob sie immer mit '/' enden oder nicht oder ob einige der Pfade relativ sind). Relevante Funktionen sind os.path.abspath () und os.path.normpath () .

PPS : Wie Peter Briggs in den Kommentaren erwähnt hat, kann der oben beschriebene einfache Ansatz fehlschlagen:

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

obwohl /usr/varist kein allgemeines Präfix der Pfade. Das Erzwingen, dass alle Pfade vor dem Aufruf mit '/' enden, commonprefix()löst dieses (spezifische) Problem.

PPPS : Wie in bluenote10 erwähnt, löst das Hinzufügen eines Schrägstrichs das allgemeine Problem nicht. Hier ist seine Folgefrage: Wie kann man den Irrtum von Pythons os.path.commonprefix umgehen?

PPPPS : Ab Python 3.4 haben wir pathlib , ein Modul, das eine sanere Pfadmanipulationsumgebung bietet. Ich vermute, dass das gemeinsame Präfix eines Satzes von Pfaden erhalten werden kann, indem alle Präfixe jedes Pfades (mit PurePath.parents()) abgerufen , der Schnittpunkt aller dieser übergeordneten Sätze genommen und das längste gemeinsame Präfix ausgewählt werden.

PPPPPS : Python 3.5 hat eine geeignete Lösung für diese Frage eingeführt : os.path.commonpath(), die einen gültigen Pfad zurückgibt.

Eric O Lebigot
quelle
Genau das, was ich brauche. Danke für deine schnelle Antwort. Akzeptiert Ihre Antwort, sobald die zeitliche Beschränkung aufgehoben ist.
Tamakisquare
10
Seien Sie vorsichtig mit commonprefix, wie z. B. dem allgemeinen Präfix für /usr/var/logund /usr/var2/logwird zurückgegeben als /usr/var- was wahrscheinlich nicht das ist, was Sie erwarten würden. (Es ist auch möglich, Pfade zurückzugeben, die keine gültigen Verzeichnisse sind.)
Peter Briggs
@ PeterBriggs: Danke, diese Einschränkung ist wichtig. Ich habe ein PPS hinzugefügt.
Eric O Lebigot
1
@EOL: Ich sehe nicht wirklich, wie man das Problem durch Anhängen eines Schrägstrichs behebt :(. Was ist, wenn wir haben ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10
1
@EOL: Da ich keine ansprechende Lösung für dieses Problem gefunden habe, ist es möglicherweise in Ordnung, dieses Unterproblem in einer separaten Frage zu diskutieren .
bluenote10
86

os.path.relpath::

Geben Sie einen relativen Dateipfad entweder aus dem aktuellen Verzeichnis oder von einem optionalen Startpunkt zum Pfad zurück.

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

Wenn also der relative Pfad mit beginnt '..', bedeutet dies, dass der zweite Pfad nicht vom ersten Pfad abstammt.

In Python3 können Sie Folgendes verwenden 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
quelle
2
Das Überprüfen auf das Vorhandensein von os.pardirist robuster als das Überprüfen auf ..(vereinbart, es gibt jedoch nicht viele andere Konventionen).
Eric O Lebigot
8
Liege ich falsch oder ist os.relpathes mächtiger, da es funktioniert ..und PurePath.relative_to()nicht? Vermisse ich etwas
Ray Salemi
15

Eine andere Option ist

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

quelle
Dies gibt immer einen relativen Pfad zurück; Dies zeigt nicht direkt an, ob einer der Pfade über dem anderen liegt (man kann jedoch prüfen, ob os.pardirvor den beiden möglichen resultierenden relativen Pfaden vorhanden ist).
Eric O Lebigot
8

Eine Zusammenfassung von Jmes Vorschlag mit pathlib in 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
quelle
So dir1.relative_to(dir2)gibt PosixPath ( ‚‘) , wenn sie gleich sind. Wenn Sie if dir2 in dir1.parentsdann verwenden, wird der Identitätsfall ausgeschlossen. Wenn jemand Pfade vergleicht und ausführen möchte, relative_to()wenn er pfadkompatibel ist, ist if dir2 in (dir1 / 'x').parentsoder möglicherweise eine bessere Lösung if dir2 in dir1.parents or dir2 == dir1. Dann werden alle Fälle von Pfadkompatibilität abgedeckt.
Ingyhere
3

Pure Python2 ohne 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
quelle
Dieser sieht gut aus, aber wie ich stolpere, gibt es ein Problem, wenn cwdund pathsind das gleiche. es sollte zuerst prüfen, ob diese beiden gleich sind und entweder ""oder"."
Srđan Popić
1

Bearbeiten: Siehe jmes Antwort für den besten Weg mit Python3.

Mit pathlib haben Sie folgende Lösung:

Angenommen, wir möchten überprüfen, ob sones sich um einen Nachkommen von handelt parent, und beide sind PathObjekte. Wir können eine Liste der Teile im Pfad mit erhalten list(parent.parts). Dann überprüfen wir einfach, ob der Anfang des Sohnes der Liste der Segmente des Elternteils entspricht.

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

Wenn Sie den verbleibenden Teil erhalten möchten, können Sie dies einfach tun

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

Es ist eine Zeichenfolge, aber Sie können sie natürlich als Konstruktor eines anderen Path-Objekts verwenden.

Jeremy Cochoy
quelle
4
Es ist noch einfacher als das: einfach parent in son.parentsund wenn ja , den Rest mit son.relative_to(parent).
jme
@jme Du antwortest noch besser, warum postest du es nicht?
Jeremy Cochoy