Python: รับเส้นทางสัมพัทธ์จากการเปรียบเทียบสองเส้นทางสัมบูรณ์


143

บอกว่าฉันมีสองเส้นทางสัมบูรณ์ ฉันต้องตรวจสอบว่าตำแหน่งที่อ้างถึงโดยหนึ่งในเส้นทางคือลูกหลานของอีก ถ้าเป็นจริงฉันต้องหาเส้นทางญาติของผู้สืบทอดจากบรรพบุรุษ เป็นวิธีที่ดีในการนำไปใช้ใน 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
'/'

คุณสามารถทดสอบว่าคำนำหน้าทั่วไปเป็นหนึ่งในเส้นทางหรือไม่เช่นถ้าหนึ่งในเส้นทางนั้นเป็นบรรพบุรุษร่วมกัน:

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 กล่าวไว้การเพิ่มเครื่องหมายสแลชไม่สามารถแก้ปัญหาทั่วไปได้ นี่คือคำถามติดตามของเขา: จะหลีกเลี่ยงการเข้าใจผิดของ os.path.commonprefix ของ Python ได้อย่างไร

PPPPS : เริ่มต้นด้วย Python 3.4 เรามีpathlibโมดูลที่จัดเตรียมสภาพแวดล้อมการจัดการพา ธ saner ฉันเดาว่าคำนำหน้าทั่วไปของชุดของเส้นทางสามารถรับได้โดยนำหน้าทั้งหมดของแต่ละเส้นทาง (ด้วยPurePath.parents()), การตัดกันของชุดผู้ปกครองเหล่านี้ทั้งหมดและเลือกคำนำหน้าทั่วไปที่ยาวที่สุด

PPPPPS : Python 3.5 แนะนำวิธีแก้ปัญหาที่เหมาะสมสำหรับคำถามนี้: os.path.commonpath()ซึ่งส่งคืนพา ธ ที่ถูกต้อง


สิ่งที่ฉันต้องการ ขอบคุณสำหรับคำตอบที่รวดเร็ว จะยอมรับคำตอบของคุณเมื่อมีการ จำกัด เวลา
tamakisquare

10
ใช้ความระมัดระวังcommonprefixเช่นเช่นคำนำหน้าทั่วไปสำหรับ/usr/var/logและ/usr/var2/logส่งคืนเป็น/usr/var- ซึ่งอาจไม่ใช่สิ่งที่คุณคาดหวัง (อาจเป็นไปได้ที่จะส่งคืนพา ธ ที่ไม่ใช่ไดเรกทอรีที่ถูกต้อง)
Peter Briggs

@ PeterBriggs: ขอบคุณข้อแม้นี้มีความสำคัญ ฉันเพิ่ม PPS
Eric O Lebigot

1
@EOL: ฉันไม่เห็นวิธีการแก้ไขปัญหาโดยการเพิ่มเครื่องหมายทับ :( จะเกิดอะไรขึ้นถ้าเรามี['/usr/var1/log/', '/usr/var2/log/']?
bluenote10 10

1
@EOL: เนื่องจากฉันล้มเหลวในการค้นหาวิธีแก้ไขปัญหาที่น่าสนใจสำหรับปัญหานี้ฉันถึงแม้ว่ามันอาจไม่เป็นไรที่จะพูดถึงปัญหาย่อยนี้ในคำถามอื่น
bluenote10 10

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

บทความแนะนำของ jme โดยใช้ pathlib ใน 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'

ดังนั้นdir1.relative_to(dir2)จะให้ PosixPath ('.') หากพวกเขาเหมือนกัน เมื่อคุณใช้if dir2 in dir1.parentsแล้วมันจะไม่รวมกรณีตัวตน หากมีใครเปรียบเทียบเส้นทางและต้องการที่จะทำงานrelative_to()หากพวกเขาเป็นเส้นทางได้ทางออกที่ดีกว่าอาจจะเป็นหรือif dir2 in (dir1 / 'x').parents if dir2 in dir1.parents or dir2 == dir1จากนั้นทุกกรณีของความเข้ากันได้ของเส้นทางจะได้รับการคุ้มครอง
ingyhere

3

Pure Python2 ที่ไม่มีค่าใช้จ่าย:

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 "."

อันนี้ดูดี แต่อย่างที่ฉันสะดุดมีปัญหาเมื่อcwdและpathเป็นเหมือนกัน ควรตรวจสอบก่อนว่าทั้งสองอย่างนั้นเหมือนกัน""หรือไม่และกลับมาหรือ"."
SrđanPopić

1

แก้ไข:ดูคำตอบของ jme สำหรับวิธีที่ดีที่สุดด้วย Python3

การใช้ 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):])

มันเป็นสตริง แต่แน่นอนคุณสามารถใช้มันเป็นตัวสร้างของวัตถุเส้นทางอื่น


4
ก็จะยิ่งง่ายกว่าว่าเพียงและถ้ามันก็คือการที่เหลือนั้นด้วยparent in son.parents son.relative_to(parent)
jme

@jme คุณตอบได้ดียิ่งขึ้นทำไมคุณไม่โพสต์มัน
Jeremy Cochoy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.