先看两行 Python。fs 是 fsspec 的 HTTP 文件系统,url 指向一个能正常下载的小文件。当前目录已存在且可以写入,result.bin 还不存在。

python
fs.get(url, "result.bin")
fs.get([url], ["result.bin"])

你觉得它们会有什么区别?第二行只是把来源和目标都放进列表,按理说仍然是下载一个文件。

在这次修复之前,第一行能完成,第二行却抛出 FileNotFoundError。服务器甚至没有收到请求。失败发生得比下载更早:程序试图创建一个名字为空的目录。

先猜,再点开路径

试着把目标换成 nested/result.bin,或者一个绝对路径,再看它们会走到哪里。

下面的演示把工作目录设为 C:/lesson,只处理普通文件路径,不考虑通配符、递归和尾斜杠。它只模拟路径处理流程,不访问你的文件系统,也不执行真实下载。

演示也展示嵌套、绝对路径的标量分支,方便比较流程;这两种标量形式没有列入本次八项实测,实测范围见后面的表格。

给文件名套个列表,会发生什么?

教学模拟 · 工作目录 C:/lesson 已存在,父目录允许创建。只展示普通文件的路径处理,不访问文件或网络。

源和目标同时传列表:走快捷分支,保留目标路径原样。

传入的目标
["result.bin"]
进入目录处理时的单个目标
result.bin
dirname 得到的父目录
""(空字符串)

原版

os.makedirs("", exist_ok=True) → 空路径失败,流程停止。

修复版

父目录为空 → 跳过 os.makedirs,流程可继续(未执行下载)。

对应的 Python 调用形式

# 假设 fs 是已配置好的 HTTPFileSystem;此处只展示参数形式。
# example.invalid 是示例地址,不用于实际下载。
source = "https://example.invalid/payload"
fs.get([source], ["result.bin"])

教学模拟:默认展示列表裸名。原版尝试创建空目录而失败,修复版跳过这一步,流程可继续。启用 JavaScript 后可切换其余组合。

列表省掉了一步,后面却还在等它

下载实现为两个列表准备了一条快捷分支:既然调用者已经逐项指定来源和目标,就直接使用这些路径。标量输入走另一条分支,会先做路径规范化和目标推导。

两个分支最后汇合到同一段准备工作:取出每个目标的父目录,确保它存在,然后开始下载。问题就在这儿。nested/result.bin 有父目录 nested,而裸文件名没有:

python
os.path.dirname("result.bin")  # ""

空字符串被交给 os.makedirs,于是报错。标量分支前面已经处理过路径,列表分支保留了裸文件名。看起来相同的用户意图,到了公共逻辑这里,路径的形态已经不同。合入前源码

换成 Path("result.bin") 也不会绕过这个问题。这里调用的是 os.path.dirname,得到的仍然是空字符串;它和 Path("result.bin").parent 返回 . 是两回事。

修复里还有一次小讨论

我最初用了当前目录作为空父目录的替代值。维护者指出,当前目录本来就不需要创建。这也让修复更准确了:父目录非空时才创建,裸文件名直接写入已有工作目录。

python
for lp in lpaths:
    if parent := os.path.dirname(lp):
        os.makedirs(parent, exist_ok=True)

这段是上游最终接受的实现。回归测试同时检查:裸文件名能下载,程序也没有偷偷去创建空路径或 .。我以 adenzhou1350 提交的 PR #2376 由 martindurant 于 2026 年 10 月 27 日合入。

修好的同时,正常输入也要能用

只看到报错消失还不够。如果修复把创建父目录的动作全部删掉,裸文件名可能好了,嵌套目录却会坏掉。所以我在 10 月 29 日用公开的相邻两个提交重新跑了这些形式:

目标形式,每种含 str 和 Path修复前修复后
直接传相对文件名2 项成功2 项成功
相对文件名列表2 项失败,请求数均为 02 项成功
嵌套相对路径列表2 项成功2 项成功
绝对路径列表2 项成功2 项成功

成功项都检查了完整文件字节,同步 MemoryFileSystem 的列表下载也作为参考跑过。这里检查的是同一个问题的不同输入;没有测吞吐,也没有覆盖全部云存储后端。

在自己电脑上重放

下载回放包,解压后在目录里运行:

powershell
python -B -X utf8 run_case.py

本次验证环境是 Windows、Python 3.14.3 和 aiohttp 3.13.5。包内 README 有独立虚拟环境的准备命令。首次运行会下载两个固定提交的完整公开源码,并核对哈希;测试通过本机环回 HTTP 提供文件,不需要云账号。缓存齐备后可以加 --offline 重跑。

脚本保留每种输入的异常、请求数和字节校验结果。你也可以先看这次的结果 JSON。遇到类似的“单个能用,批量失败”,先把两个入口走过的路径画出来,再给正常输入留几个对照,往往比盯着最后一行异常更容易找到问题。

本文使用 AI 工具辅助整理。实验设置和测试结果见正文。

相关原始记录 ↗更多 PR 修复记录 →讨论这个问题 ↗