Python: Nhận đường dẫn tương đối từ việc so sánh hai đường dẫn tuyệt đối


143

Nói rằng, tôi có hai con đường tuyệt đối. Tôi cần kiểm tra xem vị trí được đề cập bởi một trong các đường dẫn có phải là hậu duệ của đường dẫn kia không. Nếu đúng, tôi cần tìm ra con đường tương đối của con cháu từ tổ tiên. Cách tốt để thực hiện điều này trong Python là gì? Bất kỳ thư viện mà tôi có thể được hưởng lợi từ?

Câu trả lời:


167

os.path.commonprefix ()os.path.relpath () là bạn của bạn:

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

Do đó, bạn có thể kiểm tra xem tiền tố chung có phải là một trong các đường dẫn hay không, tức là nếu một trong các đường dẫn là tổ tiên chung:

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

Sau đó, bạn có thể tìm thấy các đường dẫn tương đối:

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

Bạn thậm chí có thể xử lý nhiều hơn hai đường dẫn, với phương thức này và kiểm tra xem tất cả các đường dẫn có nằm dưới một trong số chúng không.

PS : tùy thuộc vào đường dẫn của bạn trông như thế nào, trước tiên bạn có thể muốn thực hiện một số chuẩn hóa (điều này hữu ích trong trường hợp người ta không biết liệu chúng luôn kết thúc bằng '/' hay không, hoặc nếu một số đường dẫn có tương đối). Các hàm có liên quan bao gồm os.path.abspath ()os.path.normpath () .

PPS : như Peter Briggs đã đề cập trong các bình luận, cách tiếp cận đơn giản được mô tả ở trên có thể thất bại:

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

mặc dù /usr/varkhông một tiền tố chung của các đường dẫn. Buộc tất cả các đường dẫn kết thúc bằng '/' trước khi gọi commonprefix()giải quyết vấn đề (cụ thể) này.

PPPS : như bluenote10 đã đề cập, việc thêm dấu gạch chéo không giải quyết được vấn đề chung. Đây là câu hỏi tiếp theo của anh ấy: Làm thế nào để tránh sự ngụy biện của os.path.commonprefix của Python?

PPPPS : bắt đầu với Python 3.4, chúng tôi có pathlib , một mô-đun cung cấp môi trường thao tác đường dẫn saner. Tôi đoán rằng tiền tố chung của một tập hợp các đường dẫn có thể thu được bằng cách lấy tất cả các tiền tố của mỗi đường dẫn (với PurePath.parents()), lấy giao điểm của tất cả các tập hợp cha mẹ này và chọn tiền tố chung dài nhất.

PPPPPS : Python 3.5 đã giới thiệu một giải pháp phù hợp cho câu hỏi này : os.path.commonpath(), trả về một đường dẫn hợp lệ.


Chính xác những gì tôi cần. Cảm ơn câu trả lời nhanh chóng của bạn. Sẽ chấp nhận câu trả lời của bạn sau khi hạn chế thời gian được dỡ bỏ.
tamakisapes

10
Hãy cẩn thận commonprefix, ví dụ như tiền tố phổ biến cho /usr/var/log/usr/var2/logđược trả về là /usr/var- có lẽ không phải là điều bạn mong đợi. (Cũng có thể nó trả về các đường dẫn không phải là thư mục hợp lệ.)
Peter Briggs

@PeterBriggs: Cảm ơn, sự cảnh báo này rất quan trọng. Tôi đã thêm một PPS.
Eric O Lebigot

1
@EOL: Tôi thực sự không thấy cách khắc phục sự cố bằng cách thêm dấu gạch chéo :(. Nếu chúng ta có ['/usr/var1/log/', '/usr/var2/log/']thì sao?
bluenote10

1
@EOL: Vì tôi không tìm được giải pháp hấp dẫn cho vấn đề này nên tôi có thể thảo luận về vấn đề phụ này trong một câu hỏi riêng .
bluenote10

86

os.path.relpath:

Trả lại một filepath tương đối cho đường dẫn từ thư mục hiện tại hoặc từ một điểm bắt đầu tùy chọn.

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

Vì vậy, nếu đường dẫn tương đối bắt đầu bằng '..'- nó có nghĩa là đường dẫn thứ hai không phải là hậu duệ của đường dẫn thứ nhất.

Trong Python3 bạn có thể sử dụng 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
Kiểm tra sự hiện diện của os.pardirmạnh mẽ hơn kiểm tra ..(đã đồng ý, mặc dù không có nhiều quy ước khác).
Eric O Lebigot

8
Tôi sai hay os.relpathmạnh hơn vì nó xử lý ..PurePath.relative_to()không? Tui bỏ lỡ điều gì vậy?
Ray Salemi

15

Một lựa chọn khác là

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

Điều này luôn trả về một con đường tương đối; điều này không trực tiếp chỉ ra liệu một trong những đường dẫn có nằm trên đường kia hay không (người ta có thể kiểm tra sự hiện diện của os.pardirphía trước hai đường dẫn tương đối có thể xảy ra hay không).
Eric O Lebigot

8

Một bài viết về đề xuất của jme, sử dụng pathlib, trong 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'

Vì vậy, dir1.relative_to(dir2)sẽ cung cấp cho PosixPath ('.') Nếu chúng giống nhau. Khi bạn sử dụng if dir2 in dir1.parentsthì nó loại trừ trường hợp nhận dạng. Nếu ai đó đang so sánh Đường dẫn và muốn chạy relative_to()nếu chúng tương thích với đường dẫn, một giải pháp tốt hơn có thể là if dir2 in (dir1 / 'x').parentshoặc if dir2 in dir1.parents or dir2 == dir1. Sau đó, tất cả các trường hợp tương thích đường dẫn được bảo hiểm.
trong

3

Python2 tinh khiết 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 "."

Điều này có vẻ tốt, nhưng, khi tôi vấp ngã, có một vấn đề khi cwdpathgiống nhau. trước tiên, nên kiểm tra xem hai cái đó có giống nhau không và trả về ""hoặc"."
Srđan Popić

1

Chỉnh sửa: Xem câu trả lời của jme để biết cách tốt nhất với Python3.

Sử dụng pathlib, bạn có giải pháp sau:

Giả sử chúng ta muốn kiểm tra xem có phải sonlà hậu duệ parenthay không và cả hai đều là Pathđối tượng. Chúng ta có thể nhận được một danh sách các phần trong đường dẫn với list(parent.parts). Sau đó, chúng tôi chỉ kiểm tra xem sự bắt đầu của con trai có bằng danh sách các phân đoạn của cha mẹ không.

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

Nếu bạn muốn có được phần còn lại, bạn có thể làm

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

Đó là một chuỗi, nhưng tất nhiên bạn có thể sử dụng nó như là một hàm tạo của một đối tượng Path khác.


4
Nó thậm chí còn dễ dàng hơn thế: đơn giản parent in son.parents, và nếu có, nhận phần còn lại với son.relative_to(parent).
jme

@jme Bạn trả lời thậm chí còn tốt hơn, tại sao bạn không đăng nó?
Jeremy Cochoy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.