【问题标题】:How can I speed up fetching pages with urllib2 in python?如何在 python 中加快使用 urllib2 获取页面的速度?
【发布时间】:2011-03-30 05:51:30
【问题描述】:

我有一个脚本可以获取多个网页并解析信息。

(可以在http://bluedevilbooks.com/search/?DEPT=MATH&CLASS=103&SEC=01看到一个例子)

我在上面运行了 cProfile,正如我假设的那样,urlopen 占用了很多时间。有没有办法更快地获取页面?或者一次获取多个页面的方法?我会做任何最简单的事情,因为我是 python 和 web 开发的新手。

提前致谢! :)

更新:我有一个名为 fetchURLs() 的函数,我用它来制作我需要的 URL 数组 所以像urls = fetchURLS().URL 都是来自 Amazon 和 eBay API 的 XML 文件(这让我很困惑为什么加载需要这么长时间,也许我的虚拟主机很慢?)

我需要做的是加载每个 URL,读取每个页面,并将该数据发送到脚本的另一部分,该部分将解析和显示数据。

请注意,在获取所有页面之前,我无法执行后一部分,这就是我的问题。

另外,我相信我的主机一次限制我最多 25 个进程,所以服务器上最简单的任何一个都很好 :)


时间到了:

Sun Aug 15 20:51:22 2010    prof

         211352 function calls (209292 primitive calls) in 22.254 CPU seconds

   Ordered by: internal time
   List reduced from 404 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       10   18.056    1.806   18.056    1.806 {_socket.getaddrinfo}
     4991    2.730    0.001    2.730    0.001 {method 'recv' of '_socket.socket' objects}
       10    0.490    0.049    0.490    0.049 {method 'connect' of '_socket.socket' objects}
     2415    0.079    0.000    0.079    0.000 {method 'translate' of 'unicode' objects}
       12    0.061    0.005    0.745    0.062 /usr/local/lib/python2.6/HTMLParser.py:132(goahead)
     3428    0.060    0.000    0.202    0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1306(endData)
     1698    0.055    0.000    0.068    0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1351(_smartPop)
     4125    0.053    0.000    0.056    0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:118(setup)
     1698    0.042    0.000    0.358    0.000 /usr/local/lib/python2.6/HTMLParser.py:224(parse_starttag)
     1698    0.042    0.000    0.275    0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1397(unknown_starttag)

【问题讨论】:

    标签: python time urllib2 urlopen cprofile


    【解决方案1】:

    编辑:我正在扩展答案以包含一个更精美的示例。我在这篇文章中发现了很多关于线程与线程的敌意和错误信息。异步 I/O。因此,我还添加了更多论据来驳斥某些无效的主张。我希望这将帮助人们为正确的工作选择正确的工具。

    这是 3 天前的一个问题。

    Python urllib2.open 很慢,需要更好的方法来读取多个 url - 代码日志 Python urllib2.urlopen() is slow, need a better way to read several urls

    我正在完善代码以展示如何使用线程并行获取多个网页。

    import time
    import threading
    import Queue
    
    # utility - spawn a thread to execute target for each args
    def run_parallel_in_threads(target, args_list):
        result = Queue.Queue()
        # wrapper to collect return value in a Queue
        def task_wrapper(*args):
            result.put(target(*args))
        threads = [threading.Thread(target=task_wrapper, args=args) for args in args_list]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        return result
    
    def dummy_task(n):
        for i in xrange(n):
            time.sleep(0.1)
        return n
    
    # below is the application code
    urls = [
        ('http://www.google.com/',),
        ('http://www.lycos.com/',),
        ('http://www.bing.com/',),
        ('http://www.altavista.com/',),
        ('http://achewood.com/',),
    ]
    
    def fetch(url):
        return urllib2.urlopen(url).read()
    
    run_parallel_in_threads(fetch, urls)
    

    如您所见,应用程序特定代码只有 3 行,如果您比较激进,可以折叠成 1 行。我认为没有人可以证明他们声称这是复杂且不可维护的。

    不幸的是,这里发布的大多数其他线程代码都有一些缺陷。他们中的许多人进行主动轮询以等待代码完成。 join() 是同步代码的更好方法。到目前为止,我认为这段代码已经改进了所有线程示例。

    保持连接

    如果所有 URL 都指向同一个服务器,WoLpH 关于使用保持连接的建议可能会非常有用。

    扭曲

    Aaron Gallagher 是twisted 框架的粉丝,他对任何建议线程的人都怀有敌意。不幸的是,他的许多说法都是错误的信息。例如,他说“-1 表示建议线程。这是 IO 绑定的;线程在这里没用。”这与 Nick T 和我都证明了使用线程的速度增益的证据相反。事实上,I/O 绑定的应用程序从使用 Python 的线程中获益最多(相对于 CPU 绑定的应用程序没有任何收益)。 Aaron 对线程的误导性批评表明,他对一般的并行编程相当困惑。

    适合工作的正确工具

    我很清楚与使用线程、python、异步 I/O 等进行并行编程有关的问题。每个工具都有其优点和缺点。对于每种情况,都有一个合适的工具。我不反对扭曲(尽管我自己没有部署一个)。但我不相信我们可以直截了当地说螺纹在所有情况下都是坏的,扭曲是好的。

    例如,如果 OP 的要求是并行获取 10,000 个网站,则首选异步 I/O。线程将不适用(除非可能使用无堆栈 Python)。

    Aaron 反对线程主要是泛化。他没有意识到这是一项微不足道的并行化任务。每个任务都是独立的,不共享资源。所以他的大部分攻击都不适用。

    鉴于我的代码没有外部依赖,我将其称为正确的工具来完成正确的工作。

    性能

    我想大多数人都会同意这个任务的性能很大程度上取决于网络代码和外部服务器,平台代码的性能应该可以忽略不计。然而,Aaron 的基准测试显示,与线程代码相比,速度提高了 50%。我认为有必要对这种明显的速度增益做出反应。

    在尼克的代码中,有一个明显的缺陷导致效率低下。但是您如何解释我的代码的 233 毫秒速度增益?我想即使是twisted的粉丝也不会贸然下结论将其归因于twisted的效率。毕竟系统代码之外还有大量的变量,比如远程服务器的性能、网络、缓存、urllib2和twisted web客户端的区别实现等等。

    为了确保 Python 的线程不会导致大量的低效率,我做了一个快速基准测试来生成 5 个线程,然后是 500 个线程。我很自在地说产生 5 个线程的开销可以忽略不计,无法解释 233 毫秒的速度差异。

    In [274]: %time run_parallel_in_threads(dummy_task, [(0,)]*5)
    CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s
    Wall time: 0.00 s
    Out[275]: <Queue.Queue instance at 0x038B2878>
    
    In [276]: %time run_parallel_in_threads(dummy_task, [(0,)]*500)
    CPU times: user 0.16 s, sys: 0.00 s, total: 0.16 s
    Wall time: 0.16 s
    
    In [278]: %time run_parallel_in_threads(dummy_task, [(10,)]*500)
    CPU times: user 1.13 s, sys: 0.00 s, total: 1.13 s
    Wall time: 1.13 s       <<<<<<<< This means 0.13s of overhead
    

    对我的并行提取的进一步测试显示,在 17 次运行中响应时间存在巨大差异。 (很遗憾我没有扭曲验证 Aaron 的代码)。

    0.75 s
    0.38 s
    0.59 s
    0.38 s
    0.62 s
    1.50 s
    0.49 s
    0.36 s
    0.95 s
    0.43 s
    0.61 s
    0.81 s
    0.46 s
    1.21 s
    2.87 s
    1.04 s
    1.72 s
    

    我的测试不支持 Aaron 的结论,即线程始终比异步 I/O 慢一个可测量的幅度。考虑到涉及的变量数量,我不得不说这不是衡量异步 I/O 和线程之间系统性能差异的有效测试。

    【讨论】:

    • 查看我刚刚留下的另一条评论:我从未说过线程在这种情况下无效。每个人似乎在他们的答案中忘记或忽略的线程问题是不值得的。这是一个启发性的图形:erights.org/elib/concurrency/images/badtradeoff.gif
    • 这不是答案,它是三个 cmets。请不要滥用问答系统,必要时发表评论。
    • 我不需要测试异步 I/O。我之所以这么说是因为我自己的测试范围相差多达 2.51 秒,因此有人声称替代解决方案始终以小得多的幅度更快是无效的。除非替代代码比此代码慢很多比 2.51 大很多,否则我们可以声称它始终比此代码慢。
    • @Aaron,你没有发现任何问题。你只是提出 FUD 索赔。如果您在 urllib2 或 Python 的任何其他非线程安全部分中发现任何内容,请提交错误。有大量使用线程的生产软件。如果 Python 不是为线程安全而设计的,那么在生产环境中使用它都是白痴。
    • 离题;这个答案怎么会有 5 票反对?扭曲的粉丝是相当斗气的。 :P
    【解决方案2】:

    使用twisted!与使用线程相比,它使这种事情变得非常容易。

    from twisted.internet import defer, reactor
    from twisted.web.client import getPage
    import time
    
    def processPage(page, url):
        # do somewthing here.
        return url, len(page)
    
    def printResults(result):
        for success, value in result:
            if success:
                print 'Success:', value
            else:
                print 'Failure:', value.getErrorMessage()
    
    def printDelta(_, start):
        delta = time.time() - start
        print 'ran in %0.3fs' % (delta,)
        return delta
    
    urls = [
        'http://www.google.com/',
        'http://www.lycos.com/',
        'http://www.bing.com/',
        'http://www.altavista.com/',
        'http://achewood.com/',
    ]
    
    def fetchURLs():
        callbacks = []
        for url in urls:
            d = getPage(url)
            d.addCallback(processPage, url)
            callbacks.append(d)
    
        callbacks = defer.DeferredList(callbacks)
        callbacks.addCallback(printResults)
        return callbacks
    
    @defer.inlineCallbacks
    def main():
        times = []
        for x in xrange(5):
            d = fetchURLs()
            d.addCallback(printDelta, time.time())
            times.append((yield d))
        print 'avg time: %0.3fs' % (sum(times) / len(times),)
    
    reactor.callWhenRunning(main)
    reactor.run()
    

    此代码的性能也比发布的任何其他解决方案都好(在我关闭一些占用大量带宽的东西后编辑):

    Success: ('http://www.google.com/', 8135)
    Success: ('http://www.lycos.com/', 29996)
    Success: ('http://www.bing.com/', 28611)
    Success: ('http://www.altavista.com/', 8378)
    Success: ('http://achewood.com/', 15043)
    ran in 0.518s
    Success: ('http://www.google.com/', 8135)
    Success: ('http://www.lycos.com/', 30349)
    Success: ('http://www.bing.com/', 28611)
    Success: ('http://www.altavista.com/', 8378)
    Success: ('http://achewood.com/', 15043)
    ran in 0.461s
    Success: ('http://www.google.com/', 8135)
    Success: ('http://www.lycos.com/', 30033)
    Success: ('http://www.bing.com/', 28611)
    Success: ('http://www.altavista.com/', 8378)
    Success: ('http://achewood.com/', 15043)
    ran in 0.435s
    Success: ('http://www.google.com/', 8117)
    Success: ('http://www.lycos.com/', 30349)
    Success: ('http://www.bing.com/', 28611)
    Success: ('http://www.altavista.com/', 8378)
    Success: ('http://achewood.com/', 15043)
    ran in 0.449s
    Success: ('http://www.google.com/', 8135)
    Success: ('http://www.lycos.com/', 30349)
    Success: ('http://www.bing.com/', 28611)
    Success: ('http://www.altavista.com/', 8378)
    Success: ('http://achewood.com/', 15043)
    ran in 0.547s
    avg time: 0.482s
    

    并且使用 Nick T 的代码,也可以给出 5 的平均值并更好地显示输出:

    Starting threaded reads:
    ...took 1.921520 seconds ([8117, 30070, 15043, 8386, 28611])
    Starting threaded reads:
    ...took 1.779461 seconds ([8135, 15043, 8386, 30349, 28611])
    Starting threaded reads:
    ...took 1.756968 seconds ([8135, 8386, 15043, 30349, 28611])
    Starting threaded reads:
    ...took 1.762956 seconds ([8386, 8135, 15043, 29996, 28611])
    Starting threaded reads:
    ...took 1.654377 seconds ([8117, 30349, 15043, 8386, 28611])
    avg time: 1.775s
    
    Starting sequential reads:
    ...took 1.389803 seconds ([8135, 30147, 28611, 8386, 15043])
    Starting sequential reads:
    ...took 1.457451 seconds ([8135, 30051, 28611, 8386, 15043])
    Starting sequential reads:
    ...took 1.432214 seconds ([8135, 29996, 28611, 8386, 15043])
    Starting sequential reads:
    ...took 1.447866 seconds ([8117, 30028, 28611, 8386, 15043])
    Starting sequential reads:
    ...took 1.468946 seconds ([8153, 30051, 28611, 8386, 15043])
    avg time: 1.439s
    

    并使用伟业东的代码:

    Fetched 8117 from http://www.google.com/
    Fetched 28611 from http://www.bing.com/
    Fetched 8386 from http://www.altavista.com/
    Fetched 30051 from http://www.lycos.com/
    Fetched 15043 from http://achewood.com/
    done in 0.704s
    Fetched 8117 from http://www.google.com/
    Fetched 28611 from http://www.bing.com/
    Fetched 8386 from http://www.altavista.com/
    Fetched 30114 from http://www.lycos.com/
    Fetched 15043 from http://achewood.com/
    done in 0.845s
    Fetched 8153 from http://www.google.com/
    Fetched 28611 from http://www.bing.com/
    Fetched 8386 from http://www.altavista.com/
    Fetched 30070 from http://www.lycos.com/
    Fetched 15043 from http://achewood.com/
    done in 0.689s
    Fetched 8117 from http://www.google.com/
    Fetched 28611 from http://www.bing.com/
    Fetched 8386 from http://www.altavista.com/
    Fetched 30114 from http://www.lycos.com/
    Fetched 15043 from http://achewood.com/
    done in 0.647s
    Fetched 8135 from http://www.google.com/
    Fetched 28611 from http://www.bing.com/
    Fetched 8386 from http://www.altavista.com/
    Fetched 30349 from http://www.lycos.com/
    Fetched 15043 from http://achewood.com/
    done in 0.693s
    avg time: 0.715s
    

    我不得不说,我确实喜欢顺序提取对我来说更好

    【讨论】:

    • 我确实喜欢我在没有 cmets 的情况下得到 -2!来吧,downvoters,试着证明我的代码很糟糕~
    • 您的基准测试存在轻微缺陷恕我直言。您正在对总是几乎即时响应的优秀搜索引擎进行基准测试。将您的解决方案与普通网站一起使用时,顺序提取的性能会更差,因为瓶颈将出现在服务器端/互联网而不是您的 Python 代码上。
    • @WoLpH,我修改了我测试的其他代码以请求相同的站点。看看长度基本一样吗?
    • @WoLpH,也是“巨大的”? Twisted 比 python 小很多。
    • @Parker,如果您的 url 列表很大,这种方法可能不适合您,因为它或多或少地同时为每个 url 打开一个连接。这可能会导致您的互联网连接阻塞。尝试一次运行较少数量的网址,看看是否有帮助
    【解决方案3】:

    这是一个使用 python Threads 的示例。此处的其他线程示例为每个 url 启动一个线程,如果它导致服务器处理的命中过多(例如蜘蛛通常在同一主机上拥有多个 url),这不是非常友好的行为

    from threading import Thread
    from urllib2 import urlopen
    from time import time, sleep
    
    WORKERS=1
    urls = ['http://docs.python.org/library/threading.html',
            'http://docs.python.org/library/thread.html',
            'http://docs.python.org/library/multiprocessing.html',
            'http://docs.python.org/howto/urllib2.html']*10
    results = []
    
    class Worker(Thread):
        def run(self):
            while urls:
                url = urls.pop()
                results.append((url, urlopen(url).read()))
    
    start = time()
    threads = [Worker() for i in range(WORKERS)]
    any(t.start() for t in threads)
    
    while len(results)<40:
        sleep(0.1)
    print time()-start
    

    注意:这里给出的时间是 40 个 url,很大程度上取决于您的互联网连接速度和服务器延迟。在澳大利亚,我的 ping > 300ms

    使用WORKERS=1 运行需要 86 秒
    使用 WORKERS=4 运行需要 23 秒
    使用 WORKERS=10 运行需要 10 秒

    所以 10 个线程的下载速度是单个线程的 8.6 倍。

    这是一个使用队列的升级版本。至少有几个优点。
    1. url 是按照它们在列表中出现的顺序请求的
    2.可以使用q.join()来检测请求何时全部完成
    3.结果保持与url列表相同的顺序

    from threading import Thread
    from urllib2 import urlopen
    from time import time, sleep
    from Queue import Queue
    
    WORKERS=10
    urls = ['http://docs.python.org/library/threading.html',
            'http://docs.python.org/library/thread.html',
            'http://docs.python.org/library/multiprocessing.html',
            'http://docs.python.org/howto/urllib2.html']*10
    results = [None]*len(urls)
    
    def worker():
        while True:
            i, url = q.get()
            # print "requesting ", i, url       # if you want to see what's going on
            results[i]=urlopen(url).read()
            q.task_done()
    
    start = time()
    q = Queue()
    for i in range(WORKERS):
        t=Thread(target=worker)
        t.daemon = True
        t.start()
    
    for i,url in enumerate(urls):
        q.put((i,url))
    q.join()
    print time()-start
    

    【讨论】:

    • 不符合我的基准。我认为你做错了什么。
    • 我想听听你告诉我们 list.append 如何不是原子的。我查看了字节码 - dis.dis(compile("[].append(1)","","exec"))。追加发生在指令 #9 中。在我看来它是原子的。
    • @Aaron,队列不仅仅是以原子方式传输数据。它是一个有界缓冲区,这意味着它可以阻塞生产者或消费者,直到数据或空间可用于同步目的。
    • @Aaron,关于队列,让我提醒你上下文。您正在挑战 gnibbler 声称 list.append 是原子的。你说如果 list.append 是原子的,队列就不会存在。我提醒你,Queue 的主要目的是实现有界缓冲区。
    • @Aaron,关于附加函数调用是否是原子的。我认为任何有理智的人都会设计一个基本操作,比如作为单个步骤追加,而不是由其他 python 步骤组成。但如果你不相信,公平地说,让我们看一下 Python 源代码。 (svn.python.org/view/python/tags/r27rc2/Objects/…) 追加由 PyList_Append() 实现。对我来说这看起来很理智。没有发布 GIL。不调用其他 Python 函数。当它到达 PyMem_RESIZE 时,我停止了跟踪。但我认为他们在那里发布 GIL 会很疯狂。
    【解决方案4】:

    实际的等待可能不在urllib2,而是在服务器和/或您与服务器的网络连接中。

    有两种方法可以加快速度。

    1. 保持连接有效(有关如何做到这一点,请参阅此问题:Python urllib2 with keep alive
    2. 使用多重连接,您可以按照 Aaron Gallagher 的建议使用线程或异步方法。为此,只需使用任何线程示例就可以了 :) 您还可以使用 multiprocessing 库让事情变得非常简单。

    【讨论】:

    • -1 用于建议线程。这是 IO 绑定的;线程在这里没用。
    • @Aaron,通常线程非常适合下载网页。该进程不会受 I/O 限制,除非它正在下载非常大的文件或延迟非常低。 urllib2 通常会花费大部分时间被阻塞,等待响应,这是 Python 的 GIL/线程的完美条件
    • @gnibbler,不,这就是 IO 绑定的意思:进程大部分时间都在等待 IO。多线程不会让您更快地等待数据。只需使用非阻塞 IO;没有额外的代码复杂性或锁定开销。
    • @Aaron,确定你对 IO Bound 的定义是正确的,但是对于线程下载一堆 url 的有效性是错误的。
    • @gnibbler,我从来没有说过它没有有效。我只是声称它不值得与之相关的众多陷阱和警告,大多数答案很容易掩盖或忽略。
    【解决方案5】:

    大多数答案都集中在同时从不同服务器获取多个页面 (线程)但不重用已经打开的 HTTP 连接。如果 OP 向同一服务器/站点发出多个请求。

    在 urllib2 中,每个请求都会创建一个单独的连接,这会影响性能,从而降低页面获取速度。 urllib3 通过使用连接池解决了这个问题。可以在这里阅读更多内容urllib3 [也是线程安全的]

    还有Requests一个使用urllib3的HTTP库

    这与线程结合应该会提高获取页面的速度

    【讨论】:

      【解决方案6】:

      自从发布此问题以来,似乎有更高级别的抽象可用,ThreadPoolExecutor

      https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example

      为方便起见,此处粘贴的示例:

      import concurrent.futures
      import urllib.request
      
      URLS = ['http://www.foxnews.com/',
              'http://www.cnn.com/',
              'http://europe.wsj.com/',
              'http://www.bbc.co.uk/',
              'http://some-made-up-domain.com/']
      
      # Retrieve a single page and report the url and contents
      def load_url(url, timeout):
          with urllib.request.urlopen(url, timeout=timeout) as conn:
              return conn.read()
      
      # We can use a with statement to ensure threads are cleaned up promptly
      with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
          # Start the load operations and mark each future with its URL
          future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
          for future in concurrent.futures.as_completed(future_to_url):
              url = future_to_url[future]
              try:
                  data = future.result()
              except Exception as exc:
                  print('%r generated an exception: %s' % (url, exc))
              else:
                  print('%r page is %d bytes' % (url, len(data)))
      

      还有map,我认为它使代码更容易:https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map

      【讨论】:

      • 你也可以使用multiprocessing.pool.ThreadPool,即使在 Python 2 上也可用。这里是code example
      • 您必须安装futures,才能在 Python 2 中使用它
      【解决方案7】:

      现在有一个出色的 Python 库可以为您执行此操作,称为 requests

      如果您想要基于线程的解决方案,请使用标准的请求 api;如果您想要基于非阻塞 IO 的解决方案,请使用异步 api(在后台使用 gevent)。

      【讨论】:

        【解决方案8】:

        Ray 提供了一种优雅的方式来执行此操作(在 Python 2 和 Python 3 中)。 Ray 是一个用于编写并行和分布式 Python 的库。

        只需使用 @ray.remote 装饰器定义 fetch 函数。然后你可以通过调用fetch.remote(url)在后台获取一个URL。

        import ray
        import sys
        
        ray.init()
        
        @ray.remote
        def fetch(url):
            if sys.version_info >= (3, 0):
                import urllib.request
                return urllib.request.urlopen(url).read()
            else:
                import urllib2
                return urllib2.urlopen(url).read()
        
        urls = ['https://en.wikipedia.org/wiki/Donald_Trump',
                'https://en.wikipedia.org/wiki/Barack_Obama',
                'https://en.wikipedia.org/wiki/George_W._Bush',
                'https://en.wikipedia.org/wiki/Bill_Clinton',
                'https://en.wikipedia.org/wiki/George_H._W._Bush']
        
        # Fetch the webpages in parallel.
        results = ray.get([fetch.remote(url) for url in urls])
        

        如果你也想并行处理网页,可以将处理代码直接放入fetch,也可以定义一个新的远程函数组合在一起。

        @ray.remote
        def process(html):
            tokens = html.split()
            return set(tokens)
        
        # Fetch and process the pages in parallel.
        results = []
        for url in urls:
            results.append(process.remote(fetch.remote(url)))
        results = ray.get(results)
        

        如果您想要获取的 URL 列表很长,您可能希望发出一些任务,然后按照它们完成的顺序处理它们。您可以使用ray.wait 执行此操作。

        urls = 100 * urls  # Pretend we have a long list of URLs.
        results = []
        
        in_progress_ids = []
        
        # Start pulling 10 URLs in parallel.
        for _ in range(10):
            url = urls.pop()
            in_progress_ids.append(fetch.remote(url))
        
        # Whenever one finishes, start fetching a new one.
        while len(in_progress_ids) > 0:
            # Get a result that has finished.
            [ready_id], in_progress_ids = ray.wait(in_progress_ids)
            results.append(ray.get(ready_id))
            # Start a new task.
            if len(urls) > 0:
                in_progress_ids.append(fetch.remote(urls.pop()))
        

        查看Ray documentation

        【讨论】:

        • 很遗憾,这不适用于 Windows,因为尚未发布对 Windows 的支持。
        【解决方案9】:

        获取网页显然需要一段时间,因为您没有访问任何本地内容。如果您有多个要访问,您可以使用threading 模块同时运行几个。

        这是一个非常粗略的例子

        import threading
        import urllib2
        import time
        
        urls = ['http://docs.python.org/library/threading.html',
                'http://docs.python.org/library/thread.html',
                'http://docs.python.org/library/multiprocessing.html',
                'http://docs.python.org/howto/urllib2.html']
        data1 = []
        data2 = []
        
        class PageFetch(threading.Thread):
            def __init__(self, url, datadump):
                self.url = url
                self.datadump = datadump
                threading.Thread.__init__(self)
            def run(self):
                page = urllib2.urlopen(self.url)
                self.datadump.append(page.read()) # don't do it like this.
        
        print "Starting threaded reads:"
        start = time.clock()
        for url in urls:
            PageFetch(url, data2).start()
        while len(data2) < len(urls): pass # don't do this either.
        print "...took %f seconds" % (time.clock() - start)
        
        print "Starting sequential reads:"
        start = time.clock()
        for url in urls:
            page = urllib2.urlopen(url)
            data1.append(page.read())
        print "...took %f seconds" % (time.clock() - start)
        
        for i,x in enumerate(data1):
            print len(data1[i]), len(data2[i])
        

        这是我运行它时的输出:

        Starting threaded reads:
        ...took 2.035579 seconds
        Starting sequential reads:
        ...took 4.307102 seconds
        73127 19923
        19923 59366
        361483 73127
        59366 361483
        

        通过附加到列表来从线程中获取数据可能是不明智的(队列会更好),但它说明存在差异。

        【讨论】:

        • 谢谢尼克!非常感谢:)
        • 请问为什么 self.datadump.append(page.read()) # 不这样做。不明智?
        • -1 用于建议线程。这是 IO 绑定的;线程在这里没用。
        • @Aaron Gallagher 为什么使用线程运行速度是原来的两倍?
        • 我从不否认您的代码可以在更短的时间内执行。问题在于,与使用异步 IO 相比,实现该目标的方法会产生不可持续、过于复杂的代码。
        【解决方案10】:

        这是一个标准库解决方案。它没有那么快,但它使用的内存比线程解决方案少。

        try:
            from http.client import HTTPConnection, HTTPSConnection
        except ImportError:
            from httplib import HTTPConnection, HTTPSConnection
        connections = []
        results = []
        
        for url in urls:
            scheme, _, host, path = url.split('/', 3)
            h = (HTTPConnection if scheme == 'http:' else HTTPSConnection)(host)
            h.request('GET', '/' + path)
            connections.append(h)
        for h in connections:
            results.append(h.getresponse().read())
        

        另外,如果您的大部分请求都发往同一主机,那么重用相同的 http 连接可能比并行处理更有帮助。

        【讨论】:

          【解决方案11】:

          单连接慢速识别请找Python网络基准脚本:

          """Python network test."""
          from socket import create_connection
          from time import time
          
          try:
              from urllib2 import urlopen
          except ImportError:
              from urllib.request import urlopen
          
          TIC = time()
          create_connection(('216.58.194.174', 80))
          print('Duration socket IP connection (s): {:.2f}'.format(time() - TIC))
          
          TIC = time()
          create_connection(('google.com', 80))
          print('Duration socket DNS connection (s): {:.2f}'.format(time() - TIC))
          
          TIC = time()
          urlopen('http://216.58.194.174')
          print('Duration urlopen IP connection (s): {:.2f}'.format(time() - TIC))
          
          TIC = time()
          urlopen('http://google.com')
          print('Duration urlopen DNS connection (s): {:.2f}'.format(time() - TIC))
          

          Python 3.6 的结果示例:

          Duration socket IP connection (s): 0.02
          Duration socket DNS connection (s): 75.51
          Duration urlopen IP connection (s): 75.88
          Duration urlopen DNS connection (s): 151.42
          

          Python 2.7.13 的结果非常相似。

          在这种情况下,很容易识别出 DNS 和 urlopen 缓慢。

          【讨论】:

            猜你喜欢
            • 2014-05-12
            • 2016-07-06
            • 2020-08-08
            • 1970-01-01
            • 1970-01-01
            • 2017-12-16
            • 1970-01-01
            • 2018-04-24
            • 1970-01-01
            相关资源
            最近更新 更多