先看两行 Python。fs 是 fsspec 的 HTTP 文件系统,url 指向一个能正常下载的小文件。当前目录已存在且可以写入,result.bin 还不存在。
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,而裸文件名没有:
os.path.dirname("result.bin") # ""空字符串被交给 os.makedirs,于是报错。标量分支前面已经处理过路径,列表分支保留了裸文件名。看起来相同的用户意图,到了公共逻辑这里,路径的形态已经不同。合入前源码
换成 Path("result.bin") 也不会绕过这个问题。这里调用的是 os.path.dirname,得到的仍然是空字符串;它和 Path("result.bin").parent 返回 . 是两回事。
修复里还有一次小讨论
我最初用了当前目录作为空父目录的替代值。维护者指出,当前目录本来就不需要创建。这也让修复更准确了:父目录非空时才创建,裸文件名直接写入已有工作目录。
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 项失败,请求数均为 0 | 2 项成功 |
| 嵌套相对路径列表 | 2 项成功 | 2 项成功 |
| 绝对路径列表 | 2 项成功 | 2 项成功 |
成功项都检查了完整文件字节,同步 MemoryFileSystem 的列表下载也作为参考跑过。这里检查的是同一个问题的不同输入;没有测吞吐,也没有覆盖全部云存储后端。
在自己电脑上重放
下载回放包,解压后在目录里运行:
python -B -X utf8 run_case.py本次验证环境是 Windows、Python 3.14.3 和 aiohttp 3.13.5。包内 README 有独立虚拟环境的准备命令。首次运行会下载两个固定提交的完整公开源码,并核对哈希;测试通过本机环回 HTTP 提供文件,不需要云账号。缓存齐备后可以加 --offline 重跑。
脚本保留每种输入的异常、请求数和字节校验结果。你也可以先看这次的结果 JSON。遇到类似的“单个能用,批量失败”,先把两个入口走过的路径画出来,再给正常输入留几个对照,往往比盯着最后一行异常更容易找到问题。