Python: ottieni il percorso relativo confrontando due percorsi assoluti


143

Dì, ho due percorsi assoluti. Devo verificare se la posizione a cui fa riferimento uno dei percorsi è discendente dell'altro. Se vero, ho bisogno di scoprire il percorso relativo del discendente dall'antenato. Qual è un buon modo per implementarlo in Python? Qualche libreria di cui posso beneficiare?

Risposte:


167

os.path.commonprefix () e os.path.relpath () sono i tuoi amici:

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

È quindi possibile verificare se il prefisso comune è uno dei percorsi, ovvero se uno dei percorsi è un antenato comune:

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

È quindi possibile trovare i percorsi relativi:

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

È anche possibile gestire più di due percorsi, con questo metodo, e verificare se tutti i percorsi sono tutti al di sotto di uno di essi.

PS : a seconda dell'aspetto dei tuoi percorsi, potresti voler prima eseguire un po 'di normalizzazione (questo è utile in situazioni in cui non si sa se finiscono sempre con' / 'o no, o se alcuni dei percorsi sono relativi). Le funzioni rilevanti includono os.path.abspath () e os.path.normpath () .

PPS : come menzionato da Peter Briggs nei commenti, il semplice approccio sopra descritto può fallire:

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

anche se non/usr/var è un prefisso comune dei percorsi. Forzare la fine di tutti i percorsi con '/' prima di chiamare risolve questo problema (specifico).commonprefix()

PPPS : come menzionato bluenote10, l'aggiunta di una barra non risolve il problema generale. Ecco la sua domanda di follow-up: Come eludere l'errore di os.path.commonprefix di Python?

PPPPS : a partire da Python 3.4, abbiamo pathlib , un modulo che fornisce un ambiente di manipolazione del percorso più igienico . Immagino che il prefisso comune di un insieme di percorsi possa essere ottenuto prendendo tutti i prefissi di ciascun percorso (con PurePath.parents()), prendendo l'intersezione di tutti questi insiemi principali e selezionando il prefisso comune più lungo.

PPPPPS : Python 3.5 ha introdotto una soluzione adeguata a questa domanda :,os.path.commonpath() che restituisce un percorso valido.


Esattamente quello di cui ho bisogno. Grazie per la tua pronta risposta. Accetterà la tua risposta una volta che il limite di tempo è stato revocato.
Tamakisquare,

10
Fai attenzione commonprefix, ad esempio il prefisso comune per /usr/var/loge /usr/var2/logviene restituito come /usr/var- che probabilmente non è quello che ti aspetteresti. (È anche possibile che restituisca percorsi che non sono directory valide.)
Peter Briggs

@PeterBriggs: grazie, questo avvertimento è importante. Ho aggiunto un PPS.
Eric O Lebigot,

1
@EOL: Non vedo davvero come risolvere il problema aggiungendo una barra :(. E se avessimo ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10

1
@EOL: Dal momento che non sono riuscito a trovare una soluzione accattivante per questo problema, ho pensato che potesse essere giusto discutere questo sotto-problema in una domanda separata .
bluenote10,

86

os.path.relpath:

Restituisce un percorso file relativo al percorso dalla directory corrente o da un punto iniziale facoltativo.

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

Quindi, se il percorso relativo inizia con '..'- significa che il secondo percorso non è discendente del primo percorso.

In Python3 puoi usare 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
Il controllo della presenza di os.pardirè più solido del controllo ..(concordato, tuttavia non ci sono molte altre convenzioni).
Eric O Lebigot,

8
Sbaglio o è os.relpathpiù potente poiché gestisce ..e PurePath.relative_to()non lo fa? Mi sto perdendo qualcosa?
Ray Salemi,

15

Un'altra opzione è

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

Questo restituisce sempre un percorso relativo; ciò non indica direttamente se uno dei percorsi è sopra l'altro (si può verificare la presenza di os.pardirfronte ai due possibili percorsi relativi risultanti, però).
Eric O Lebigot,

8

Un resoconto del suggerimento di jme, usando 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'

Quindi dir1.relative_to(dir2)darà PosixPath ('.') Se sono uguali. Quando lo usi if dir2 in dir1.parents, esclude il caso di identità. Se qualcuno sta confrontando i percorsi e desidera eseguirli relative_to()se sono compatibili con il percorso, una soluzione migliore potrebbe essere if dir2 in (dir1 / 'x').parentso if dir2 in dir1.parents or dir2 == dir1. Quindi vengono trattati tutti i casi di compatibilità dei percorsi.
ingyhere

3

Python2 puro senza 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 "."

Questo sembra buono, ma, mentre inciampo, c'è un problema quando cwde pathsono gli stessi. dovrebbe prima verificare se quei due sono uguali e restituire uno ""o"."
Srđan Popić,

1

Modifica: vedi la risposta di jme per il modo migliore con Python3.

Usando pathlib, hai la seguente soluzione:

Diciamo che vogliamo verificare se sonè un discendente di parent, ed entrambi sono Pathoggetti. Possiamo ottenere un elenco delle parti nel percorso con list(parent.parts). Quindi, controlliamo solo che l'inizio del figlio sia uguale all'elenco dei segmenti del genitore.

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

Se vuoi ottenere la parte rimanente, puoi semplicemente farlo

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

È una stringa, ma puoi ovviamente usarla come costruttore di un altro oggetto Path.


4
È ancora più facile di così: semplicemente parent in son.parents, e se lo è, ottenere il resto con son.relative_to(parent).
jme,

@jme La tua risposta è ancora migliore, perché non la pubblichi?
Jeremy Cochoy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.