【问题标题】:Python: File download using ftplib hangs forever after file is successfully downloadedPython:文件下载成功后,使用 ftplib 下载文件永远挂起
【发布时间】:2018-10-03 04:54:52
【问题描述】:

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

我尝试将 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)

正在打印以下日志。

cmd 'TYPE I' put 'TYPE I\r\n' get '200 类型设置为 I。\r\n' resp '200 类型设置为 I。' cmd 'PASV' put 'PASV\r\n' get '227 进入被动模式 (64,27,160,28,133,251)。\r\n' resp '227 进入被动模式(64,27,160,28,133,251)。 cmd 'RETR FFFT_BRA_PM_R_201711.txt' put 'RETR FFFT_BRA_PM_R_201711.txt\r\n' get '150 打开 FFFT_BRA_PM_R_201711.txt 的 BINARY 模式数据连接。\r\n' resp '150 打开 FFFT_BRA_PM_R_201711.txt 的 BINARY 模式数据连接。'

【问题讨论】:

  • 您尝试等待文件下载完成多长时间?您可以使用与您的 Python 代码在同一台机器上运行的任何 FTP 客户端下载相同的文件吗?
  • 是的,我可以使用 FTP 客户端下载文件。文件下载已完成,但它从不发布任何内容。
  • 抱歉您的评论含糊不清。通过您的第二句话(“文件下载完成”),您指的是 FTP 客户端还是您的 Python 代码?向我们展示 FTP 客户端的日志文件。
  • 我的错!我实际上误解并给出了非常神秘的回应。当我尝试从 ftp 客户端下载文件时,即使在文件下载后处于挂起状态。虽然如果我在 shell 脚本中使用期望并设置超时,我可以下载这个文件
  • 好的,所以你的问题与 Python 或 ftplib 无关。所以它在 Stack Overflow 上是题外话。考虑将其移至Super User

标签: python download ftp freeze ftplib


【解决方案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='test@example.com')
            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='test@example.com')
        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.closeftp.abort 之后。
  • ftp.abortsock.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。试着把它推得更远。

结合keepalives

例如,如果您的下载在 10MB 时失败,而您问题中的 keepalive 代码将大小增加到 512MB,但对于 3GB 来说还是不够,您可以将两者结合起来。使用 keepalive 一次读取 512MB,然后中止或重新连接并读取下一个 512MB,直到完成。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-04-29
    • 1970-01-01
    • 1970-01-01
    • 2012-07-19
    • 2018-02-04
    • 2014-10-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多