检查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组件是包含无/
字符(例如,bergtatt
,ind
,i
,和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
。
- 在SunOS和* BSD系列的操作系统下,
- 在Windows下,其
至关重要的是,这意味着只有驻留在现有目录中的path名是可validation的。 当传递驻留在不存在目录中的path名时, os.stat()
和os.lstat()
函数会引发通用的FileNotFoundError
exception,无论这些path名是否无效。 目录存在优先于path名无效。
这是否意味着驻留在不存在的目录中的path名是不可validation的? 是的 – 除非我们修改这些path名驻留在现有的目录中。 那是否安全可行呢? 不应该修改path名阻止我们validation原始path名?
为了回答这个问题,从上面回想一下, ext4
文件系统上的语法上正确的path名不包含包含空字节的path组件(A)或长度超过255个字节的(B) 。 因此,当且仅当该path名中的所有path组件都有效时, ext4
path名才有效。 大多数 真实世界的文件系统都是如此。
这种迂腐的见解是否真的帮助我们? 是。 它将一次性validation完整path名称的较大问题减less到只validation该path名中所有path组件的较小问题。 通过遵循以下algorithm,跨平台方式可以validation任意path名(无论该path名是否驻留在现有目录中):
- 将path名分解成path组件(例如path名
/troldskog/faren/vild
到列表['', 'troldskog', 'faren', 'vild']
)。 - 对于每个这样的组件:
- 将保证存在该组件的目录的path名join新的临时path名(例如
/troldskog
)。 - 将该path名传递给
os.stat()
或os.lstat()
。 如果这个path名和这个组件是无效的,这个调用保证会引发一个暴露无效types的exception,而不是普通的FileNotFoundError
exception。 为什么? 因为该path名驻留在现有的目录中。 (循环逻辑是循环的。)
- 将保证存在该组件的目录的path名join新的临时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:\Windows
或C:\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
。