Python: Dapatkan jalur relatif dari membandingkan dua jalur absolut

143

Katakanlah, saya memiliki dua jalur absolut. Saya perlu memeriksa apakah lokasi yang dirujuk oleh salah satu jalur adalah turunan dari jalur lainnya. Jika benar, saya perlu mengetahui jalur relatif keturunan dari leluhur. Apa cara yang baik untuk mengimplementasikan ini di Python? Apakah ada perpustakaan yang bisa saya manfaatkan?

tamakisquare
sumber

Jawaban:

167

os.path.commonprefix () dan os.path.relpath () adalah teman Anda:

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

Anda dapat menguji apakah awalan umum adalah salah satu jalur, yaitu jika salah satu jalur adalah leluhur yang sama:

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

Anda kemudian dapat menemukan jalur relatif:

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

Anda bahkan dapat menangani lebih dari dua jalur, dengan metode ini, dan menguji apakah semua jalur di bawah salah satunya.

PS : tergantung pada bagaimana jalan Anda terlihat, Anda mungkin ingin melakukan normalisasi terlebih dahulu (ini berguna dalam situasi di mana orang tidak tahu apakah selalu berakhir dengan '/' atau tidak, atau jika beberapa jalur relatif). Fungsi yang relevan termasuk os.path.abspath () dan os.path.normpath () .

PPS : seperti yang disebutkan Peter Briggs dalam komentar, pendekatan sederhana yang dijelaskan di atas dapat gagal:

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

meskipun /usr/varadalah bukan awalan umum dari jalur. Memaksa semua jalur diakhiri dengan '/' sebelum memanggil commonprefix()memecahkan masalah (spesifik) ini.

PPPS : seperti yang disebutkan bluenote10, menambahkan garis miring tidak menyelesaikan masalah umum. Berikut adalah pertanyaan lanjutannya: Bagaimana cara menghindari kesalahan dari os.path.commonprefix Python?

PPPPS : dimulai dengan Python 3.4, kami memiliki pathlib , modul yang menyediakan lingkungan manipulasi jalur yang lebih waras. Saya kira awalan umum dari satu set lintasan dapat diperoleh dengan mendapatkan semua awalan dari setiap lintasan (dengan PurePath.parents()), mengambil persimpangan dari semua set induk ini, dan memilih awalan umum terpanjang.

PPPPPS : Python 3.5 memperkenalkan solusi yang tepat untuk pertanyaan ini :,os.path.commonpath() yang mengembalikan jalur yang valid.

Eric O Lebigot
sumber
Apa yang saya butuhkan. Terima kasih atas jawaban cepat Anda. Akan menerima jawaban Anda setelah batasan waktu dicabut.
tamakisquare
10
Hati-hati dengan commonprefix, misalnya awalan umum untuk /usr/var/logdan /usr/var2/logdikembalikan sebagai /usr/var- yang mungkin bukan yang Anda harapkan. (Mungkin juga untuk mengembalikan jalur yang bukan direktori yang valid.)
Peter Briggs
@ PeterBriggs: Terima kasih, peringatan ini penting. Saya menambahkan PPS.
Eric O Lebigot
1
@ EOL: Saya tidak benar-benar melihat cara untuk memperbaiki masalah dengan menambahkan slash :(. Bagaimana jika kita miliki ['/usr/var1/log/', '/usr/var2/log/']?
bluenote10
1
@ EOL: Karena saya gagal menemukan solusi yang menarik untuk masalah ini, saya pikir mungkin tidak masalah untuk membahas sub-masalah ini dalam pertanyaan terpisah .
bluenote10
86

os.path.relpath:

Kembalikan file relatif ke jalur baik dari direktori saat ini atau dari titik awal opsional.

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

Jadi, jika jalur relatif dimulai dengan '..'- itu berarti bahwa jalur kedua bukan turunan dari jalur pertama.

Di Python3 Anda dapat menggunakan 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
sumber
2
Memeriksa keberadaan os.pardirlebih kuat daripada memeriksa ..(disepakati, tidak ada banyak konvensi lain).
Eric O Lebigot
8
Apakah saya salah atau os.relpathlebih kuat karena ia menangani ..dan PurePath.relative_to()tidak? Apakah saya melewatkan sesuatu?
Ray Salemi
15

Pilihan lainnya adalah

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

sumber
Ini selalu mengembalikan jalur relatif; ini tidak secara langsung menunjukkan apakah salah satu jalur berada di atas yang lain (satu dapat memeriksa keberadaan os.pardirdi depan dua jalur relatif yang dihasilkan, meskipun).
Eric O Lebigot
8

Penulisan saran jme, menggunakan pathlib, dengan 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
sumber
Jadi dir1.relative_to(dir2)akan memberikan PosixPath ('.') Jika mereka sama. Ketika Anda menggunakan if dir2 in dir1.parentsmaka tidak termasuk kasus identitas. Jika seseorang membandingkan Paths dan ingin dijalankan relative_to()jika mereka kompatibel dengan path, solusi yang lebih baik mungkin if dir2 in (dir1 / 'x').parentsatau if dir2 in dir1.parents or dir2 == dir1. Kemudian semua kasus kompatibilitas jalur tercakup.
sana
3

Python2 murni tanpa 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
sumber
Yang ini terlihat bagus, tetapi, ketika saya menemukan, ada masalah kapan cwddan pathsama. itu harus memeriksa dulu apakah keduanya sama dan mengembalikan salah satu ""atau"."
Srđan Popić
1

Sunting: Lihat jawaban jme untuk cara terbaik dengan Python3.

Menggunakan pathlib, Anda memiliki solusi berikut:

Katakanlah kita ingin memeriksa apakah sonketurunannya parent, dan keduanya adalah Pathobjek. Kita bisa mendapatkan daftar bagian - bagian di jalan dengan list(parent.parts). Kemudian, kami hanya memeriksa bahwa permulaan anak laki-laki sama dengan daftar segmen orang tua.

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

Jika Anda ingin mendapatkan bagian yang tersisa, Anda bisa melakukannya

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

Ini adalah string, tetapi tentu saja Anda dapat menggunakannya sebagai konstruktor dari objek Path lainnya.

Jeremy Cochoy
sumber
4
Ini bahkan lebih mudah dari itu: sederhana parent in son.parents, dan jika ya, dapatkan sisanya son.relative_to(parent).
jme
@ jme Jawaban Anda bahkan lebih baik, mengapa Anda tidak mempostingnya?
Jeremy Cochoy