使用pathlib的relative_to处理同级目录

43

Python库pathlib提供了Path.relative_to。此函数适用于一个路径是另一个路径的子路径的情况,例如:

from pathlib import Path
foo = Path("C:\\foo")
bar = Path("C:\\foo\\bar")
bar.relative_to(foo)

> WindowsPath('bar')
然而,如果两个路径处于同一级别relative_to 将无法工作。
baz = Path("C:\\baz")
foo.relative_to(baz)

> ValueError: 'C:\\foo' does not start with 'C:\\baz'

我期望结果是

WindowsPath("..\\baz")

函数os.path.relpath可以正确执行此操作:

import os
foo = "C:\\foo"
bar = "C:\\bar"
os.path.relpath(foo, bar)

> '..\\foo'

有没有一种使用 pathlib.Path 实现 os.path.relpath 功能的方法?


6
你是否解决了这个问题?我也遇到了同样的问题。我想在尽可能使用pathlib而不是os.path的情况下进行标准化,但是这个问题让我束手无策。 - Phil
1
@Phil 在这种情况下似乎你被迫回到 os.path.relpath :( ... 似乎 pathlib 模块并没有被考虑作为 os.path 的替代品 :(. 或者你找到了一个只使用 pathlib 的解决方案吗? - Ciprian Tomoiagă
不,我还没有找到一个仅使用“pathlib”的解决方案。 - Phil
4个回答

41
第一部分解决了OP的问题,但如果像我一样,他真的想要相对于共同根目录的解决方案,那么第二部分就为他解决了这个问题。第三部分描述了我最初的方法,仅供参考。

相对路径

最近,在Python 3.4-6中,os.path模块已扩展以接受pathlib.Path对象。但在以下情况下,它不会返回Path对象,而必须强制包装结果。
foo = Path("C:\\foo")
baz = Path("C:\\baz")
Path(os.path.relpath(foo, baz))

> Path("..\\foo")

常用路径

我猜测您真正需要的是相对于共同根目录的路径。如果是这种情况,以下内容来自EOL,将更加有用。

Path(os.path.commonpath([foo, baz]))

> Path('c:/root')

公共前缀

在我接触到os.path.commonpath之前,我曾使用过os.path.commonprefix

foo = Path("C:\\foo")
baz = Path("C:\\baz")
baz.relative_to(os.path.commonprefix([baz,foo]))

> Path('baz')

但请注意,您不应该在这种情况下使用它(参见commonprefix:是的,那个老掉牙的问题

foo = Path("C:\\route66\\foo")
baz = Path("C:\\route44\\baz")
baz.relative_to(os.path.commonprefix([baz,foo]))

> ...
> ValueError : `c:\\route44\baz` does not start with `C:\\route`

但是我更倾向于使用来自J. F. Sebastian的以下方法。
Path(*os.path.commonprefix([foo.parts, baz.parts]))

> Path('c:/root')

...或者如果您感觉需要详细解释...

from itertools import takewhile
Path(*[set(i).pop() for i in (takewhile(lambda x : x[0]==x[1], zip(foo.parts, baz.parts)))])

7

这个问题很困扰我,所以这里提供一个仅使用pathlib的版本,我认为它可以实现os.path.relpath的功能。

def relpath(path_to, path_from):
    path_to = Path(path_to).resolve()
    path_from = Path(path_from).resolve()
    try:
        for p in (*reversed(path_from.parents), path_from):
            head, tail = p, path_to.relative_to(p)
    except ValueError:  # Stop when the paths diverge.
        pass
    return Path('../' * (len(path_from.parents) - len(head.parents))).joinpath(tail)

1
只是一个小问题,如果任何路径是符号链接,这将导致不同的结果。 - ypnos

2

这是一个基于递归的 @Brett_Ryland 的相对路径函数,使用pathlib库。我认为这个版本更易读,并且在大多数情况下,第一次尝试就可以成功,因此它应该与原始的 relative_to 函数具有类似的性能:

def relative(target: Path, origin: Path):
    """ return path of target relative to origin """
    try:
        return Path(target).resolve().relative_to(Path(origin).resolve())
    except ValueError as e: # target does not start with origin
        # recursion with origin (eventually origin is root so try will succeed)
        return Path('..').joinpath(relative(target, Path(origin).parent))

我喜欢这个,但是提醒一下,这会导致递归层级过多,所以对于
  • /home/me/subpath_a/subpath_b/file.exe
  • /home/me/subpath_c/subpath_d/file.exe
你会得到 ../../../file.exe 而不是 ../../file.exe我做了一个非常懒的修复,将这个放在一个内部函数中,然后在返回之前删除最后一个 ..,这样就能工作了,但不够美观。
- undefined
嗯 - 我以为起点会是一个目录。也就是说,你想要相对路径从哪个目录开始。所以如果你传递 origin=/home/me/subpath_c/subpath_d/file.exe,那么 file.exe 就被当作一个目录。所以从那里开始,你需要往回走3次,直到到达 me,也就是 ../../../ 而不是 ../..。换句话说,origin 是你的程序所在的目录,而不是一个文件。 - undefined

0
Python 3.12在Path.relative_to方法中添加了walk_up参数。现在你可以这样做:
>>> from pathlib import Path

>>> foo = Path.home() / 'foo'
>>> bar = Path.home() / 'bar'

>>> bar.relative_to(foo, walk_up=True)
PosixPath('../bar')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接