检查Python中的path是否有效,而不在path的目标处创build文件

我有一个path(包括目录和文件名)。
我需要testing一下,如果文件名是有效的,例如,如果文件系统将允许我创build一个这样的名称的文件。
文件名有一些unicode字符

假设path的目录段是有效的和可访问的( 我试图让这个问题更适用于某种程度,显然我太过于自信了 )是安全的。

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

我会发布一些我正在处理的示例字符,但显然它们会被堆栈交换系统自动删除。 无论如何,我想保持标准的统一码实体像ö ,只能转义在文件名无效的东西。


这是抓住。 可能(或可能不)已经成为path目标上的文件。 如果它存在,我需要保留该文件,如果不存在,则不要创build文件。

基本上我想检查是否可以写入path, 而不打开写入path (以及通常需要的自动文件创build/文件破坏)。

因此:

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

从这里

是不可接受的,因为它会覆盖现有的文件,我不想触摸(如果有的话),或者如果不存在的话创build文件。

我知道我可以这样做:

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

但是,这将创build filePath文件,然后我将不得不os.unlink

最后,似乎花了6或7行来做一些简单的操作,比如os.isvalidpath(filePath)或类似的操作。


顺便说一句,我需要这个(至less)Windows和MacOS上运行,所以我想避免平台特定的东西。

TL;博士

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

严格Python 3.这就是我们如何滚动。

两个问题的故事

“我如何testingpath名的有效性,对于有效的path名,这些path的存在性还是可写性? 显然是两个不同的问题。 两者都很有趣,在这里也没有得到一个真正令人满意的答案……或者,我可以在任何地方都可以。

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

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

我们要解决这一切。

问题#0:什么是path名的有效性呢?

在将我们脆弱的肉套装扔进python般的痛苦之中之前,我们应该定义“path名称有效性”的含义。 究竟是什么定义了有效性?

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

“根文件系统”是指:

  • 在POSIX兼容系统上,挂载到根目录( / )的文件系统。
  • 在Windows上,挂载到%HOMEDRIVE%的文件系统是包含当前Windows安装(通常但不一定是C: %HOMEDRIVE%的冒号后缀驱动器号。

“语法正确性”的含义又取决于根文件系统的types。 对于ext4 (以及大多数但不是全部的POSIX兼容)文件系统,当且仅当该path名为path名时,path名在语法上是正确的:

  • 不包含空字节(即Python中的\x00 )。 对于所有POSIX兼容的文件系统来说这是一个很难的要求。
  • 不包含长度超过255个字节的path组件(例如,Python中的'a'*256 )。 path组件是包含无/字符(例如, bergtattindi ,和path名/bergtatt/ind/i/fjeldkamrene )的path名的最长子string。

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

问题1:我们现在应该如何做path名的有效性?

在Python中validationpath名是非常不直观的。 我在这里与假名称一致:官方os.path包应该提供一个开箱即用的解决scheme。 对于未知的(可能不是很成功的)原因,事实并非如此。 幸运的是,展开你自己的临时解决scheme并不是那种让人痛苦的事情

好的,实际上是。 它毛茸茸的; 这是讨厌的; 它可能会因为它发光而发出咔嚓声和咯咯笑声。 但是你会怎么做? Nuthin'。

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

  • 对于驻留在不存在的目录中的path名, FileNotFoundError实例。
  • 对于驻留在现有目录中的path名:
    • 在Windows下,其winerror属性为123 (即ERROR_INVALID_NAME )的WindowsError实例。
    • 在所有其他操作系统下:
    • 对于包含空字节的path名(例如'\x00' ), TypeError实例。
    • 对于包含长于255字节的path组件的path名,其errcode属性为的OSError实例:
      • 在SunOS和* BSD系列的操作系统下, errno.ERANGE 。 (这似乎是一个操作系统级别的错误,或者被称为POSIX标准的“select性解释”。)
      • 在所有其他操作系统下, errno.ENAMETOOLONG

至关重要的是,这意味着只有驻留在现有目录中的path名是可validation的。 当传递驻留在不存在目录中的path名时, os.stat()os.lstat()函数会引发通用的FileNotFoundErrorexception,无论这些path名是否无效。 目录存在优先于path名无效。

这是否意味着驻留在不存在的目录中的path名是不可validation的? 是的 – 除非我们修改这些path名驻留在现有的目录中。 那是否安全可行呢? 不应该修改path名阻止我们validation原始path名?

为了回答这个问题,从上面回想一下, ext4文件系统上的语法上正确的path名不包含包含空字节的path组件(A)或长度超过255个字节的(B) 。 因此,当且仅当该path名中的所有path组件都有效时, ext4path名才有效。 大多数 真实世界的文件系统都是如此。

这种迂腐的见解是否真的帮助我们? 是。 它将一次性validation完整path名称的较大问题减less到只validation该path名中所有path组件的较小问题。 通过遵循以下algorithm,跨平台方式可以validation任意path名(无论该path名是否驻留在现有目录中):

  1. 将path名分解成path组件(例如path名/troldskog/faren/vild到列表['', 'troldskog', 'faren', 'vild'] )。
  2. 对于每个这样的组件:
    1. 将保证存在该组件的目录的path名join新的临时path名(例如/troldskog )。
    2. 将该path名传递给os.stat()os.lstat() 。 如果这个path名和这个组件是无效的,这个调用保证会引发一个暴露无效types的exception,而不是普通的FileNotFoundErrorexception。 为什么? 因为该path名驻留在现有的目录中。 (循环逻辑是循环的。)

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

将位于任何其他目录(因此不能保证存在)的path名传递给os.stat()os.lstat()竞争条件,即使该目录之前已经被testing过。 为什么? 因为外部进程testing执行之后 ,但是将path名传递给os.stat()os.lstat() 之前 ,无法防止同时移除该目录。 释放心灵狡猾的狗!

上述方法还有一个重要的副作用: 安全性。 (这不是很好吗?)具体来说:

前置应用程序通过简单地将这些path名传递给os.stat()os.lstat()来validation来自不受信任来源的任意path名,这些应用程序很容易受到拒绝服务(DoS)攻击和其他黑帽os.lstat()攻击。 恶意用户可能会尝试重复validation驻留在已知为陈旧或其他缓慢的文件系统上的path名(例如,NFS Samba共享)。 在这种情况下,盲目地统计input的path名称最终可能会因连接超时而失败,或者比您的失败能力消耗更多的时间和资源。

上述方法通过仅对根文件系统的根目录validationpath名的path组件来避免这种情况。 (如果即使陈旧,缓慢或无法访问,你的问题比path名validation更大。)

丢失? 大。 让我们开始。 (Python 3假设,请参阅“什么是易碎的希望300, leycec ?”)

 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://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx 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 (eg, `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 (eg, 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 (eg, 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" (ie, no such file or directory) rather # than "ENAMETOOLONG" (ie, 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 (eg, 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 # (eg, a bug). Permit this exception to unwind the call stack. # # Did we mention this should be shipped with Python already? 

完成。 不要眯起眼睛看那个代码。 ( 它咬人

问题2:可能无效的path名存在或创造力,呃?

考虑到上面的解决scheme,testing可能无效的path名称的存在性或可创build性通常是微不足道的。 这里的小键是testing传递的path之前调用之前定义的函数:

 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 (eg, 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上可能无效的path名存在或可写性

有一个警告。 当然有。

正如官方的os.access()文档所承认的那样:

注意:即使在os.access()指示它们会成功时,I / O操作也可能失败,特别是对于可能具有超出通常的POSIX权限位模型的权限语义的networking文件系统上的操作。

没有人惊讶,Windows是这里通常的嫌疑犯。 由于在NTFS文件系统上广泛使用了访问控制列表(ACL),简化的POSIX权限位模型很难映射到基本的Windows实际。 虽然(可以说)不是Python的错,但它可能是Windows兼容应用程序的关注点。

如果这是你,一个更强大的select是想要的。 如果传递的path不存在,我们试图创build一个临时文件,保证立即删除该path的父目录 – 一个更便携(如果昂贵)的创造力testing:

 import os, tempfile def is_path_sibling_creatable(pathname: str) -> bool: ''' `True` if the current user has sufficient permissions to create **siblings** (ie, 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 (eg, 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及其后续的迭代公然谎言系统目录的权限。 当非pipe理员用户试图在规范的C:\WindowsC:\Windows\system32目录中创build文件时,UAC在表面上允许用户这样做,而实际上将所有创build的文件隔离到该用户configuration文件中的“虚拟商店” 。 (谁能想象欺骗用户会产生有害的长期后果?)

这太疯狂了。 这是Windows。

certificate给我看

我们敢吗? 是时候对上述testing进行testing了。

由于NULL是面向UNIX的文件系统中唯一禁止使用path名称的字符,因此让我们利用这一点来展示冷酷,难以理解的事实 – 忽略不可忽视的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的可移植性问题。

 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所以如果包含的目录存在等,您可能必须执行更精细的testing,如testing。


在我和OP讨论后发现,主要的问题似乎是,文件名可能包含文件系统不允许的字符。 当然,他们需要被删除,但是操作系统想要保持与文件系统一样多的可读性。

可惜我不知道有什么好的解决办法。 然而塞西尔·库里的回答更仔细地检查了这个问题。

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

将会打开文件,如果不存在则提示错误。 如果有错误,那么你可以尝试写入path,如果你不能,那么你会得到第二个错误

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

在这里也看看有关Windows的权限

尝试os.path.exists这将检查path,如果存在则返回True否则返回False