zoukankan      html  css  js  c++  java
  • 爬虫性能相关

    一、背景

      爬虫的本质就是一个socket客户端与服务端的通信过程,如果我们有多个url待爬取,采用串行的方式执行,只能等待爬取一个结束后才能继续下一个,效率会非常低。

      注意:串行并不意味着抵消,如果串行的都是纯计算的任务,那么cpu的利用率仍然会很高,之所以爬虫程序的串行低效,是因为爬虫程序是明显的IO密集型程序。

      那么该如何提高爬取性能呢?

    二、同步、异步、回调机制

    1、同步调用:即提交一个任务后就在原地等待任务结束,拿到任务的结果后再继续执行下一行代码,效率低下

    import requests
    
    def get_page(url):
        response=requests.get(url)
        if response.status_code == 200:
            return response.text
    
    
    urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
    for url in urls:
        res=get_page(url) #调用一个任务,就在原地等待任务结束拿到结果后才继续往后执行
        print(len(res))
    
    同步调用
    同步调用

    2、简单的解决方案:多线程或多进程

    #在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
    from multiprocessing import Process
    from threading import Thread
    import requests
    
    def get_page(url):
        response=requests.get(url)
        if response.status_code == 200:
            return response.text
    
    
    if __name__ == '__main__':
        urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
        for url in urls:
            p=Process(target=get_page,args=(url,))
            p.start()
            # t=Thread(target=get_page,args=(url,))
            # t.start()
    多线程或多进程

    该方案的问题是:

      我们是无法无限制地开启多线程或多进程的,在遇到要同时响应成百上千个的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程半壶纱呢也更容易进入假死状态。

    3、改进方案:线程池或进程池+异步调用:提交一个任务后并不会等待任务结束,而是接续下一行代码

      很多人可能会考虑使用“线程池”或“连接池”,“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池。尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    import requests
    
    def get_page(url):
        print('GET : %s' %url)
        response=requests.get(url)
        if response.status_code == 200:
            return response.text
    
    
    if __name__ == '__main__':
        p=ProcessPoolExecutor()
        # p=ThreadPoolExecutor()
    
        urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
        for url in urls:
            p.submit(get_page,url)
        p.shutdown(wait=True)
    进程池或线程池
    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    import requests
    import os
    
    def get_page(url):
        print('%s GET : %s' %(os.getpid(),url))
        response=requests.get(url)
        if response.status_code == 200:
            return response.text
    
    def parse_page(res):
        res=res.result()
        print('%s parsing' %os.getpid())
    
    if __name__ == '__main__':
        p=ProcessPoolExecutor()
        # p=ThreadPoolExecutor()
    
        urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
        for url in urls:
            p.submit(get_page,url).add_done_callback(parse_page)
        p.shutdown(wait=True)
    
    异步调用+回调机制
    异步调用+回调机制

    改进后方案其实也存在问题:

      “线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上线,当请求大大超过上限是,“池”构成的系统对外界的英雄并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

    三、高性能

      上述无论哪种解决方案其实都没有解决一个性能相关的问题:IO阻塞,无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,程序的执行效率因此就降低了下来。

      解决这一问题的关键在于,我们自己从应用程序级别检测IO阻塞然后切换到我们自己程序的其他任务执行,这样把我们程序的IO降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO比较少的程序。从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的。

      1.在python3.3之后新增了asyncio模块,可以帮我们检测IO(只能是网络IO),实现应用程序级别的切换

    import asyncio
    
    @asyncio.coroutine
    def task(task_id,senconds):
        print('%s is start' %task_id)
        yield from asyncio.sleep(senconds) #只能检测网络IO,检测到IO后切换到其他任务执行
        print('%s is end' %task_id)
    
    tasks=[task(task_id=1,senconds=3),task(task_id=2,senconds=4)]
    
    loop=asyncio.get_event_loop()
    loop.run_until_complete(asyncio.gather(*tasks))
    loop.close()
    基本使用

      2.但asyncio模块只能发tcp级别的请求,不能发http协议,因此,在我们需要发送http请求的时候,需要我们自定义http报头

    #我们爬取一个网页的过程,以https://www.python.org/doc/为例,将关键步骤列举如下
    #步骤一:向www.python.org这台主机发送tcp三次握手,是IO阻塞操作
    #步骤二:封装http协议的报头
    #步骤三:发送http协议的请求包,是IO阻塞操作
    #步骤四:接收http协议的响应包,是IO阻塞操作
    import asyncio
    
    @asyncio.coroutine
    def get_page(host,port=80,url='/'):
        #步骤一(IO阻塞):发起tcp链接,是阻塞操作,因此需要yield from
        recv,send=yield from asyncio.open_connection(host,port)
    
        #步骤二:封装http协议的报头,因为asyncio模块只能封装并发送tcp包,因此这一步需要我们自己封装http协议的包
        requset_headers="""GET %s HTTP/1.0
    Host: %s
    
    """ % (url, host,)
        # requset_headers="""POST %s HTTP/1.0
    Host: %s
    
    name=egon&password=123""" % (url, host,)
        requset_headers=requset_headers.encode('utf-8')
    
        #步骤三(IO阻塞):发送http请求包
        send.write(requset_headers)
        yield from send.drain()
    
        #步骤四(IO阻塞):接收http协议的响应包
        text=yield from recv.read()
    
        #其他处理
        print(host,url,text)
        send.close()
        print('-===>')
        return 1
    
    tasks=[get_page(host='www.python.org',url='/doc'),get_page(host='www.cnblogs.com',url='linhaifeng'),get_page(host='www.openstack.org')]
    
    loop=asyncio.get_event_loop()
    results=loop.run_until_complete(asyncio.gather(*tasks))
    loop.close()
    
    print('=====>',results) #[1, 1, 1]
    asyncio+自定义http协议报头

      3.由于自定义http有点麻烦,浴室有了aiohttp模块,专门帮我们封装http报头,然后我们还需要用asyncio检测IO实现切换

    import aiohttp
    import asyncio
    
    @asyncio.coroutine
    def get_page(url):
        print('GET:%s' %url)
        response=yield from aiohttp.request('GET',url)
    
        data=yield from response.read()
    
        print(url,data)
        response.close()
        return 1
    
    tasks=[
        get_page('https://www.python.org/doc'),
        get_page('https://www.cnblogs.com/linhaifeng'),
        get_page('https://www.openstack.org')
    ]
    
    loop=asyncio.get_event_loop()
    results=loop.run_until_complete(asyncio.gather(*tasks))
    loop.close()
    
    print('=====>',results) #[1, 1, 1]
    asyncio+aiohttp

      4.此外,还可以将requests.get函数传给asyncio,就能够被检测了

    import requests
    import asyncio
    
    @asyncio.coroutine
    def get_page(func,*args):
        print('GET:%s' %args[0])
        loog=asyncio.get_event_loop()
        furture=loop.run_in_executor(None,func,*args)
        response=yield from furture
    
        print(response.url,len(response.text))
        return 1
    
    tasks=[
        get_page(requests.get,'https://www.python.org/doc'),
        get_page(requests.get,'https://www.cnblogs.com/linhaifeng'),
        get_page(requests.get,'https://www.openstack.org')
    ]
    
    loop=asyncio.get_event_loop()
    results=loop.run_until_complete(asyncio.gather(*tasks))
    loop.close()
    
    print('=====>',results) #[1, 1, 1]
    
    asyncio+requests模块的方法
    asyncio+requests模块的方法

      5.还有之前在协程时介绍的gevent模块

    from gevent import monkey;monkey.patch_all()
    import gevent
    import requests
    
    def get_page(url):
        print('GET:%s' %url)
        response=requests.get(url)
        print(url,len(response.text))
        return 1
    
    # g1=gevent.spawn(get_page,'https://www.python.org/doc')
    # g2=gevent.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
    # g3=gevent.spawn(get_page,'https://www.openstack.org')
    # gevent.joinall([g1,g2,g3,])
    # print(g1.value,g2.value,g3.value) #拿到返回值
    
    
    #协程池
    from gevent.pool import Pool
    pool=Pool(2)
    g1=pool.spawn(get_page,'https://www.python.org/doc')
    g2=pool.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
    g3=pool.spawn(get_page,'https://www.openstack.org')
    gevent.joinall([g1,g2,g3,])
    print(g1.value,g2.value,g3.value) #拿到返回值
    
    gevent+requests
    gevent+requests

      6.封装了gevent+requests模块的grequests模块

    #pip3 install grequests
    
    import grequests
    
    request_list=[
        grequests.get('https://wwww.xxxx.org/doc1'),
        grequests.get('https://www.cnblogs.com/linhaifeng'),
        grequests.get('https://www.openstack.org')
    ]
    
    
    ##### 执行并获取响应列表 #####
    # response_list = grequests.map(request_list)
    # print(response_list)
    
    ##### 执行并获取响应列表(处理异常) #####
    def exception_handler(request, exception):
        # print(request,exception)
        print("%s Request failed" %request.url)
    
    response_list = grequests.map(request_list, exception_handler=exception_handler)
    print(response_list)
    
    grequests
    grequests

      7.twisted:是一个网络框架,其中一个功能是发送异步请求,检测IO并自动切换

    '''
    #问题一:error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
    https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    pip3 install C:UsersAdministratorDownloadsTwisted-17.9.0-cp36-cp36m-win_amd64.whl
    pip3 install twisted
    
    #问题二:ModuleNotFoundError: No module named 'win32api'
    https://sourceforge.net/projects/pywin32/files/pywin32/
    
    #问题三:openssl
    pip3 install pyopenssl
    '''
    
    #twisted基本用法
    from twisted.web.client import getPage,defer
    from twisted.internet import reactor
    
    def all_done(arg):
        # print(arg)
        reactor.stop()
    
    def callback(res):
        print(res)
        return 1
    
    defer_list=[]
    urls=[
        'http://www.baidu.com',
        'http://www.bing.com',
        'https://www.python.org',
    ]
    for url in urls:
        obj=getPage(url.encode('utf=-8'),)
        obj.addCallback(callback)
        defer_list.append(obj)
    
    defer.DeferredList(defer_list).addBoth(all_done)
    
    reactor.run()
    
    
    
    
    #twisted的getPage的详细用法
    from twisted.internet import reactor
    from twisted.web.client import getPage
    import urllib.parse
    
    
    def one_done(arg):
        print(arg)
        reactor.stop()
    
    post_data = urllib.parse.urlencode({'check_data': 'adf'})
    post_data = bytes(post_data, encoding='utf8')
    headers = {b'Content-Type': b'application/x-www-form-urlencoded'}
    response = getPage(bytes('http://dig.chouti.com/login', encoding='utf8'),
                       method=bytes('POST', encoding='utf8'),
                       postdata=post_data,
                       cookies={},
                       headers=headers)
    response.addBoth(one_done)
    
    reactor.run()
    
    twisted的用法
    twisted的用法

      8.tornado

    from tornado.httpclient import AsyncHTTPClient
    from tornado.httpclient import HTTPRequest
    from tornado import ioloop
    
    
    def handle_response(response):
        """
        处理返回值内容(需要维护计数器,来停止IO循环),调用 ioloop.IOLoop.current().stop()
        :param response: 
        :return: 
        """
        if response.error:
            print("Error:", response.error)
        else:
            print(response.body)
    
    
    def func():
        url_list = [
            'http://www.baidu.com',
            'http://www.bing.com',
        ]
        for url in url_list:
            print(url)
            http_client = AsyncHTTPClient()
            http_client.fetch(HTTPRequest(url), handle_response)
    
    
    ioloop.IOLoop.current().add_callback(func)
    ioloop.IOLoop.current().start()
    
    Tornado
    tornado
  • 相关阅读:
    IO和序列化
    委托与事件
    [基础不过关填坑] 跨iframe触发事件
    有哪些新手程序员不知道的小技巧?
    给echarts加个“全屏展示”
    bootstrapTable使用场景及方式
    bootstrap datetimepicker 格式化yyyymmdd时,无法读取yyyymmdd格式
    【变态需求】bootstrapTable列排序-选择正序倒序不排序
    myeclipse、maven、tomcat、jdk技巧和坑【待完善】
    【调试技巧】 Fiddler高级用法之url映射请求
  • 原文地址:https://www.cnblogs.com/78pikaqiu/p/7811013.html
Copyright © 2011-2022 走看看