Python: obtenir un chemin relatif en comparant deux chemins absolus

143

Dis, j'ai deux chemins absolus. Je dois vérifier si l'emplacement auquel se réfère l'un des chemins est un descendant de l'autre. Si c'est vrai, je dois trouver le chemin relatif du descendant de l'ancêtre. Quel est le bon moyen d'implémenter cela en Python? Une bibliothèque dont je peux bénéficier?

tamakisquare
la source

Réponses:

168

os.path.commonprefix () et os.path.relpath () sont vos amis:

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

Vous pouvez ainsi tester si le préfixe commun est l'un des chemins, c'est à dire si l'un des chemins est un ancêtre commun:

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

Vous pouvez ensuite trouver les chemins relatifs:

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

Vous pouvez même gérer plus de deux chemins, avec cette méthode, et tester si tous les chemins sont tous en dessous de l'un d'eux.

PS : selon l'apparence de vos chemins, vous voudrez peut-être commencer par effectuer une normalisation (cela est utile dans les situations où l'on ne sait pas s'ils se terminent toujours par '/' ou non, ou si certains des chemins sont relatifs). Les fonctions pertinentes incluent os.path.abspath () et os.path.normpath () .

PPS : comme Peter Briggs l'a mentionné dans les commentaires, l'approche simple décrite ci-dessus peut échouer:

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

même si ce /usr/varn'est pas un préfixe commun des chemins. Forcer tous les chemins à se terminer par «/» avant d'appeler commonprefix()résout ce problème (spécifique).

PPPS : comme bluenote10 l'a mentionné, l'ajout d'une barre oblique ne résout pas le problème général. Voici sa question de suivi: Comment contourner l'erreur du préfixe os.path.commonprefix de Python?

PPPPS : à partir de Python 3.4, nous avons pathlib , un module qui fournit un environnement de manipulation de chemin plus sain . Je suppose que le préfixe commun d'un ensemble de chemins peut être obtenu en obtenant tous les préfixes de chaque chemin (avec PurePath.parents()), en prenant l'intersection de tous ces ensembles parents et en sélectionnant le préfixe commun le plus long.

PPPPPS : Python 3.5 a introduit une solution appropriée à cette question :,os.path.commonpath() qui renvoie un chemin valide.

Eric O Lebigot
la source
Exactement ce dont j'ai besoin. Merci pour votre réponse rapide. Acceptera votre réponse une fois la limite de temps levée.
tamakisquare le
10
Faites attention avec commonprefix, par exemple, le préfixe commun pour /usr/var/loget /usr/var2/logest renvoyé comme /usr/var- ce qui n'est probablement pas ce à quoi vous vous attendez. (Il est également possible qu'il renvoie des chemins qui ne sont pas des répertoires valides.)
Peter Briggs
@PeterBriggs: Merci, cette mise en garde est importante. J'ai ajouté un PPS.
Eric O Lebigot
1
@EOL: Je ne vois pas vraiment comment résoudre le problème en ajoutant une barre oblique :(. Et si nous avons ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10
1
@EOL: Comme je n'ai pas réussi à trouver une solution attrayante à ce problème, j'ai pensé qu'il serait peut-être acceptable de discuter de ce sous-problème dans une question distincte .
bluenote10
86

os.path.relpath:

Renvoie un chemin de fichier relatif au chemin à partir du répertoire actuel ou d'un point de départ facultatif.

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

Donc, si le chemin relatif commence par '..'- cela signifie que le deuxième chemin n'est pas le descendant du premier chemin.

En Python3, vous pouvez utiliser 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
la source
2
Vérifier la présence de os.pardirest plus robuste que vérifier ..(d'accord, il n'y a pas beaucoup d'autres conventions, cependant).
Eric O Lebigot le
8
Est-ce que je me trompe ou est os.relpathplus puissant car il gère ..et PurePath.relative_to()ne fonctionne pas? Est-ce que je manque quelque chose?
Ray Salemi
15

Une autre option est

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

la source
Cela renvoie toujours un chemin relatif; cela n'indique pas directement si l'un des chemins est au-dessus de l'autre (on peut cependant vérifier la présence de os.pardirdevant les deux chemins relatifs résultants possibles).
Eric O Lebigot
8

Une rédaction de la suggestion de jme, en utilisant pathlib, en 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
la source
Ainsi dir1.relative_to(dir2)donnera PosixPath ('.') S'ils sont identiques. Lorsque vous utilisez, if dir2 in dir1.parentsil exclut le cas d'identité. Si quelqu'un compare des chemins et veut s'exécuter relative_to()s'ils sont compatibles avec les chemins, une meilleure solution peut être if dir2 in (dir1 / 'x').parentsou if dir2 in dir1.parents or dir2 == dir1. Ensuite, tous les cas de compatibilité de chemin sont couverts.
ingyhere
3

Pure Python2 sans 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
la source
Celui-ci a l'air bien, mais, comme je tombe sur, il y a un problème quand cwdet pathsont les mêmes. il devrait d'abord vérifier si ces deux sont identiques et renvoyer l'un ""ou l' autre ou"."
Srđan Popić
1

Edit: Voir la réponse de jme pour le meilleur moyen avec Python3.

En utilisant pathlib, vous avez la solution suivante:

Disons que nous voulons vérifier si sonest un descendant de parent, et que les deux sont des Pathobjets. Nous pouvons obtenir une liste des parties du chemin avec list(parent.parts). Ensuite, on vérifie juste que le début du fils est égal à la liste des segments du parent.

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

Si vous voulez obtenir la partie restante, vous pouvez simplement faire

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

C'est une chaîne, mais vous pouvez bien sûr l'utiliser comme constructeur d'un autre objet Path.

Jeremy Cochoy
la source
4
C'est encore plus facile que cela: simplement parent in son.parents, et si c'est le cas, obtenir le reste avec son.relative_to(parent).
jme
@jme Votre réponse est encore meilleure, pourquoi ne la postez-vous pas?
Jeremy Cochoy du