不能直接用asyncio+ftplib,因ftplib是同步阻塞实现,会卡住事件循环;需用原生异步库aioftp,注意主动模式、编码设置、并发用asyncio.gather、下载须流式处理防OOM。

为什么不能直接用 asyncio + ftplib
因为 ftplib 是纯同步阻塞实现,所有方法(如 retrbinary、storbinary)都会卡住 event loop。强行丢进 loop.run_in_executor 虽能“伪异步”,但无法真正并发多个 FTP 任务,还容易在连接复用、被动模式端口协商等环节出错。真正要并发上传/下载几十个文件,必须用原生支持 asyncio 的库——aioftp 是目前最稳定的选择。
安装与基础连接:注意 passive mode 和编码问题
aioftp 默认启用 passive mode,但某些 FTP 服务器(尤其是企业内网 NAT 后的)会拒绝被动端口回调。遇到 ConnectionRefusedError 或超时,先加 conn_type="active" 尝试:
import aioftp
<p>client = aioftp.Client()
await client.connect("ftp.example.com", port=21)
await client.login("user", "pass", conn_type="active") # 关键
另外,很多旧 FTP 服务器用 gbk 或 latin-1 编码文件名,而 aioftp 默认用 utf-8。若 list() 返回乱码或抛 UnicodeDecodeError,需显式指定编码:
client = aioftp.Client(encoding="gbk")
并发上传多个文件:用 asyncio.gather 而非 for 循环
逐个 await client.upload(...) 是串行的,起不到异步效果。必须把协程对象收集起来,统一 await:
立即学习“Python免费学习笔记(深入)”;
- 上传单个文件:用
client.upload(path, file_obj),其中file_obj必须是支持async read()的对象(如aiofiles.open(..., "rb")返回的 async file) - 并发上传:用
asyncio.gather(*[client.upload(dst, aiofiles.open(src, "rb")) for src, dst in files]) - 别忘了异常处理——某个文件失败不会中断其他上传,但你要主动检查返回结果或捕获
aioftp.StatusCodeError
示例片段:
import asyncio
import aioftp
import aiofiles
<p>async def upload_one(client, local_path, remote_path):
try:
async with aiofiles.open(local_path, "rb") as f:
await client.upload(remote_path, f)
print(f"✓ {local_path} → {remote_path}")
except Exception as e:
print(f"✗ {local_path}: {e}")</p><p>async def main():
async with aioftp.ClientSession("ftp.example.com", "u", "p") as client:
await asyncio.gather(
upload_one(client, "a.txt", "/pub/a.txt"),
upload_one(client, "b.log", "/pub/b.log"),
upload_one(client, "c.zip", "/pub/c.zip"),
)
下载大文件时内存爆满?必须流式读写
client.download() 默认把整个文件读进内存再写磁盘,下载 500MB 文件会直接 OOM。正确做法是手动分块读取、分块写入:
- 用
client.download(file_path)获取一个异步迭代器(每项是bytes) - 用
aiofiles.open(..., "wb")打开目标文件 - 循环
async for chunk in iterator,并await f.write(chunk)
关键不是“能不能下”,而是“会不会压垮内存”。漏掉流式处理,自动化脚本跑几次就崩。


















