在 Python 中检查路径是否有效,而不需要在路径的目标处创建文件

我有一个路径(包括目录和文件名)。
我需要测试文件名是否有效,例如,文件系统是否允许我创建具有这样一个名称的文件。
其中的文件名 有一些 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 上运行它,所以我希望避免特定于平台的东西。

``

139559 次浏览

尝试 os.path.exists这将检查路径,如果存在返回 True,如果不存在返回 False

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 的回答对检测问题进行了更仔细的研究。

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

还可以查看有关窗口权限的 给你

博士

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

严格遵守 Python 3我们就是这么做的。

两个问题的故事

“如何测试路径名的有效性,以及对于有效的路径名,这些路径的存在性或可写性?”显然是两个不同的问题。两者都很有趣,而且都没有得到一个真正令人满意的答案... ... 或者,好吧,任何地方,我可以抓住。

维基的 回答可能是最接近的,但它有一些显著的缺点:

  • 不必要地打开(然后又无法可靠地关闭)文件句柄。
  • 不必要地写入(然后不能可靠地关闭或删除)0字节的文件。
  • 忽略操作系统特有的错误,区分不可忽略的无效路径名和可忽略的文件系统问题
  • 忽略由于外部进程同时(重新)移动要测试的路径名的父目录而导致的竞态条件
  • 忽略驻留在陈旧、缓慢或其他临时无法访问的文件系统上的路径名导致的连接超时。这个 可以使面向公众的服务暴露于潜在的 拒绝驱动的攻击。(请看下面。)

我们会解决这一切的。

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

在把我们脆弱的肉身扔进满是蟒蛇的痛苦泥坑之前,我们可能应该定义我们所说的“路径名有效性”的含义到底是什么定义了有效性?

通过“路径名有效性”,我们指的是路径名相对于当前系统的 根文件系统根文件系统句法正确性-不管该路径或其父目录是否实际存在。根据这个定义,如果路径名符合根文件系统的所有语法要求,那么它在语法上是正确的。

“根文件系统”的意思是:

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

“语法正确性”的含义依次取决于根文件系统的类型。对于 ext4(以及除 没有以外的大多数与 POSIX 兼容的)文件系统,路径名在语法上是正确的,当且仅当该路径名:

  • 不包含空字节(即 Python 中的 \x00)
  • 不包含超过255字节的路径组件(例如,Python 中的 'a'*256)。路径组件是路径名中不包含 /字符的最长子字符串(例如,路径名 /bergtatt/ind/i/fjeldkamrene中的 bergtattindifjeldkamrene)。

语法正确,根文件系统,就是这样。

问题 # 1: 现在我们如何做路径名有效性?

在 Python 中验证路径名是非常不直观的。我在这里与 假名达成了一致: 官方的 os.path软件包应该为此提供一个开箱即用的解决方案。由于未知(或许不那么令人信服)的原因,它没有。幸运的是,展开您自己的特别解决方案并不是令人痛苦的 那个..。

好吧,确实是。它毛茸茸的,令人讨厌的,它可能会在发光的时候咯咯地笑,发光的时候咯咯地笑。但你能怎么办呢?什么都没有。

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

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

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

这是否意味着位于不存在的目录中的路径名是可验证的?是的——除非我们修改那些路径名以驻留在现有的目录中。然而,这种做法安全可行吗?修改路径名不应该阻止我们验证原始的路径名吗?

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

这种迂腐的见解真的对我们有帮助吗?是的。它将一下子验证完整路径名的较大问题降低为只验证该路径名中的所有路径组件的较小问题。任何任意路径名都可以通过以下算法以跨平台的方式进行验证(不管该路径名是否位于现有目录中) :

  1. 将路径名拆分为路径组件(例如,将路径名 /troldskog/faren/vild拆分为列表 ['', 'troldskog', 'faren', 'vild'])。
  2. 对于每个这样的组成部分:
    1. 将与该组件一起保证存在的目录的路径名连接到一个新的临时路径名(例如,/troldskog)。
    2. 将路径名传递给 os.stat()os.lstat()。如果该路径名和因此该组件无效,则此调用肯定会引发一个暴露无效类型的异常,而不是泛型 FileNotFoundError异常。为什么?循环逻辑是循环的

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

将驻留在任何其他目录中的路径名(因此不能保证存在)传递给 os.stat()os.lstat()会引发竞态条件,即使该目录之前已经被测试为存在。为什么?因为不能阻止外部进程同时删除已经执行测试的目录 之后,而是将路径名 之前传递给 os.stat()os.lstat()。释放狗的精神-口交精神错乱!

上述方法还有一个实质性的好处: 保安。(那个不是很好吗?)具体来说:

通过简单地将任意路径名传递给 os.stat()os.lstat()来验证来自不可信来源的任意路径名的前端应用程序很容易受到分布式拒绝服务攻击(doS)攻击和其他黑帽子诡计的影响。恶意用户可能会尝试重复验证文件系统上的路径名,这些路径名已经过时或者速度慢(例如,NFS Samba 共享) ; 在这种情况下,盲目地声明传入路径名可能最终会因连接超时而失败,或者消耗的时间和资源超过您承受失业的能力。

上述方法通过仅根据根文件系统的根目录验证路径名的路径组件来避免这种情况。(如果甚至 那是陈旧、缓慢或无法访问,那么您将面临比路径名验证更大的问题。)

遗失? 很好。让我们开始吧。(假设是 Python 3,参见“ What Is Fragile Hope for 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://learn.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()文档所承认的:

注意: I/O 操作可能会失败,即使 os.access()表明它们会成功,特别是对于网络文件系统上的操作,这些操作的权限语义可能超出通常的 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 和所有随后的关于系统目录权限的 公然撒谎迭代。当非管理员用户试图在规范的 C:\WindowsC:\Windows\system32目录中创建文件时,UAC 表面上允许用户这样做,而 事实上将所有创建的文件隔离到该用户配置文件中的“虚拟存储”中。(谁能想到欺骗用户会产生有害的长期后果?)

这太疯狂了,这是温杜斯。

证明给我看

我们敢吗? 现在是测试以上测试的时候了。

由于在面向 UNIX 的文件系统中,NULL 是路径名中唯一被禁止的字符,所以让我们利用它来演示这个冷酷无情的事实——忽略不可忽视的 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 的可移植性问题。

使用 Python 3,如何:

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

使用“ x”选项,我们也不必担心竞态条件。

现在,这将创建一个非常短命的临时文件,如果它不存在已经-除非名称是无效的。如果你能接受这一点,事情就简单多了。

我找到了一个名为 pathvalidate的 PyPI 模块。

pip install pathvalidate

它内部有一个名为 sanitize_filepath的函数,该函数将获取一个文件路径并将其转换为一个有效的文件路径:

from pathvalidate import sanitize_filepath
file1 = "ap:lle/fi:le"
print(sanitize_filepath(file1))
# Output: "apple/file"

它也可以处理保留名。如果你给它提供文件路径 con,它将返回 con_

有了这些知识,我们就可以检查输入的文件路径是否与经过消毒的路径相等,这意味着文件路径是有效的。

import os
from pathvalidate import sanitize_filepath


def check(filePath):
if os.path.exists(filePath):
return True
if filePath == sanitize_filepath(filePath):
return True
return False