Python:使用 ftplib 下载文件在成功下载后永远挂起

Python:使用 ftplib 下载文件在成功下载后永远挂起

我一直在尝试解决从 ftp/ftps 下载文件时出现的问题。文件已成功下载,但文件下载完成后未执行任何操作。没有发生错误,这可以提供有关该问题的更多信息。我尝试在 stackoverflow 上搜索此问题,并找到了此关联谈到了类似的问题陈述,看起来我也遇到了类似的问题,但我不确定。需要更多帮助来解决问题。

我尝试将 FTP 连接超时设置为 60 分钟,但帮助不大。在此之前,我使用 ftplib 的 retrbinary(),但那里也出现了同样的问题。我尝试传递不同的块大小和窗口大小,但问题仍然重现。

我正在尝试从 AWS EMR 集群下载大小约为 3GB 的文件。示例代码如下所示。

    def download_ftp(self, ip, port, user_name, password, file_name, target_path):
    try:
        os.chdir(target_path)
        ftp = FTP(host=ip)
        ftp.connect(port=int(port), timeout=3000)
        ftp.login(user=user_name, passwd=password)

        if ftp.nlst(file_name) != []:
            dir = os.path.split(file_name)
            ftp.cwd(dir[0])
            for filename in ftp.nlst(file_name):
                sock = ftp.transfercmd('RETR ' + filename)

                def background():
                    fhandle = open(filename, 'wb')
                    while True:
                        block = sock.recv(1024 * 1024)
                        if not block:
                            break
                        fhandle.write(block)
                    sock.close()

                t = threading.Thread(target=background)
                t.start()
                while t.is_alive():
                    t.join(60)
                    ftp.voidcmd('NOOP')
                logger.info("File " + filename + " fetched successfully")
            return True
        else:
            logger.error("File " + file_name + " is not present in FTP")

    except Exception, e:
        logger.error(e)
        raise

上述链接中建议的另一个选项是在下载完小块文件后关闭连接,然后重新启动连接。有人能建议如何实现这一点吗?不确定如何在关闭连接之前从上次文件下载停止的同一点恢复下载。这种方法是否是下载整个文件的完整证据?

我不太了解 FTP 服务器级超时设置,所以不知道需要更改什么以及如何更改。我基本上想编写一个通用的 FTP 下载器,它可以帮助从 FTP/FTPS 下载文件。

当我使用 ftplib 的 retrbinary() 方法并将调试级别设置为 2 时。

ftp.set_debuglevel(2)
ftp.retrbinary('RETR ' + filename, fhandle.write)

正在打印以下日志。

命令'I 类' ‘类型 I\r\n’ 得到'200 类型设置为 I.\r\n' 相应‘200 类型设置为 I。’ 命令'PASV' ‘PASV\r\n’ 得到'227 进入被动模式 (64,27,160,28,133,251)。\r\n' 相应‘227 进入被动模式 (64,27,160,28,133,251)。’ 命令'返回 FFFT_BRA_PM_R_201711.txt' '返回 FFFT_BRA_PM_R_201711.txt\r\n' 得到'150 为 FFFT_BRA_PM_R_201711.txt 打开二进制模式数据连接。\r\n' 相应‘150 打开 FFFT_BRA_PM_R_201711.txt 的二进制模式数据连接。’

答案1

在做任何事情之前,请注意您的连接存在严重问题,诊断并修复它比绕过它要好得多。但有时,您只需要处理损坏的服务器,即使发送保持连接也无济于事。那么,您能做什么呢?

诀窍是一次下载一个块,然后中止下载 - 或者,如果服务器无法处理中止,则关闭并重新打开连接。

请注意,我正在使用以下所有内容进行测试ftp://speedtest.tele2.net/5MB.zip,希望这不会导致数百万人开始攻击他们的服务器。当然,您需要使用实际服务器进行测试。

测试REST

当然,整个解决方案依赖于服务器能够恢复传输,但并非所有服务器都能做到这一点——尤其是当您处理严重损坏的服务器时。所以我们需要对此进行测试。请注意,此测试将非常缓慢,并且对服务器的负担非常大,因此不要使用 3GB 文件进行测试;找一些小得多的文件。此外,如果您可以在那里放置一些可读的内容,这将有助于调试,因为您可能无法在十六进制编辑器中比较文件。

def downit():
    with open('5MB.zip', 'wb') as f:
        while True:
            ftp = FTP(host='speedtest.tele2.net', user='anonymous', passwd='[email protected]')
            pos = f.tell()
            print(pos)
            ftp.sendcmd('TYPE I')
            sock = ftp.transfercmd('RETR 5MB.zip', rest=pos)
            buf = sock.recv(1024 * 1024)
            if not buf:
                return
            f.write(buf)

您可能不会一次获得 1MB,而是获得 8KB 以下的数据。假设您看到 1448,然后是 2896、4344 等。

  • 如果您收到异常REST,则服务器不会处理恢复 - 放弃吧,您完蛋了。
  • 如果文件超出实际文件大小,请按 ^C,然后在十六进制编辑器中检查。
    • 如果您一遍又一遍地看到相同的 1448 字节或其他数字(您看到打印出来的数字),那么您就遇到了麻烦。
    • 如果您拥有正确的数据,但每 1448 字节块之间有额外的字节,那么这实际上是可以修复的。如果您遇到这种情况并且无法弄清楚如何使用 修复它f.seek,我可以解释——但您可能不会遇到它。

测试ABRT

我们可以做的一件事是尝试中止下载并不是重新连接。

def downit():
    with open('5MB.zip', 'wb') as f:
        ftp = FTP(host='speedtest.tele2.net', user='anonymous', passwd='[email protected]')
        while True:
            pos = f.tell()
            print(pos)
            ftp.sendcmd('TYPE I')
            sock = ftp.transfercmd('RETR 5MB.zip', rest=pos)
            buf = sock.recv(1024 * 1024)
            if not buf:
                return
            f.write(buf)
            sock.close()
            ftp.abort()

您可能想尝试多种变化:

  • sock.close
  • ftp.abort
  • sock.close之后ftp.abort
  • ftp.abort之后sock.close
  • 以上所有四个都重复进行,TYPE I但每次都移至循环之前。

有些会引发异常。其他的似乎会永远挂起。如果所有 8 个都是这样,我们需要放弃中止。但如果其中任何一个有效,那就太好了!

下载完整块

另一种加快速度的方法是每次下载 1MB(或更多),然后再中止或重新连接。只需替换此代码:

buf = sock.recv(1024 * 1024)
if buf:
    f.write(buf)

有了这个:

chunklen = 1024 * 1024
while chunklen:
    print('   ', f.tell())
    buf = sock.recv(chunklen)
    if not buf:
        break
    f.write(buf)
    chunklen -= len(buf)

现在,您每次传输读取的数据不再是 1442 或 8192 字节,而是高达 1MB。尝试将其推得更远。

与 Keepalive 结合

假设您的下载在 10MB 时失败,而问题中的 keepalive 代码将下载量提升至 512MB,但还不够 3GB — 您可以将两者结合起来。使用 keepalive 一次读取 512MB,然后中止或重新连接并读取下一个 512MB,直到完成。

相关内容