160 votes

Obtenir un chemin relatif en comparant deux chemins absolus

Disons que 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 le cas, je dois trouver le chemin relatif du descendant à partir de l'ancêtre. Quelle est la meilleure façon d'implémenter cela en Python ? Y a-t-il une bibliothèque dont je pourrais bénéficier ?

183voto

EOL Points 24342

os.path.commonprefix() y 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 vérifier 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 alors trouver les chemins relatifs :

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

Cette méthode permet même de gérer plus de deux chemins et de vérifier si tous les chemins se trouvent en dessous de l'un d'entre eux.

PS Selon l'aspect de vos chemins d'accès, il est possible que vous souhaitiez d'abord effectuer une normalisation (utile dans les situations où l'on ne sait pas si les chemins d'accès se terminent toujours par '/' ou non, ou si certains chemins d'accès sont relatifs). Les fonctions pertinentes sont les suivantes os.path.abspath() y os.path.normpath() .

PPS Le projet de loi sur l'emploi et l'égalité des chances : comme Peter Briggs l'a mentionné dans les commentaires, l'approche simple décrite plus haut peut échouer :

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

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

PPPS : comme l'a indiqué bluenote10, 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 de os.path.commonprefix de Python ?

PPPPS : en commençant par Python 3.4, nous avons pathlib Un module qui offre un environnement plus sain pour la manipulation des chemins d'accès. Je suppose que le préfixe commun d'un ensemble de chemins peut être obtenu en récupérant tous les préfixes de chaque chemin (avec PurePath.parents() ), en prenant l'intersection de tous ces ensembles de 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.

0 votes

Exactement ce dont j'ai besoin. Merci pour votre réponse rapide. J'accepterai votre réponse une fois que la restriction de temps sera levée.

10 votes

Soyez prudent avec commonprefix comme par exemple le préfixe commun de /usr/var/log y /usr/var2/log est renvoyée sous la forme /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).

0 votes

@PeterBriggs : Merci, cette mise en garde est importante. J'ai ajouté un PPS.

107voto

warvariuc Points 11787

os.path.relpath :

Renvoie un chemin d'accès relatif au chemin d'accès, soit à partir du répertoire actuel, soit à partir 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'

Ainsi, si le chemin relatif commence par '..' - cela signifie que le deuxième chemin n'est pas un 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'

3 votes

Vérification de la présence de os.pardir est plus robuste que la vérification de la présence de .. (il est vrai qu'il n'y a pas beaucoup d'autres conventions).

15 votes

Est-ce que je me trompe ou est-ce que os.relpath plus puissant puisqu'il gère .. y PurePath.relative_to() ne le fait pas ? Ai-je oublié quelque chose ?

0 votes

@RaySalemi a raison, mais il convient de noter que >>> Path('/usr/var').relative_to('/usr/var/log') échoue avec un ValueError

16voto

Brandon Invergo Points 2104

Une autre option est

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

0 votes

Elle renvoie toujours un chemin relatif ; cela n'indique pas directement si l'un des chemins est au-dessus de l'autre (on peut vérifier la présence de os.pardir devant les deux chemins relatifs possibles qui en résultent).

15voto

Tahlor Points 136

Un résumé de la suggestion de jme, utilisant pathlib, dans 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'

4voto

Jan Stürtz Points 41

Pure Python2 w/o 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 "."

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X