Получите имена файлов внутри zip-файла на FTP-сервере, не скачивая весь архив - PullRequest
0 голосов
/ 04 ноября 2018

У меня много zip-архивов на удаленном FTP-сервере, и их размеры достигают 20 ТБ. Мне просто нужны имена файлов внутри этих zip-архивов, чтобы я мог включить их в свои скрипты Python.

Есть ли способ просто получить имена файлов без фактической загрузки файлов и их извлечения на моем локальном компьютере? Если да, может кто-нибудь направить меня в нужную библиотеку / пакет?

1 Ответ

0 голосов
/ 04 ноября 2018

Вы можете реализовать файлоподобный объект, который читает данные с 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 КБ слишком малы, чтобы содержать полный список, вы можете повторить код с большим фрагментом.

...