通过 Python2.7 中的多处理访问 url 列表

accessing a list of urls via Multi-processing in Python2.7

我一直在玩多处理,我的代码在查看较小的数字时有效,但当我想 运行 较大的样本时,发生了 2 件事:要么代码锁定,要么我收到以下错误消息: "urlopen error [Errno 10054] An existing connection was forcibly closed by the remote host" 。我不知道如何让它工作。谢谢

    from multiprocessing import cpu_count
    import urllib2
    from bs4 import BeautifulSoup
    import json
    import timeit
    import socket
    import errno

    def parseWeb(id):
        url = 'https://carhood.com.au/rent/car_detail/'+str(id)+'/'
        hdr = {'Accept': 'text/html,application/xhtml+xml,*/*',"user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36"}

        html = urllib2.urlopen(url).read()
        soup=BeautifulSoup(html,"lxml")
        car=soup.find("h1",{"class":"intro-title intro-title-tertiary"}).text

        return car

    if __name__ == '__main__':
        start = timeit.default_timer()
        pool = Pool(cpu_count()*100)
#This works for me when xrange(1,70)
        results=pool.map(parseWeb,xrange(1,400))
        print results
##I've tried this as a solution but it didn't work
        ##    startNum=1
##    endNum=470
##    for x in range(startNum,endNum,70):
##        print x
##        results=pool.map(parseWeb,xrange(startNum,x))
##        print results
##        startNum=x  
        stop = timeit.default_timer()
        print stop - start

John Zwinck 在问题评论中的建议很中肯。

部分问题是您无法控制接收服务器。当您放置过多的进程时,您会强制另一端的服务器找出一次处理所有请求的正确方法。这会导致您的进程闲置在那里等待服务器在某个时候返回它们 - 因为 pool.map() 仅在您的所有进程完成时完成(这是 阻塞 呼叫),这意味着只要服务器为它们中的每一个提供服务,您就可以等待。

现在一切都取决于服务器。

  • 服务器可以选择将其资源用于一个一个地处理您的所有请求 - 这实际上意味着您的请求现在正在 队列中等待 ,与您刚刚一个接一个地连续发送请求相比,没有任何优势。单线程服务器可以像这样建模,尽管它们的主要加速来自于它们是异步的并且在请求和请求之间快速跳转的事实。

  • 有些服务器通常有少量进程或线程,这些进程或线程会生成大量子线程,这些子线程都一个接一个地处理传入的请求 - 例如 Apache 服务器,starts off with 2 dedicated processes with 25 threads each ,因此理论上它可以处理 50 个并发请求,并且可以根据配置进行扩展。此时它将尽可能多地提供服务,并且要么搁置剩余的多余请求,要么拒绝为它们提供服务。

  • 有些服务器会在威胁系统过载或达到内部超时时简单地终止或关闭连接。后者的可能性更大,也更经常遇到。

另一方面,您自己的 CPU 内核无法处理您要求它们执行的操作。 内核可以处理 一个 线程一次——当我们谈到并行性时,我们实际上是在谈论多个内核同时处理一个线程。具有大量较小线程的进程可以将这些线程分布在不同的 CPU 内核中,因此您可以从中受益。

但是您有一百个进程,每个进程都会引发一个 阻塞 I/O 调用(urlopenblocking)。如果该 I/O 调用立即得到响应,那么到目前为止一切顺利 - 如果没有,现在其他进程正在等待此进程完成,占用宝贵的 CPU 核心。您已经成功地将 waiting 引入到您想要显式 avoid 等待的系统中。如果您将此问题与您在接收服务器上引起的压力结合起来,您会发现许多延迟源于打开的连接。

解决方案

有很多解决方案,但在我看来,它们都归结为同一件事:

  • 避免阻塞调用。使用触发请求的解决方案,将负责该请求的线程置于睡眠状态并离开调度程序 运行 队列,并在注册事件时将其唤醒。

  • 使用 异步性对您有利。单个线程可以在不阻塞的情况下发出多个请求,您只需要能够智能地处理一个接一个的响应。您甚至可以将响应传递给其他不做任何工作的线程(例如使用 Queue)。诀窍是让他们无缝地协同工作。

multiprocessing 虽然是处理进程的良好解决方案,但不是处理 HTTP 请求与进程适当行为之间交互的捆绑解决方案。这是您通常必须自己编写的逻辑,如果您能够更好地控制 urlopen 的工作方式,则 可以 完成 - 您必须想出一种方法来确保 urlopen 不会阻塞,或者至少愿意在发送请求后立即订阅事件通知。

当然,这一切都可以完成 - 但网络抓取是一个已解决的问题,无需重写轮子。

相反,有几个经过尝试和测试的选项:

  • asyncio is the standard as of Python 3.5. While not a full-fledged HTTP service, it offers asynchronous support for I/O bound operations. You can make HTTP requests using aiohttp. Here's a tutorial关于如何用同样的方法刮。

  • Scrapy is viable on Python 2.7 and Python 3. It uses Twisted, asyncio's non-standard fore-runner and the go-to tool for fast network requests. I mention Scrapy instead of Twisted simply because Scrapy has already taken care of the underlying architecture for you [which can be read about here] - 如果您愿意,您当然应该探索 Twisted 以了解底层系统。它是我将在这里提到的所有解决方案中最方便的,但根据我的经验,它也是性能最高的。

  • grequests is an extension of the popular requests library (which is incidentally superior to urllib2 and should be used at every opportunity) to support so-called coroutines: threads that can be suspended and resumed at multiple points in their execution, very ideal if you want the thread to do work while waiting for an I/O response. grequests builds on top of gevent(协程库)让您可以在单个线程中发出多个请求,并按照您自己的节奏处理它们。