检查路径在Python中是否有效,而无需在路径的目标位置创建文件


98

我有一个路径(包括目录和文件名)。
我需要测试文件名是否有效,例如,文件系统是否允许我创建具有该名称的文件。
文件名中包含一些Unicode字符

可以安全地假设路径的目录段是有效且可访问的(我试图使这个问题更笼统地适用,并且显然我走得太远了)。

除非必须,否则我非常不想逃脱任何东西。

我会发布一些我正在处理的示例字符,但是显然它们会被堆栈交换系统自动删除。无论如何,我想保留标准的unicode实体,例如ö,仅转义文件名中无效的内容。


这里是要抓住的地方。路径目标上可能已经(可能没有)文件。如果该文件存在,我需要保留该文件,如果不存在,则不要创建该文件。

基本上,我想检查是否可以不实际打开写入路径的情况下写入路径(以及通常需要进行的自动文件创建/文件破坏)。

因此:

try:
    open(filename, 'w')
except OSError:
    # handle error here

从这里

这是不可接受的,因为它将覆盖我不想触摸的现有文件(如果存在),或者如果不存在则创建该文件。

我知道我可以做:

if not os.access(filePath, os.W_OK):
    try:
        open(filePath, 'w').close()
        os.unlink(filePath)
    except OSError:
        # handle error here

但这将在处创建文件filePath,然后我将不得不os.unlink

最后,似乎花了6或7行来完成应该简单os.isvalidpath(filePath)或相似的操作。


顺便说一句,我需要在(至少)Windows和MacOS上运行它,因此我想避免使用特定于平台的东西。

``


如果要测试路径是否存在并且可以对其进行写入,则只需创建并删除其他文件。给它起一个唯一的名称(或尽可能唯一),以避免出现多用户/多线程问题。否则,您正在查看签出权限,这将使您直接进入特定于操作系统的混乱状态。
托尼·霍普金森

3
@Tony Hopkinson-基本上,我想检查是否可以写路径而不实际写任何东西
假名称

如果您没有任何要写入文件的内容,那么为什么需要知道是否能够写入?
Karl Knechtel'3

@Karl Knechtel-如果我写它,并且那里已经有文件,它将损坏现有的文件。
假名称

2
@FakeName-您在这里总是会有一个微妙的比赛条件。在检查文件不存在但可以创建之前,再创建文件之间,可以通过其他一些过程来创建文件,并且无论如何您都会破坏文件。当然,这是否是一个现实的问题取决于您的用法...
很好

Answers:


153

tl; dr

调用is_path_exists_or_creatable()下面定义的函数。

严格地使用Python3。这就是我们的发展方向。

两个问题的故事

问题“如何测试路径名的有效性,以及对于有效路径名,这些路径的存在或可写性?” 显然是两个独立的问题。两者都很有趣,而且在这里还是我能找到的任何地方都没有收到真正令人满意的答案。

vikki答案可能是最接近的,但有以下明显的缺点:

  • 不必要地打开(然后无法可靠地关闭)文件句柄。
  • 不必要的写作( ...然后无法可靠地关闭或删除)0字节文件。
  • 忽略操作系统特定的错误,以区分不可忽略的无效路径名和可忽略的文件系统问题。毫不奇怪,这在Windows下至关重要。(见下文。
  • 忽略由外部进程同时(重新)移动要测试的路径名的父目录导致的竞争条件。(见下文。
  • 忽略此路径名导致的连接超时,该路径名位于陈旧,缓慢或暂时不可访问的文件系统上。这可能会使面向公众的服务遭受潜在的DoS驱动的攻击。(见下文。

我们将解决所有问题。

问题#0:路径名有效性又是什么?

在将我们脆弱的肉类衣服扔进蟒蛇般的痛苦中之前,我们可能应该定义“路径名有效性”的含义。究竟是什么定义了有效性?

“路径名有效性”是指路径名相对于当前系统的根文件系统语法正确性,无论该路径或其父目录是否物理存在。如果路径名符合根文件系统的所有语法要求,则在此定义下语法上正确。

所谓“根文件系统”,是指:

  • 在与POSIX兼容的系统上,文件系统已安装到根目录(/)。
  • 在Windows中,文件系统安装到%HOMEDRIVE%,包含当前的Windows安装(通常但结肠-后缀盘符必然C:)。

反过来,“语法正确性”的含义取决于根文件系统的类型。对于ext4(且不是大多数但与所有POSIX兼容的)文件系统,路径名称在且仅当该路径名称在语法上正确:

  • 不包含空字节(即,\x00在Python中)。这是所有POSIX兼容文件系统的硬性要求。
  • 包含不超过255个字节的路径组件(例如,'a'*256在Python中)。路径成分是含有不路径名的最长子串/字符(例如,bergtattindi,和fjeldkamrene在路径名/bergtatt/ind/i/fjeldkamrene)。

句法正确性。根文件系统。而已。

问题1:我们现在应如何进行路径名有效性?

令人惊讶的是,在Python中验证路径名是不直观的。我在这里与Fake Name达成坚定协议:官方os.path软件包应为此提供现成的解决方案。出于未知(可能不令人信服)的原因,事实并非如此。幸运的是,展开您自己的临时解决方案并不是那么费劲……

好的,实际上是。毛茸茸的 讨厌 它在发光时发出嘶哑和咯咯笑声时可能会发痒。但是你会怎么做?Nuthin'。

我们将很快进入低级代码的放射性深渊。但首先,让我们谈谈高级商店。当传递无效的路径名时,标准os.stat()os.lstat()函数会引发以下异常:

  • 对于驻留在不存在的目录中的路径名, FileNotFoundError
  • 对于现有目录中的路径名:
    • 在Windows下,WindowsErrorwinerror属性为123(即ERROR_INVALID_NAME)的实例。
    • 在所有其他操作系统下:
    • 对于包含空字节(即'\x00')的路径名,请使用的实例TypeError
    • 对于包含长度超过255个字节的路径成分的路径名,OSErrorerrcode属性的实例为:
      • 在SunOS和* BSD系列操作系统下,errno.ERANGE。(这似乎是操作系统级别的错误,否则称为POSIX标准的“选择性解释”。)
      • 在所有其他操作系统下,errno.ENAMETOOLONG

至关重要的是,这意味着仅存在于现有目录中的路径名是有效的。当传递的路径名驻留在不存在的目录中时,不管这些路径名是否无效,os.stat()andos.lstat()函数都会引发通用FileNotFoundError异常。目录存在优先于路径名无效。

这是否意味着不存在的目录中的路径名无效?是的-除非我们修改这些路径名以驻留在现有目录中。但是,这甚至安全可行吗?修改路径名是否应该阻止我们验证原始路径名?

要回答这个问题,请从上面回忆一下,ext4文件系统上语法正确的路径名不包含路径组件(A)包含空字节,或(B)长度超过255个字节。因此,ext4仅当该路径名中的所有路径组件均有效时,该路径名才有效。大多数 现实世界中感兴趣的文件系统都是如此。

那根学究的见解真的对我们有帮助吗?是。它将一次验证完整路径名的较大问题减少到仅验证该路径名中的所有路径组成部分的较小问题。通过遵循以下算法,可以以跨平台方式对任意路径名进行有效验证(无论该路径名是否位于现有目录中):

  1. 将该路径名拆分为路径组成部分(例如,将路径名/troldskog/faren/vild拆分为list ['', 'troldskog', 'faren', 'vild'])。
  2. 对于每个这样的组件:
    1. 将保证与该组件一起存在的目录的路径名加入新的临时路径名(例如/troldskog)。
    2. 将该路径名传递给os.stat()os.lstat()。如果该路径名及其组件无效,则可以确保此调用引发一个暴露无效类型的异常,而不是通用FileNotFoundError异常。为什么?因为该路径名位于现有目录中。(循环逻辑是循环的。)

是否有目录保证存在?是的,但通常只有一个:根文件系统的最顶层目录(如上定义)。

将驻留在任何其他目录(因此不保证存在)中的路径名传递给竞争条件os.stat()os.lstat()引发竞争条件,即使该目录先前已被测试存在。为什么?因为在执行该测试之后将该路径名传递给os.stat()or之前,无法阻止外部进程同时删除该目录os.lstat()。释放令人发疯的狗!

上述方法也有一个很大的附带好处:安全性。(是不是好的?)具体为:

前端应用程序通过简单地将这样的路径名传递给拒绝服务(DoS)攻击os.stat()os.lstat()容易受到拒绝的攻击,从而验证来自不受信任来源的任意路径名。恶意用户可能试图反复验证驻留在已知陈旧或缓慢的文件系统上的路径名(例如,NFS Samba共享);在这种情况下,盲目声明传入的路径名可能最终会因连接超时而失败,或者消耗的时间和资源要比您承受失业的能力弱。

上面的方法通过仅针对根文件系统的根目录验证路径名的路径组成部分来避免这种情况。(即使这是陈旧,缓慢或无法访问的,也比路径名验证要麻烦得多。)

丢失?大。让我们开始。(假定使用Python3。请参阅“ leycec对300的脆弱希望是什么?”)

import errno, os

# Sadly, Python fails to provide the following magic number for us.
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.

See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
    Official listing of all such codes.
'''

def is_pathname_valid(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname for the current OS;
    `False` otherwise.
    '''
    # If this pathname is either not a string or is but is empty, this pathname
    # is invalid.
    try:
        if not isinstance(pathname, str) or not pathname:
            return False

        # Strip this pathname's Windows-specific drive specifier (e.g., `C:\`)
        # if any. Since Windows prohibits path components from containing `:`
        # characters, failing to strip this `:`-suffixed prefix would
        # erroneously invalidate all valid absolute Windows pathnames.
        _, pathname = os.path.splitdrive(pathname)

        # Directory guaranteed to exist. If the current OS is Windows, this is
        # the drive to which Windows was installed (e.g., the "%HOMEDRIVE%"
        # environment variable); else, the typical root directory.
        root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
            if sys.platform == 'win32' else os.path.sep
        assert os.path.isdir(root_dirname)   # ...Murphy and her ironclad Law

        # Append a path separator to this directory if needed.
        root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep

        # Test whether each path component split from this pathname is valid or
        # not, ignoring non-existent and non-readable path components.
        for pathname_part in pathname.split(os.path.sep):
            try:
                os.lstat(root_dirname + pathname_part)
            # If an OS-specific exception is raised, its error code
            # indicates whether this pathname is valid or not. Unless this
            # is the case, this exception implies an ignorable kernel or
            # filesystem complaint (e.g., path not found or inaccessible).
            #
            # Only the following exceptions indicate invalid pathnames:
            #
            # * Instances of the Windows-specific "WindowsError" class
            #   defining the "winerror" attribute whose value is
            #   "ERROR_INVALID_NAME". Under Windows, "winerror" is more
            #   fine-grained and hence useful than the generic "errno"
            #   attribute. When a too-long pathname is passed, for example,
            #   "errno" is "ENOENT" (i.e., no such file or directory) rather
            #   than "ENAMETOOLONG" (i.e., file name too long).
            # * Instances of the cross-platform "OSError" class defining the
            #   generic "errno" attribute whose value is either:
            #   * Under most POSIX-compatible OSes, "ENAMETOOLONG".
            #   * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE".
            except OSError as exc:
                if hasattr(exc, 'winerror'):
                    if exc.winerror == ERROR_INVALID_NAME:
                        return False
                elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
                    return False
    # If a "TypeError" exception was raised, it almost certainly has the
    # error message "embedded NUL character" indicating an invalid pathname.
    except TypeError as exc:
        return False
    # If no exception was raised, all path components and hence this
    # pathname itself are valid. (Praise be to the curmudgeonly python.)
    else:
        return True
    # If any other exception was raised, this is an unrelated fatal issue
    # (e.g., a bug). Permit this exception to unwind the call stack.
    #
    # Did we mention this should be shipped with Python already?

做完了 不要斜视那个代码。(它咬。

问题2:路径名的存在或可创建性可能无效,是吗?

在上述解决方案的基础上,测试可能无效的路径名的存在或可创建性通常很简单。这里的关键是测试传递的路径之前调用先前定义的函数:

def is_path_creatable(pathname: str) -> bool:
    '''
    `True` if the current user has sufficient permissions to create the passed
    pathname; `False` otherwise.
    '''
    # Parent directory of the passed path. If empty, we substitute the current
    # working directory (CWD) instead.
    dirname = os.path.dirname(pathname) or os.getcwd()
    return os.access(dirname, os.W_OK)

def is_path_exists_or_creatable(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname for the current OS _and_
    either currently exists or is hypothetically creatable; `False` otherwise.

    This function is guaranteed to _never_ raise exceptions.
    '''
    try:
        # To prevent "os" module calls from raising undesirable exceptions on
        # invalid pathnames, is_pathname_valid() is explicitly called first.
        return is_pathname_valid(pathname) and (
            os.path.exists(pathname) or is_path_creatable(pathname))
    # Report failure on non-fatal filesystem complaints (e.g., connection
    # timeouts, permissions issues) implying this path to be inaccessible. All
    # other exceptions are unrelated fatal issues and should not be caught here.
    except OSError:
        return False

完成完成的。除了不太一样。

问题3:Windows上可能存在无效的路径名或可写性

有一个警告。当然有。

官方os.access()文件承认:

注意:即使os.access()表明I / O操作将成功,它也可能会失败,尤其是对于网络文件系统上的操作,其权限语义可能超出通常的POSIX权限位模型。

毫不奇怪,Windows通常是这里的嫌疑人。由于在NTFS文件系统上广泛使用了访问控制列表(ACL),因此简单的POSIX权限位模型无法很好地映射到底层Windows现实。尽管这(不是问题)不是Python的错,但对于与Windows兼容的应用程序,它可能仍然值得关注。

如果是您,那么需要一个更强大的替代方案。如果传递的路径也不会存在,我们不是试图建立保证该路径的父目录被立即删除临时文件- creatability的更便携的(如昂贵的)测试:

import os, tempfile

def is_path_sibling_creatable(pathname: str) -> bool:
    '''
    `True` if the current user has sufficient permissions to create **siblings**
    (i.e., arbitrary files in the parent directory) of the passed pathname;
    `False` otherwise.
    '''
    # Parent directory of the passed path. If empty, we substitute the current
    # working directory (CWD) instead.
    dirname = os.path.dirname(pathname) or os.getcwd()

    try:
        # For safety, explicitly close and hence delete this temporary file
        # immediately after creating it in the passed path's parent directory.
        with tempfile.TemporaryFile(dir=dirname): pass
        return True
    # While the exact type of exception raised by the above function depends on
    # the current version of the Python interpreter, all such types subclass the
    # following exception superclass.
    except EnvironmentError:
        return False

def is_path_exists_or_creatable_portable(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname on the current OS _and_
    either currently exists or is hypothetically creatable in a cross-platform
    manner optimized for POSIX-unfriendly filesystems; `False` otherwise.

    This function is guaranteed to _never_ raise exceptions.
    '''
    try:
        # To prevent "os" module calls from raising undesirable exceptions on
        # invalid pathnames, is_pathname_valid() is explicitly called first.
        return is_pathname_valid(pathname) and (
            os.path.exists(pathname) or is_path_sibling_creatable(pathname))
    # Report failure on non-fatal filesystem complaints (e.g., connection
    # timeouts, permissions issues) implying this path to be inaccessible. All
    # other exceptions are unrelated fatal issues and should not be caught here.
    except OSError:
        return False

但是请注意,即使可能还不够。

多亏了用户访问控制(UAC),永远无法模仿的Windows Vista及其所有后续迭代都明显涉及与系统目录有关的权限。当非管理员用户尝试在规范目录C:\WindowsC:\Windows\system32目录中创建文件时,UAC会从表面上允许用户这样做,同时实际上将所有创建的文件隔离到该用户配置文件中的“虚拟存储”中。(谁能想到欺骗用户会产生有害的长期后果?)

这太疯狂了。这是Windows。

证明给我看

敢吗 现在该进行上述测试了。

由于NULL是面向UNIX的文件系统上路径名中唯一禁止使用的字符,因此让我们利用它来展示冷酷的事实–忽略不可忽略的Windows恶作剧,坦白地说,这同样使我感到厌烦并激怒了我:

>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False

超越理智。超越痛苦。您会发现Python可移植性问题。


3
是的,是我!尝试将可交叉移植的路径名验证正则表达式合并在一起是徒劳的,并且保证在常见的边缘情况下失败。请考虑Windows上的路径名长度,例如:“最大路径为32,767个字符,因为系统可能会在运行时将'\\?\'前缀扩展为更长的字符串,并且这种扩展适用于总长度。” 鉴于此,构造仅匹配有效路径名的正则表达式实际上在技术上是不可行的。仅遵从Python更为合理。
Cecil Curry 2015年

2
啊。我(不情愿)看到了。您正在做的事情甚至比破解正则表达式还要奇怪。是的,这肯定会导致更大的失败。这也完全不能解决问题,不是“如何从Windows特定的基名中删除无效的子字符串?” (...由于遗漏情况,您自己无法解决,但又无法解决),但是“如何交叉移植测试路径名的有效性,以及对于有效的路径名,这些路径的存在或可写性?”
塞西尔·库里

1
特定于文件系统的约束绝对是一个有效的问题-但它有两种方式。对于使用来自不受信任来源的任意路径名的前端应用程序,盲目执行读取充其量是个好主意。在这种情况下,强制使用根文件系统不仅明智而且谨慎。但是,对于其他应用程序,用户库可能具有足够的信任度,可以授予不受禁止的文件系统访问权限。我会说,这完全取决于上下文。感谢您精明地指出这一点,没人!我将在上面添加一个警告。
Cecil Curry 2015年

2
至于命名法,我是将测试人员姓名加前缀的老兄is_。这是我的性格缺陷。尽管如此,请注意:您不能取悦所有人,有时您也不能取悦所有人。;)
Cecil Curry 2015年

1
在Fedora 24的python 3.5.3上,抛出带有嵌入式空字符的路径名:ValueError:嵌入式空字节…需要添加:```ValueError除外,例如exc:在TypeError陷阱之前或之后返回False`。
mMerlin

47
if os.path.exists(filePath):
    #the file is there
elif os.access(os.path.dirname(filePath), os.W_OK):
    #the file does not exists but write privileges are given
else:
    #can not write there

请注意,path.exists失败的原因the file is not there可能不仅仅是,所以您可能必须进行更精细的测试,例如测试包含目录是否存在等等。


在与OP讨论之后,事实证明,主要的问题似乎是文件名可能包含文件系统不允许的字符。当然,需要将它们删除,但是OP希望在文件系统允许的范围内保持尽可能多的人可读性。

可悲的是,我不知道有什么好的解决方案。但是,塞西尔·库里(Cecil Curry)的答案更仔细地研究了发现问题。


否。如果路径中的文件存在或可以创建,我需要返回true 。如果路径无效(由于在Windows上包含无效字符),则需要返回false。
假名称

or can be created好吧,我没有从你的问题中读到那句话。读取权限在某种程度上取决于平台。
没人在

1
@Fake名称:是的,它将删除一些平台依赖性,但是仍然有些平台提供了其他平台所不具有的功能,并且没有简单的方法可以将它们包装起来。我更新了答案,在那里看看。
没人在

1
我不知道为什么这个答案被赞成。它与解决核心问题并不遥不可及-简洁地说,是:“请验证路径名?” 验证路径权限是此处的一个辅助(且在很大程度上可忽略)问题。尽管从os.path.exists(filePath)技术上来说,确实会针对无效路径名引发异常,但仍需要明确捕获这些异常并将其与其他不相关的异常区分开。此外,同样的调用返回False现有路径当前用户并不会具有读取权限。简而言之,不好。
塞西尔·库里

1
@CecilCurry:回答您的问题:查看问题的编辑历史记录。像大多数问题一样,一开始并没有那么明确,甚至现在,标题的措词可能会比您说的还要理解。
没人在

9

使用Python 3,如何:

try:
    with open(filename, 'x') as tempfile: # OSError if file exists or is invalid
        pass
except OSError:
    # handle error here

使用“ x”选项,我们也不必担心比赛条件。请参阅此处的文档。

现在,如果该文件尚不存在,它将创建一个寿命很短的临时文件-除非名称无效。如果您可以忍受,那么可以简化很多事情。


2
在这一点上,需要这样做的项目已经远远超出了答案甚至是相关的程度,以至于我真的不能接受答案。
假名称

具有讽刺意味的是,实际答案还不够好。无论如何,我想您都可以查看该文件是否存在。如果是这样,请尝试将文件复制到其他位置,然后尝试覆盖。
马特

5
open(filename,'r')   #2nd argument is r and not w

将打开文件或给出错误(如果不存在)。如果有错误,那么您可以尝试写入路径,如果不能,则出现第二个错误

try:
    open(filename,'r')
    return True
except IOError:
    try:
        open(filename, 'w')
        return True
    except IOError:
        return False

也可以在这里查看有关Windows权限的信息


1
为避免需要显式取消对测试文件的链接,可以使用tempfile.TemporaryFile()它在超出范围时自动销毁临时文件。
D_Bye 2012年

@FakeName代码是不同的,我本可以在第二部分使用os.access,但是如果您按照我给的链接进行操作,将会发现它不是一个好主意,这使您可以选择实际打开写作的道路。
vikki 2012年

我正在使用构建路径os.path.join,因此没有转义问题。此外,我实际上没有目录权限问题。我遇到目录(和文件名)名称问题。
假名称

在这种情况下,@FakeName只需要尝试打开它(不需要编写),如果filename包含无效字符,python会给出错误。我已经编辑了答案
vikki 2012年

1
@HelgaIliashenko进行写入操作将覆盖一个现有文件(将其清空),即使您在不进行写入的情况下立即将其关闭。这就是为什么我要开始阅读的原因,因为那样的话,如果您没有收到错误,则说明您已经存在一个文件。
维基

-7

尝试os.path.exists此操作将检查路径,并返回True是否存在(False如果不存在)。


1
否。如果路径中的文件存在或可以创建,我需要返回true 。如果路径无效(由于在Windows上包含无效字符),则需要返回false。
假名称

哪种无效字符?
Nilesh 2012年

Dunno-特定于平台。
假名称

2
实际上,特定于文件系统。
Piotr Kalinowski
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.