Python:2つの絶対パスを比較して相対パスを取得する


143

たとえば、2つの絶対パスがあります。パスの1つによって参照されている場所が他の子孫であるかどうかを確認する必要があります。trueの場合、祖先からの子孫の相対パスを見つける必要があります。これをPythonで実装する良い方法は何ですか?私が恩恵を受けることができるライブラリはありますか?

回答:


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

したがって、共通のプレフィックスがパスの1つであるかどうか、つまり、パスの1つが共通の祖先であるかどうかをテストできます。

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]

この方法では、3つ以上のパスを処理し、すべてのパスがいずれか1つを下回っているかどうかをテストすることもできます。

PS:パスがどのように見えるかに応じて、最初にいくつかの正規化を実行する必要があります(これは、パスが常に「/」で終わるかどうかがわからない場合や、一部のパスが相対パスである場合に役立ちます)。関連する関数には、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-あなたが期待するものはおそらくされていません。(また、無効なディレクトリであるパスを返すことも可能です。)
Peter Briggs

@PeterBriggs:ありがとう、この警告は重要です。PPSを追加しました。
エリックOレビゴット

1
@EOL:私は本当にスラッシュを付加することによって、問題を解決する方法を見ていない:(私たちが持っているどのような場合。['/usr/var1/log/', '/usr/var2/log/']
bluenote10

1
@EOL:この問題の魅力的な解決策を見つけることができなかったので、このサブ問題について別の質問で説明してもかまいません。
bluenote10 2014

86

os.path.relpath

現在のディレクトリまたはオプションの開始点からpathへの相対ファイルパスを返します。

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

したがって、相対パスが次で始まる場合-2 '..'番目のパスが最初のパスの子孫ではないことを意味します。

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、2009

8
私は間違っているのですか、それともos.relpath処理..して処理PurePath.relative_to()しないのでより強力ですか?何か不足していますか?
レイサレミ2018年

15

別のオプションは

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

これは常に相対パスを返します。これは、パスの一方が他方の上にあるかどうかを直接示すものではありません(os.pardirただし、2つの可能な相対パスの前に存在を確認できます)。
Eric O Lebigot、2011

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が同じ場合に問題があります。これらの2つが同じとリターンのどちらかであるならば、それは最初にチェックすべき"""."
SrđanPopić

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、2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.