파이썬 : 두 절대 경로를 비교하여 상대 경로 얻기


143

두 가지 절대 경로가 있다고 가정 해보십시오. 경로 중 하나가 참조하는 위치가 다른 경로의 자손인지 확인해야합니다. 사실이라면 조상으로부터 후손의 상대 경로를 찾아야합니다. 파이썬에서 이것을 구현하는 좋은 방법은 무엇입니까? 혜택을받을 수있는 라이브러리가 있습니까?

답변:


167

os.path.commonprefix ()os.path.relpath () 는 당신의 친구입니다 :

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

따라서 공통 접두사가 경로 중 하나인지, 즉 경로 중 하나가 공통 조상인지 여부를 테스트 할 수 있습니다.

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

그런 다음 상대 경로를 찾을 수 있습니다.

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

이 방법을 사용하여 둘 이상의 경로를 처리하고 모든 경로가 모두 하나 아래에 있는지 테스트 할 수 있습니다.

추신 : 경로의 모양에 따라 먼저 정규화를 수행하려고 할 수 있습니다 (이것은 항상 '/'로 끝나는 지 아닌지 또는 일부 경로가 상대적인지 모르는 상황에서 유용합니다). 관련 함수에는 os.path.abspath ()os.path.normpath ()가 있습니다.

PPS : Peter Briggs가 의견에서 언급했듯이 위에서 설명한 간단한 접근 방식은 실패 할 수 있습니다.

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

비록 경로의 일반적인 접두사 /usr/var아닙니다 . 호출하기 전에 모든 경로를 '/'로 commonprefix()끝내면이 (특정) 문제가 해결됩니다.

PPPS : bluenote10에서 언급했듯이 슬래시를 추가해도 일반적인 문제는 해결되지 않습니다. 그의 후속 질문은 다음과 같습니다. Python의 os.path.commonprefix의 오류를 피하는 방법?

PPPPS : Python 3.4부터는 더 정확한 경로 조작 환경을 제공하는 모듈 인 pathlib가 있습니다. 경로 세트의 공통 접두사는 각 경로의 모든 접두사 ( PurePath.parents())를 가져 와서이 모든 부모 세트의 교차점을 취하고 가장 긴 공통 접두사를 선택하여 얻을 수 있다고 생각 합니다.

PPPPPS : Python 3.5는이 질문에 대한 올바른 해결책을 제시했습니다 os.path.commonpath(). 유효한 경로를 반환합니다.


정확히 내가 필요한 것. 신속한 답변 감사합니다. 시간 제한이 해제되면 답변을 수락합니다.
tamakisquare

10
와 함께주의 commonprefix의 공통 접두사를 예로, /usr/var/log그리고 /usr/var2/log반환됩니다 /usr/var당신이 무엇을 기대할 아마하지 않은 -. (이 유효 디렉토리하지 않은 경로를 반환하는 것도 가능합니다.)
피터 브릭스

@ PeterBriggs : 감사합니다.이 경고는 중요합니다. PPS를 추가했습니다.
Eric O Lebigot 2019

1
@EOL : 정말 슬래시 :( 추가하여 문제를 해결하는 방법을 볼 수 없습니다 우리는 무엇을해야합니다. ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10

1
@EOL :이 문제에 대한 매력적인 해결책을 찾지 못했기 때문에 별도의 질문 으로이 하위 문제를 논의해도 괜찮습니다 .
bluenote10

86

os.path.relpath:

현재 디렉토리 또는 선택적 시작점에서 상대 경로를 경로로 리턴하십시오.

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

따라서 상대 경로로 시작 '..'하면 두 번째 경로가 첫 번째 경로의 후손이 아님을 의미합니다.

Python3에서는 다음을 사용할 수 있습니다 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
존재 여부를 확인하는 것이 확인하는 os.pardir것보다 강력합니다 ..(그렇지만 다른 규칙은 많지 않습니다).
Eric O Lebigot

8
내가 os.relpath처리 ..하고 PurePath.relative_to()하지 않기 때문에 내가 틀리거나 더 강력 합니까? 뭔가 빠졌습니까?
Ray Salemi

15

다른 옵션은

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

이것은 항상 상대 경로를 반환합니다. 이것은 경로 중 하나가 다른 경로 위에 있는지 여부를 직접 나타내는 것은 아닙니다 (하나는 os.pardir가능한 두 개의 상대 경로 앞에 존재하는지 확인할 수 있습니다 ).
Eric O Lebigot

8

Python 3에서 pathlib를 사용하여 jme의 제안을 작성했습니다.

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'

그래서 dir1.relative_to(dir2)PosixPath을 줄 것이다 ( '.')가 같은 경우. 사용 if dir2 in dir1.parents하면 ID 케이스가 제외됩니다. 누군가가 경로를 비교하고 경로와 relative_to()호환되는 경우 실행 하려면 더 나은 솔루션은 if dir2 in (dir1 / 'x').parents또는 일 수 있습니다 if dir2 in dir1.parents or dir2 == dir1. 그런 다음 모든 경로 호환성 사례를 다룹니다.
ingyhere

3

순수 Python2 (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 "."

이것은 좋아 보이지만, 우연히 만났을 때 cwdpath같은 문제가 있습니다. 그 두 같은과 반환하거나 경우 먼저 확인해야 ""또는"."
Srđan Popić

1

편집 : Python3을 사용하는 가장 좋은 방법은 jme의 답변을 참조하십시오.

pathlib를 사용하면 다음과 같은 솔루션이 있습니다.

son의 자손 인지 확인 parent하고 둘 다 Path객체 인지 확인한다고 가정 해 보겠습니다 . 경로 에서 부품 목록을 얻을 수 있습니다 list(parent.parts). 그런 다음 아들의 시작이 부모의 세그먼트 목록과 같은지 확인합니다.

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

남은 부분을 얻으려면 할 수 있습니다.

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

문자열이지만 다른 Path 객체의 생성자로 사용할 수도 있습니다.


4
그것보다 훨씬 쉽습니다 : 단순히 parent in son.parents, 그렇다면 나머지를 얻는 것입니다 son.relative_to(parent).
jme

@jme 답이 더 낫습니다. 왜 게시하지 않습니까?
Jeremy Cochoy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.