代码之家  ›  专栏  ›  技术社区  ›  CodeNoob

Urllib urlopen/urlretrieve打开的文件过多错误

  •  3
  • CodeNoob  · 技术社区  · 7 年前

    问题

    我正在尝试下载>并行(使用线程)来自ftp服务器的100.000个文件。我之前用urlretrieve尝试过 here ,但这给了我以下错误: URLError(OSError(24, 'Too many open files')) . 显然,这个问题是一个bug(再也找不到引用),所以我尝试使用 urlopen 结合 shutil 然后将其写入我可以自己关闭的文件,如所述 here . 这似乎很好,但后来我又犯了同样的错误: URLRERROR(OSError(24,“打开的文件太多”)) . 我认为每当写入文件不完整或将失败时 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 回复  |  直到 4 年前
        1
  •  3
  •   AnythingIsFine    7 年前

    您可以尝试使用 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
    

    使更改生效。

        2
  •  1
  •   igrinis    7 年前

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

    您要做的是为每个文件设置FTP连接。这是一种糟糕的做法。更好的方法是打开5-15个连接并重用它们,通过 现有的 套接字,无需为每个文件进行初始FTP握手的开销。看见 this post 供参考。

    P、 此外,正如@Tarun\u Lalwani所提到的,创建一个包含1000多个文件的文件夹不是一个好主意,因为这样会降低文件系统的速度。

        3
  •  0
  •   saaj    7 年前

    如何防止这种erorr,即确保关闭所有文件?

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

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

    这是一个简化的复制,表明代码是正确的。放入一些内容 somefile 在当前目录中。

    测验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