Python: obtenha um caminho relativo ao comparar dois caminhos absolutos


143

Diga, eu tenho dois caminhos absolutos. Preciso verificar se o local referente a um dos caminhos é descendente do outro. Se for verdade, preciso descobrir o caminho relativo do descendente a partir do ancestral. Qual é uma boa maneira de implementar isso em Python? Alguma biblioteca da qual eu possa me beneficiar?

Respostas:


167

os.path.commonprefix () e os.path.relpath () são seus amigos:

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

Assim, você pode testar se o prefixo comum é um dos caminhos, ou seja, se um dos caminhos é um ancestral comum:

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

Você pode encontrar os caminhos relativos:

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

Você pode até manipular mais de dois caminhos, com esse método, e testar se todos os caminhos estão todos abaixo de um deles.

PS : dependendo da aparência de seus caminhos, primeiro você deve executar alguma normalização (isso é útil em situações em que não se sabe se elas sempre terminam com '/' ou não, ou se alguns deles são relativos). Funções relevantes incluem os.path.abspath () e os.path.normpath () .

PPS : como Peter Briggs mencionou nos comentários, a abordagem simples descrita acima pode falhar:

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

mesmo que não/usr/var seja um prefixo comum dos caminhos. Forçar todos os caminhos a terminarem com '/' antes de chamar resolve esse problema (específico).commonprefix()

PPPS : como o bluenote10 mencionou, a adição de uma barra não resolve o problema geral. Aqui está sua pergunta seguinte: Como contornar a falácia do os.path.commonprefix do Python?

PPPPS : a partir do Python 3.4, temos o pathlib , um módulo que fornece um ambiente de manipulação de caminhos mais saudável . Eu acho que o prefixo comum de um conjunto de caminhos pode ser obtido obtendo todos os prefixos de cada caminho (com PurePath.parents()), fazendo a interseção de todos esses conjuntos pai e selecionando o prefixo comum mais longo.

PPPPPS : Python 3.5 introduziu uma solução adequada para esta pergunta :,os.path.commonpath() que retorna um caminho válido.


Exatamente o que eu preciso. Obrigado pela sua resposta rápida. Aceitará sua resposta assim que a restrição de tempo for levantada.
tamakisquare

10
Tome cuidado com commonprefix, por exemplo, o prefixo comum para /usr/var/loge /usr/var2/logé retornado como /usr/var- o que provavelmente não é o que você esperaria. (Também é possível para que ele retorne caminhos que não são diretórios válidos.)
Peter Briggs

@ PeterBriggs: Obrigado, esta ressalva é importante. Eu adicionei um PPS.
Eric O Lebigot

1
@EOL: Eu realmente não vejo como corrigir o problema, adicionando uma barra :( E se nós temos. ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10

1
@EOL: Como não consegui encontrar uma solução atraente para esse problema, pensei que seria bom discutir essa questão em outra questão .
bluenote10

86

os.path.relpath:

Retorne um caminho de arquivo relativo para o caminho do diretório atual ou de um ponto de início opcional.

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

Portanto, se o caminho relativo começar com '..'- isso significa que o segundo caminho não é descendente do primeiro caminho.

No Python3 você pode usar 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
Verificar a presença de os.pardiré mais robusto do que verificar ..(concordado, não existem muitas outras convenções).
Eric O Lebigot

8
Estou errado ou é os.relpathmais poderoso, pois lida com ..e PurePath.relative_to()não? Estou esquecendo de algo?
Ray Salemi

15

Outra opção é

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

Isso sempre retorna um caminho relativo; isso não indica diretamente se um dos caminhos está acima do outro ( os.pardirno entanto, é possível verificar a presença de dois caminhos relativos resultantes).
Eric O Lebigot

8

Uma descrição da sugestão de jme, usando pathlib, em 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'

Então dir1.relative_to(dir2), dará PosixPath ('.') Se eles forem iguais. Quando você usa if dir2 in dir1.parents, ele exclui o caso de identidade. Se alguém estiver comparando Paths e quiser executar relative_to()se for compatível com o caminho, uma solução melhor pode ser if dir2 in (dir1 / 'x').parentsou if dir2 in dir1.parents or dir2 == dir1. Todos os casos de compatibilidade de caminho são abordados.
ingyhere

3

Pure Python2 sem 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 "."

Este parece ser bom, mas, como eu tropeço, há um problema quando cwde pathé o mesmo. deve verificar primeiro se esses dois são iguais e retornar ""ou"."
Srđan Popić

1

Edit: Veja a resposta do jme para a melhor maneira com o Python3.

Usando pathlib, você tem a seguinte solução:

Digamos que queremos verificar se soné um descendente de parente ambos são Pathobjetos. Podemos obter uma lista das partes no caminho list(parent.parts). Então, apenas verificamos que o início do filho é igual à lista de segmentos do pai.

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

Se você deseja obter a parte restante, basta fazer

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

É uma string, mas é claro que você pode usá-la como construtora de outro objeto Path.


4
É ainda mais fácil do que isso: simplesmente parent in son.parents, e se for, obtendo o restante son.relative_to(parent).
JME

@ jme Sua resposta é ainda melhor, por que você não a publica?
Jeremy Cochoy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.