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

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

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

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

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

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

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

因此:

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

from 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上运行它,因此我想避免使用特定于平台的东西。

``

参考方案

tl; dr

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

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

两个问题的故事

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

vikki的answer可能最接近,但具有以下明显的缺点:

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

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

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

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

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

  • 在与POSIX兼容的系统上,文件系统已安装到根目录(/)。
  • 在Windows上,文件系统挂载到%HOMEDRIVE%上,该文件系统包含当前Windows安装(通常但不一定是C:),后缀以冒号开头。
  • 反过来,“语法正确性”的含义取决于根文件系统的类型。对于ext4(以及大多数但并非所有POSIX兼容的)文件系统,路径名称在且仅当该路径名称在语法上是正确的:

  • 不包含任何空字节(即Python中的\x00)。这是所有POSIX兼容文件系统的硬性要求。
  • 不包含超过255个字节的路径组件(例如,Python中的'a'*256)。路径组成部分是路径名的最长子字符串,其中不包含/字符(例如,路径名bergtatt中的indifjeldkamrene/bergtatt/ind/i/fjeldkamrene)。
  • 句法正确性。根文件系统。而已。

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

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

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

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

  • 对于驻留在不存在的目录中的路径名,为FileNotFoundError的实例。
  • 对于现有目录中的路径名:
  • 在Windows中,WindowsError属性为winerror(即123)的ERROR_INVALID_NAME实例。
  • 在所有其他操作系统下:
  • 对于包含空字节的路径名(即'\x00'),为TypeError的实例。
  • 对于包含超过255个字节的路径组成部分的路径名,OSError实例的errcode属性为:
  • 在SunOS和* BSD操作系统家族下errno.ERANGE。 (这似乎是操作系统级别的错误,否则称为POSIX标准的“选择性解释”。)
  • 在所有其他操作系统下,errno.ENAMETOOLONG
  • 至关重要的是,这意味着存在于现有目录中的仅路径名是有效的。 当传递的路径名驻留在不存在的目录中时,不管这些路径名是否无效,os.stat()os.lstat()函数都会引发通用FileNotFoundError异常。目录存在优先于路径名无效。

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

    要回答此问题,请从上面回想一下,ext4文件系统上语法正确的路径名不包含长度为255个字节的路径组件(A)或空字符串或(B)。因此,且仅当该路径名中的所有路径组件均有效时,ext4路径名才有效。大多数感兴趣的real-world filesystems都是如此。

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

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

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

    上述方法也有很大的好处:安全。 (不好吗?)特别是:

    通过将简单的路径名传递给os.stat()os.lstat()来验证来自不受信任来源的任意路径名的前端应用程序很容易遭到拒绝服务(DoS)攻击和其他黑帽子恶作剧。恶意用户可能会尝试反复验证驻留在已知陈旧或速度较慢的文件系统上的路径名(例如,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() documentation承认:

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

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

    如果是您,那么需要一个更强大的替代方案。如果传递的路径不存在,我们将尝试创建一个保证在该路径的父目录中立即删除的临时文件,这是对可创建性的更便携式(如果比较昂贵)的测试:

    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及其随后的所有后续迭代blatantly lie有关与系统目录有关的权限。当非管理员用户尝试在规范的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可移植性问题。

    为什么使用'=='或'is'比较字符串有时会产生不同的结果? - python

    我有一个Python程序,其中将两个变量设置为'public'值。在条件表达式中,我有比较var1 is var2失败,但如果将其更改为var1 == var2,它将返回True。现在,如果我打开Python解释器并进行相同的“是”比较,则此操作成功。>>> s1 = 'public' >>…

    单行的'if'/'for'语句是否使用Python样式好? - python

    我经常在这里看到某人的代码,看起来像是“单线”,这是一条单行语句,以传统的“if”语句或“for”循环的标准方式执行。我在Google周围搜索,无法真正找到可以执行的搜索类型?任何人都可以提出建议并最好举一些例子吗?例如,我可以一行执行此操作吗?example = "example" if "exam" in exam…

    在返回'Response'(Python)中传递多个参数 - python

    我在Angular工作,正在使用Http请求和响应。是否可以在“响应”中发送多个参数。角度文件:this.http.get("api/agent/applicationaware").subscribe((data:any)... python文件:def get(request): ... return Response(seriali…

    您如何在列表内部调用一个字符串位置? - python

    我一直在做迷宫游戏。我首先决定制作一个迷你教程。游戏开发才刚刚开始,现在我正在尝试使其向上发展。我正在尝试更改PlayerAre变量,但是它不起作用。我试过放在列表内和列表外。maze = ["o","*","*","*","*","*",…

    用大写字母拆分字符串,但忽略AAA Python Regex - python

    我的正则表达式:vendor = "MyNameIsJoe. I'mWorkerInAAAinc." ven = re.split(r'(?<=[a-z])[A-Z]|[A-Z](?=[a-z])', vendor) 以大写字母分割字符串,例如:'我的名字是乔。 I'mWorkerInAAAinc”变成…