使用requests模块出现“Too many open files”错误

4

我正在使用requests模块将多个文件上传到服务器,大部分时间都可以正常工作。但是,当上传的文件数量超过256时,会出现IOError: [Errno 24] Too many open files错误。这个问题的原因是我用下面代码中显示的方式打开了许多文件,并将它们构建成一个字典。由于我没有句柄来关闭这些打开的文件,因此就会出现这个错误。这带来了以下问题:

  1. Is there a way to close these files in chunks?
  2. Does requests module close the open files automatically?

    url = 'http://httpbin.org/post'
    #dict with several files > 256
    files = {'file1': open('report.xls', 'rb'), 'file2': open('report2.xls', 'rb')}
    r = requests.post(url, files=files)
    r.text
    

我现在使用的解决方法是,在每次上传 < 256个文件后调用files.clear()。我不确定这样做是否会关闭文件,但错误就消失了。

请提供如何处理这种情况的见解。谢谢


4
与其使用files.clear()来关闭所有打开的文件,使用for f in files.values(): f.close()会更好。 - mgilson
Python中的垃圾回收器不会为您正确关闭文件。因此,您必须使用f.close()自己关闭它们。 - randomusername
1
请注意,文件句柄数量的限制是由操作系统而非Python设置的。ulimit -n应该可以确认您已经知道的事情——对于您来说,限制为256。 - mgilson
有一件事需要考虑——尽可能少地占用内存——想象一下50个用户上传200个10MB的文件...所以,确保为稍后使用而保留文件列表,但是关于内容,如果你只是存储,打开、读取一个块,将其写入存储,重复,关闭。如果需要处理,则将数据加载到内存中,处理它,存储结果并释放原始数据。 - Basic
2个回答

7

这里最简单的解决方案是自己将文件读入内存,然后传递给请求。请注意,正如文档所述,“如果您想,可以发送字符串以作为文件接收”。因此,请这样做。

换句话说,不要像构建这样的字典:

files = {tag: open(pathname, 'rb') for (tag, pathname) in stuff_to_send}

建立它的方式如下:

def read_file(pathname):
    with open(pathname, 'rb') as f:
        return f.read()
files = {tag: read_file(pathname) for (tag, pathname) in stuff_to_send}

现在您一次只能打开一个文件,这是有保证的。

这可能看起来很浪费,但实际上并不是——如果您不这样做,requests 将会从所有文件中读取所有数据。*

但与此同时,让我回答您的实际问题,而不仅仅告诉您应该怎么做。


由于我没有句柄来关闭这些打开的文件,我们看到了这个错误。

当然您有。您有一个字典,其值为这些打开的文件。

事实上,如果您没有对它们进行处理,这个问题可能会更少发生,因为垃圾收集器会(通常情况下,但不一定稳健可靠,不能指望)为您处理这些问题。事实上它从未这样做过,这意味着您必须对它们进行处理。


有没有办法以块的方式关闭这些文件?

当然可以。我不知道您如何分块,但假设每个块都是一组键,您正在传递 files = {key: files[key] for key in chunk} ,对吧?

因此,在请求之后,请执行以下操作:

for key in chunk:
    files[key].close()

或者,如果您正在像这样为每个块构建一个dict
files = {tag: open(filename, 'rb') for (tag, filename) in chunk}

只需要这样做:

for file in files.values():
    file.close()

requests模块会自动关闭打开的文件吗?

不会。你需要手动关闭。

在许多情况下,由于请求后files变量很快就会消失,而一旦没有任何对该字典的引用,则其会很快被清除(使用CPython并且没有环路时立即清除;如果其中一个不是真的,则只是“很快”),这意味着所有文件都会很快被清理,此时析构函数会自动关闭它们。但是您不能依赖这一点,请始终明确地关闭文件。

files.clear()之所以有效是因为它与让files消失做了相同的事情:它强制字典忘记所有文件,从而删除了每个文件的最后一个引用,这意味着它们将很快被清除等等。


* 如果您没有足够的页面空间来容纳所有内容怎么办?那么您无论如何都无法一次性发送所有内容。您需要进行单独的请求,或使用流式API——我认为这也意味着您必须手动执行多部分操作。但是,如果您有足够的页面空间,只是没有足够的真实RAM,因此尝试读取所有内容会将您带入交换困境地狱,则可以通过将它们全部连接在磁盘上,打开巨大的文件,mmap映射其段,并将其作为字符串发送来解决这个问题...


感谢您提供详细的信息回答我的问题,这对我非常有帮助。祝好! - user3043805

5
请不要忘记Python鸭子类型的威力!
只需为您的文件实现一个包装类:
class LazyFile(object):

    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def read(self):
        with open(self.filename, self.mode) as f:
            return f.read()

url = 'http://httpbin.org/post'
#dict with a billion files
files = {'file1': LazyFile('report.xls', 'rb'), 'file2': LazyFile('report2.xls', 'rb')}
r = requests.post(url, files=files)
r.text

以这种方式,每个文件在字典中迭代时以读取并关闭的方式逐一打开。requests 请注意,尽管此答案和abarnert的答案目前基本上执行相同的操作,但requests在将来可能不会完全在内存中构建请求,然后发送请求,而是将每个文件作为流发送,从而保持内存使用率低。此时,此代码将更加内存有效。

不错,我会试一下的。 - user3043805
这个功能可行,并应该成为 Requests 库的正式组成部分。 - user4052054

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接