Urllib urlopen / urlretrieve太多打开的文件erorr

时间:2018-02-03 12:53:44

标签: python url download data-retrieval

问题

我想从ftp服务器并行下载> 100.000文件(使用线程)。我之前使用urlretrieve尝试了解答here,但这给了我以下错误:URLError(OSError(24, 'Too many open files'))。显然这个问题是一个错误(找不到引用了),所以我尝试将urlopenshutil结合使用,然后将其写入我可以关闭自己的文件,如上所述{{3} }。这似乎工作正常,但后来我又得到了同样的错误:URLError(OSError(24, 'Too many open files'))。我认为只要写入文件不完整或失败,with语句就会导致文件关闭,但是看到文件仍然保持打开状态,最终会导致脚本暂停。

问题

如何防止此错误,即确保每个文件都关闭?

代码

import csv
import urllib.request
import shutil
from multiprocessing.dummy import Pool

def url_to_filename(url):
    filename = 'patric_genomes/' + url.split('/')[-1]
    return filename

def download(url):
    url = url.strip()
    try:
        with urllib.request.urlopen(url) as response, open(url_to_filename(url), 'wb') as out_file:
            shutil.copyfileobj(response, out_file)
    except Exception as e:
        return None, e

def build_urls(id_list):
    base_url = 'ftp://some_ftp_server/'
    urls = []
    for some_id in id_list:
        url = base_url + some_id + '/' + some_id + '.fna'
        print(url)
        urls.append(url)
    return urls


if __name__ == "__main__":
    with open('full_data/genome_ids.txt') as inFile:
        reader = csv.DictReader(inFile, delimiter = '\t')
        ids = {row['some_id'] for row in reader}
        urls = build_urls(ids)
        p = Pool(100)
        print(p.map(download, urls)) 

3 个答案:

答案 0 :(得分:2)

您可以尝试使用contextlib关闭文件:

import contextlib
[ ... ]

with contextlib.closing(urllib.request.urlopen(url)) as response, open(url_to_filename(url), 'wb') as out_file:
        shutil.copyfileobj(response, out_file)

[ ... ]

根据docs

contextlib.closing(thing)

    Return a context manager that closes thing upon completion of the block. [ ... ] without needing to explicitly close page. Even if an error occurs, page.close() will be called when the with block is exited.

***解决方法是提高Linux操作系统的开放文件限制。检查您当前的打开文件限制:

ulimit -Hn

/etc/sysctl.conf文件中添加以下行:

fs.file-max = <number>

其中<number>是您要设置的打开文件的新上限。 关闭并保存文件。

sysctl -p

以便更改生效。

答案 1 :(得分:1)

我相信您创建的文件处理程序不会被系统及时处理,因为关闭连接需要一些时间。所以你最终会很快使用所有免费文件处理程序(包括网络套接字)。

您要为每个文件设置FTP连接。这是一种不好的做法。更好的方法是打开5-15个连接并重用它们,通过现有套接字下载文件,而不会为每个文件进行初始FTP握手。请参阅this post以供参考。

P.S。另外,正如@Tarun_Lalwani所提到的,创建一个包含超过〜1000个文件的文件夹并不是一个好主意,因为它会降低文件系统的速度。

答案 2 :(得分:0)

  

如何防止此错误,即确保每个文件都关闭?

要防止错误,您需要increase open file limit,或者更合理的是,减少线程池中的并发性。连接和文件关闭由上下文管理器正确完成。

您的线程池有100个线程,并打开至少200个句柄(一个用于FTP连接,另一个用于文件)。合理的并发性大约是10到30个线程。

这里的简化复制表明代码没问题。将一些内容放在当前目录的somefile中。

test.py

#!/usr/bin/env python3

import sys
import shutil
import logging
from pathlib import Path
from urllib.request import urlopen
from multiprocessing.dummy import Pool as ThreadPool


def download(id):
    ftp_url = sys.argv[1]
    filename = Path(__name__).parent / 'files'
    try:
        with urlopen(ftp_url) as src, (filename / id).open('wb') as dst:
            shutil.copyfileobj(src, dst)
    except Exception as e:
        logging.exception('Download error')


if __name__ == '__main__':
    with ThreadPool(10) as p:
        p.map(download, (str(i).zfill(4) for i in range(1000)))

然后在同一目录中:

$ docker run --name=ftp-test -d -e FTP_USER=user -e FTP_PASSWORD=pass \
  -v `pwd`/somefile:/srv/dir/somefile panubo/vsftpd vsftpd /etc/vsftpd.conf
$ IP=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' ftp-test`
$ curl ftp://user:pass@$IP/dir/somefile
$ python3 client.py ftp://user:pass@$IP/dir/somefile    
$ docker stop ftp-test && docker rm -v ftp-test