Вы можете реализовать файлоподобный объект, который читает данные с FTP, а не из локального файла. И передайте это ZipFile
конструктору вместо (локального) имени файла.
Тривиальная реализация может быть такой:
from ftplib import FTP
from ssl import SSLSocket
class FtpFile:
def __init__(self, ftp, name):
self.ftp = ftp
self.name = name
self.size = ftp.size(name)
self.pos = 0
def seek(self, offset, whence):
if whence == 0:
self.pos = offset
if whence == 1:
self.pos += offset
if whence == 2:
self.pos = self.size + offset
def tell(self):
return self.pos
def read(self, size = None):
if size == None:
size = self.size - self.pos
data = B""
# based on FTP.retrbinary
# (but allows stopping after certain number of bytes read)
ftp.voidcmd('TYPE I')
cmd = "RETR {}".format(self.name)
conn = ftp.transfercmd(cmd, self.pos)
try:
while len(data) < size:
buf = conn.recv(min(size - len(data), 8192))
if not buf:
break
data += buf
# shutdown ssl layer (can be removed if not using TLS/SSL)
if SSLSocket is not None and isinstance(conn, SSLSocket):
conn.unwrap()
finally:
conn.close()
try:
ftp.voidresp()
except:
pass
self.pos += len(data)
return data
И тогда вы можете использовать его как:
ftp = FTP(host, user, passwd)
ftp.cwd(path)
ftpfile = FtpFile(ftp, "archive.zip")
zip = zipfile.ZipFile(ftpfile)
print(zip.namelist())
Вышеприведенная реализация довольно тривиальна и неэффективна. Он начинает многочисленные (как минимум три) загрузки небольших порций данных для получения списка содержащихся файлов. Это можно оптимизировать, читая и кэшируя большие куски. Но это должно дать вам представление.
В частности, вы можете использовать тот факт, что вы собираетесь читать только список. Список находится в и из архива ZIP. Таким образом, вы можете просто загрузить последние (около) 10 КБ данных в начале. И вы сможете выполнять все read
звонки из этого кэша.
Зная это, вы действительно можете сделать небольшой взлом. Так как список находится в конце архива, вы можете скачать только конец архива. Несмотря на то, что загруженный ZIP-файл будет поврежден, он все еще может быть указан в списке. Таким образом, вам не понадобится класс FtpFile
. Вы можете даже загрузить данные в память (StringIO
).
zipstring = StringIO()
name = "archive.zip"
size = ftp.size(name)
ftp.retrbinary("RETR " + name, zipstring.write, rest = size - 10*2024)
zip = zipfile.ZipFile(zipstring)
print(zip.namelist())
Если вы получите исключение BadZipfile
, поскольку 10 КБ слишком малы, чтобы содержать полный список, вы можете повторить код с большим фрагментом.