如何检查一个目录是否是另一个目录的子目录

103 投票
14 回答
72184 浏览
提问于 2025-04-16 04:41

我想用Python写一个模板系统,这个系统可以包含文件。

比如说:

    This is a template
    You can safely include files with safe_include`othertemplate.rst`

你知道,包含文件可能会有风险。例如,如果我在一个网页应用中使用这个模板系统,允许用户创建自己的模板,他们可能会做一些不安全的事情,比如:

I want your passwords: safe_include`/etc/password`

所以,我必须限制只能包含某个特定子目录下的文件(例如 /home/user/templates)。

现在的问题是:我该如何检查 /home/user/templates/includes/inc1.rst 是否在 /home/user/templates 的子目录下呢?

下面的代码能否正常工作并且安全呢?

import os.path

def in_directory(file, directory, allow_symlink = False):
    #make both absolute    
    directory = os.path.abspath(directory)
    file = os.path.abspath(file)

    #check whether file is a symbolic link, if yes, return false if they are not allowed
    if not allow_symlink and os.path.islink(file):
        return False

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

只要 allow_symlink 设置为False,我觉得应该是安全的。当然,如果允许符号链接的话,就会不安全,因为用户可能会创建这样的链接。

更新 - 解决方案 上面的代码在中间目录是符号链接时不起作用。 为了防止这种情况,你需要使用 realpath 而不是 abspath

更新:在目录后面加一个斜杠来解决 commonprefix() 的问题,这是Reorx指出的。

这也使得 allow_symlink 变得不必要,因为符号链接会被展开到它们的真实目标。

import os.path

def in_directory(file, directory):
    #make both absolute    
    directory = os.path.join(os.path.realpath(directory), '')
    file = os.path.realpath(file)

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

14 个回答

48

许多建议方法的问题

如果你打算用字符串比较或者 os.path.commonprefix 方法来检查目录的父子关系,这些方法在处理名字相似的路径或相对路径时容易出错。例如:

  • /path/to/files/myfile 会被很多方法错误地显示为 /path/to/file 的子路径。
  • /path/to/files/../../myfiles 也不会被很多方法显示为 /path/myfiles/myfile 的父路径,但实际上它确实是。

Rob Dennis 在之前的回答中提供了一种很好的方法,可以在不遇到这些问题的情况下比较路径的父子关系。Python 3.4 增加了 pathlib 模块,这个模块可以更智能地处理这些路径操作,甚至可以不依赖底层操作系统。jme 在另一个之前的回答中描述了如何使用 pathlib 来准确判断一个路径是否是另一个路径的子路径。如果你不想使用 pathlib(不太明白为什么,因为它真的很棒),那么 Python 3.5 引入了一种新的基于操作系统的方法 os.path,可以用更少的代码以类似准确和无误的方式进行路径的父子关系检查。

Python 3.5 的新特性

Python 3.5 引入了 os.path.commonpath 函数。这个方法是特定于代码运行的操作系统的。你可以用 commonpath 以以下方式准确判断路径的父子关系:

def path_is_parent(parent_path, child_path):
    # Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
    parent_path = os.path.abspath(parent_path)
    child_path = os.path.abspath(child_path)

    # Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
    return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])

简洁的一行代码

在 Python 3.5 中,你可以把所有内容合并成一个一行的 if 语句。虽然这看起来不太美观,还包含了不必要的重复调用 os.path.abspath,并且肯定不符合 PEP 8 的 79 字符行长度规范,但如果你喜欢这种风格,可以试试:

if os.path.commonpath([os.path.abspath(parent_path_to_test)]) == os.path.commonpath([os.path.abspath(parent_path_to_test), os.path.abspath(child_path_to_test)]):
    # Yes, the child path is under the parent path

Python 3.9 的新特性

pathlibPurePath 上增加了一个新方法 is_relative_to,可以直接执行这个功能。如果你需要了解如何使用它,可以查看Python 文档中关于 is_relative_to 的说明。或者你也可以查看我另一个回答,里面有更详细的使用说明。

114

Python 3的pathlib模块让这个事情变得简单明了,它有一个叫做Path.parents的属性。举个例子:

from pathlib import Path

root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')

然后:

>>> root in child.parents
True
>>> other in child.parents
False
12

os.path.realpath(path):这个函数会返回你指定的文件名的标准路径,也就是把路径中遇到的任何符号链接都去掉(前提是你的操作系统支持这种功能)。

你可以把它用在文件夹和子文件夹的名字上,然后检查子文件夹的名字是否是以父文件夹的名字开头的。

撰写回答