美文网首页Python爬虫从入门到放弃
python爬虫从入门到放弃之十二:多协程

python爬虫从入门到放弃之十二:多协程

作者: 52d19f475fe5 | 来源:发表于2019-07-23 00:13 被阅读5次

    一个爬虫,每发起一个请求,都要等服务器返回响应后,才会执行下一步。而很多时候,由于网络不稳定,加上服务器自身也需要响应的时间,导致爬虫会浪费大量时间在等待上。这也是爬取大量数据时,爬虫的速度会比较慢的原因。

    既然一个爬虫爬取大量数据要爬很久,那我们能不能让多个爬虫一起爬取?

    • 同步、异步

    同步:就是一个任务结束才能启动下一个

    异步:在一个任务未完成时,就可以执行其他多个任务,彼此不受影响

    显然,异步执行任务会比同步更加节省时间,因为它能减少不必要的等待。

    • 多协程

    一种非抢占式的异步方式,叫多协程(在此,多是多个的意思)

    它的原理是:一个任务在执行过程中,如果遇到等待,就先去执行其他的任务,当等待结束,再回来继续之前的那个任务。在计算机的世界,这种任务来回切换得非常快速,看上去就像多个任务在被同时执行一样。

    • 多协程的用法:gevent库
    • 安装方法:
      pip install gevent(Win)
      pip3 install gevent(Mac)

    爬取8个网站(包括百度、新浪、搜狐、腾讯、网易、爱奇艺、天猫、凤凰)

    我们先用之前同步的爬虫方式爬取这8个网站,然后等下再和gevent异步爬取做一个对比。

    • 同步的爬虫方式
    import requests,time
    
    start = time.time()
    
    url_list = ['https://www.baidu.com/',
    'https://www.sina.com.cn/',
    'http://www.sohu.com/',
    'https://www.qq.com/',
    'https://www.163.com/',
    'http://www.iqiyi.com/',
    'https://www.tmall.com/',
    'http://www.ifeng.com/']
    
    for url in url_list:
        r = requests.get(url)
        print(url,r.status_code)
    
    end = time.time()
    print(end-start)
    

    运行结果:

    https://www.baidu.com/ 200
    https://www.sina.com.cn/ 200
    http://www.sohu.com/ 200
    https://www.qq.com/ 200
    https://www.163.com/ 200
    http://www.iqiyi.com/ 200
    https://www.tmall.com/ 200
    http://www.ifeng.com/ 200
    1.0101947784423828
    
    • 异步的爬虫方式
    from gevent import monkey
    monkey.patch_all()
    import gevent,time,requests
    
    start = time.time()
    
    url_list = ['https://www.baidu.com/',
    'https://www.sina.com.cn/',
    'http://www.sohu.com/',
    'https://www.qq.com/',
    'https://www.163.com/',
    'http://www.iqiyi.com/',
    'https://www.tmall.com/',
    'http://www.ifeng.com/']
    
    def crawler(url):
        r = requests.get(url)
        print(url,time.time()-start,r.status_code)
    
    tasks_list = [gevent.spawn(crawler,url) for url in url_list]
    gevent.joinall(tasks_list)
    
    end = time.time()
    print(end-start)
    

    运行结果:

    http://www.iqiyi.com/ 0.16013050079345703 200
    http://www.ifeng.com/ 0.22873139381408691 200
    https://www.qq.com/ 0.2317953109741211 200
    https://www.baidu.com/ 0.26370882987976074 200
    https://www.163.com/ 0.2657487392425537 200
    https://www.sina.com.cn/ 0.287355899810791 200
    https://www.tmall.com/ 0.3268725872039795 200
    http://www.sohu.com/ 0.35901474952697754 200
    0.35901474952697754
    

    程序运行后,打印出了网址、每个请求运行的时间、状态码和爬取8个网站最终所用时间。

    通过每个请求运行的时间,我们能知道:爬虫用了异步的方式抓取了8个网站,因为每个请求完成的时间并不是按着顺序来的。

    且每个请求完成时间之间的间隔都非常短,可以看作这些请求几乎是“同时”发起的。

    通过对比同步和异步爬取最终所花的时间,用多协程异步的爬取方式,确实比同步的爬虫方式速度更快。

    其实,我们案例爬取的数据量还比较小,不能直接体现出更大的速度差异。如果爬的是大量的数据,运用多协程会有更显著的速度优势。

    现在,我们一行行来看刚刚用了gevent的代码。

    gevent.monkey模块,能将程序转换成可异步的程序,monkey.patch_all(),它的作用其实就像你的电脑有时会弹出“是否要用补丁修补漏洞或更新”一样。它能给程序打上补丁,让程序变成是异步模式,而不是同步模式。它也叫“猴子补丁”

    我们要在导入其他库和模块前,先把monkey模块导入进来,并运行monkey.patch_all()。这样,才能先给程序打上补丁。

    由于gevent只能处理gevent的任务对象,不能直接调用普通函数,所以需要借助gevent.spawn()来创建任务对象。gevent.spawn()的参数需为要调用的函数名及该函数的参数。比如,gevent.spawn(crawler,url)就是创建一个执行crawler函数的任务,参数为crawler函数名和它自身的参数url。

    最后,调用gevent库里的joinall方法,能启动执行所有的任务。gevent.joinall(tasks_list)就是执行tasks_list这个任务列表里的所有任务,开始爬取。

    • 那么问题来了?

    如果我们要爬的不是8个网站,而是1000个网站,我们可以怎么做?

    用我们刚刚的gevent语法,我们可以用gevent.spawn()创建1000个爬取任务,再用gevent.joinall()执行这1000个任务。

    但这种方法会有问题:执行1000个任务,就是一下子发起1000次请求,这样子的恶意请求,会拖垮网站的服务器。

    那我们能不能只创建成5个任务,但每个任务爬取200个网站?

    这么做也还是会有问题的。就算我们用gevent.spawn()创建了5个分别执行爬取200个网站的任务,这5个任务之间是异步执行的,但是每个任务(爬取200个网站)内部是同步的。

    这意味着:如果有一个任务在执行的过程中,它要爬取的一个网站一直在等待响应,哪怕其他任务都完成了200个网站的爬取,它也还是不能完成200个网站的爬取。



    这时我们可以从实际生活的案例中得到启发。想想银行是怎么在一天内办理1000个客户的业务的。

    银行会开设办理业务的多个窗口,让客户取号排队,由银行的叫号系统分配客户到不同的窗口去办理业务。

    在gevent库中,也有一个模块可以实现这种功能——queue模块。

    • queue模块

    queue翻译成中文是队列的意思。我们可以用queue模块来存储任务,让任务都变成一条整齐的队列,就像银行窗口的排号做法。因为queue其实是一种有序的数据结构,可以用来存取数据。

    这样,协程就可以从队列里把任务提取出来执行,直到队列空了,任务也就处理完了。就像银行窗口的工作人员会根据排号系统里的排号,处理客人的业务,如果已经没有新的排号,就意味着客户的业务都已办理完毕。

    代码实现:

    from gevent import monkey
    monkey.patch_all()
    import gevent,time,requests
    from gevent.queue import Queue
    
    
    start = time.time()
    
    url_list = ['https://www.baidu.com/',
            'https://www.sina.com.cn/',
            'http://www.sohu.com/',
            'https://www.qq.com/',
            'https://www.163.com/',
            'http://www.iqiyi.com/',
            'https://www.tmall.com/',
            'http://www.ifeng.com/']
    
    work = Queue()
    for url in url_list:
        work.put_nowait(url)
    
    
    def crawler():
        while not work.empty():
            url = work.get_nowait()
            r = requests.get(url)
            print(url,work.qsize(),r.status_code)
    
    tasks_list = [gevent.spawn(crawler) for x in range(2)]
    gevent.joinall(tasks_list)
    
    end = time.time()
    print(end-start)
    

    以上代码
    work = Queue()实例化一个队列,可以传入参数,如Queue(10)表示队列只能存储10个成员。
    work.put_nowait(url)表示:添加队列的成员
    while not work.empty():表示:当这个队列不为空时
    url = work.get_nowait()表示:取出队列的成员
    work.qsize()表示:队列的长度
    tasks_list = [gevent.spawn(crawler) for x in range(2)]相当于创建2个爬虫执行crawler()函数
    gevent.joinall(tasks_list)启动所有任务

    • queue对象的方法
    put_nowait()    # 往队列里存储数据
    get_nowait()    # 从队列里取出数据
    empty()         # 判断队列是否为空
    full()          # 判断队列是否为满
    qsize()         # 判断队列还剩多少数量
    



    实例展示:

    时光网TOP100链接:http://www.mtime.com/top/tv/top100/

    使用多协程和队列,爬取时光网电视剧TOP100的数据(剧名、导演、主演和简介),并用csv模块将数据存储下来。

    范例代码:

    from gevent import monkey
    monkey.patch_all()
    from gevent.queue import Queue 
    from lxml import etree
    import requests,csv,gevent
    
    work = Queue()
    work.put_nowait('http://www.mtime.com/top/tv/top100/')
    for n in range(2,11):
        url = 'http://www.mtime.com/top/tv/top100/index-%d.html'%n
        work.put_nowait(url)
    
    def info(list_name):
        if list_name==[]:
            return ''
        else:
            return list_name[0]
    
    def crawler():
        while not work.empty():
            url = work.get_nowait()
            headers = {'User-Agent': 'Mozilla/5.0'}
            r = requests.get(url,headers = headers)
            html = etree.HTML(r.text)
            tvs = html.xpath('//ul[@id="asyncRatingRegion"]/li')
            for tv in tvs:
                title = tv.xpath('./div[2]/a/@title')
                director = tv.xpath('./div[3]/p[1]/a/text()')
                actor1 = tv.xpath('./div[3]/p[2]/a[1]/text()')
                actor2 = tv.xpath('./div[3]/p[2]/a[2]/text()')
                introduction = tv.xpath('./div[3]/p[3]/text()')
    
                title = info(title)
                director = info(director)
                actors = info(actor1)+' '+info(actor2)
                introduction = info(introduction)
                print(title,director,actors,introduction)
                writer.writerow([title,director,actors,introduction])
                    
    f = open('tv.csv','a',newline = '',encoding = 'utf-8')
    writer = csv.writer(f)
    writer.writerow(['剧名','导演','主演','简介'])
    
    tasks_list = [gevent.spawn(crawler) for x in range(5)]    
    gevent.joinall(tasks_list)
    
    f.close()
    
    



    >>>阅读更多文章请点击以下链接:

    python爬虫从入门到放弃之一:认识爬虫
    python爬虫从入门到放弃之二:HTML基础
    python爬虫从入门到放弃之三:爬虫的基本流程
    python爬虫从入门到放弃之四:Requests库基础
    python爬虫从入门到放弃之五:Requests库高级用法
    python爬虫从入门到放弃之六:BeautifulSoup库
    python爬虫从入门到放弃之七:正则表达式
    python爬虫从入门到放弃之八:Xpath
    python爬虫从入门到放弃之九:Json解析
    python爬虫从入门到放弃之十:selenium库
    python爬虫从入门到放弃之十一:定时发送邮件
    python爬虫从入门到放弃之十二:多协程
    python爬虫从入门到放弃之十三:Scrapy概念和流程
    python爬虫从入门到放弃之十四:Scrapy入门使用

    相关文章

      网友评论

        本文标题:python爬虫从入门到放弃之十二:多协程

        本文链接:https://www.haomeiwen.com/subject/ffxglctx.html