• Python实现的异步代理爬虫及代理池2--正确实现并发


    相关博客:

    在啃完《流畅的Python》之后,发现我之前实现的proxypool是有问题的:它虽然使用了asyncio的,但却不是并发的,依旧是顺序的,所以运行的速度非常慢。在实现并发后,按照现有的5个规则爬取一次这5个代理网站目前用时不到3分钟,而之前仅爬取西祠就需要1个小时。github上的代码已更新。

    并发访问网站的例子

    下面就是一个并发访问proxypool中实现的服务器的例子,以这个例子来说明如何实现并发。

    import aiohttp
    import asyncio
    
    
    async def localserver(semaphore):
        async with semaphore:       
            async with aiohttp.ClientSession() as session:
                async with session.get('http://127.0.0.1:8088', timeout=5) as resp:
                    print('hello')
                await asyncio.sleep(3) # 模拟网络延迟
    
    async def coro():
        semaphore = asyncio.Semaphore(5) # 限制并发量为5
        to_get = [localserver(semaphore) for _ in range(20)] # 同时建立20个协程
        await asyncio.wait(to_get) # 等待所有协程结束
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(coro())
    print(result)
    loop.close()
    

    运行上面的代码,可以在终端看到每隔3秒就打印出5个"hello",下面是服务器的日志:

    2017-06-01 14:45:35,375  DEBUG                                 server started at http://127.0.0.1:8088...
    2017-06-01 14:45:44,851  DEBUG    127.0.0.1:35698       GET    requested index page
    2017-06-01 14:45:44,853  DEBUG    127.0.0.1:35700       GET    requested index page
    2017-06-01 14:45:44,855  DEBUG    127.0.0.1:35702       GET    requested index page
    2017-06-01 14:45:44,858  DEBUG    127.0.0.1:35704       GET    requested index page
    2017-06-01 14:45:44,876  DEBUG    127.0.0.1:35706       GET    requested index page
    2017-06-01 14:45:47,864  DEBUG    127.0.0.1:35710       GET    requested index page
    ......
    2017-06-01 14:45:50,912  DEBUG    127.0.0.1:35732       GET    requested index page
    2017-06-01 14:45:53,887  DEBUG    127.0.0.1:35734       GET    requested index page
    2017-06-01 14:45:53,919  DEBUG    127.0.0.1:35736       GET    requested index page
    2017-06-01 14:45:53,924  DEBUG    127.0.0.1:35738       GET    requested index page
    2017-06-01 14:45:53,925  DEBUG    127.0.0.1:35740       GET    requested index page
    2017-06-01 14:45:53,929  DEBUG    127.0.0.1:35742       GET    requested index page
    

    可以在 14:45:44 时有5个几乎同时到达的请求,之后间隔3秒会就会有5个并发请求到达,20个请求一共耗时9秒左右。
    并发访问网站一定要限流,这里是通过asyncio.Semaphore将并发请求数量控制在5个。

    通过上面的例子可以看出实现并发的关键就在于同时建立多个协程,然后通过asyncio.wait方法等待它们结束,各个协程之间的调度交给事件循环完成。

    改造 proxypool 以实现并发

    主要修改的是proxy_crawler.pyproxy_validator.py2个模块。

    并发地爬取

    因为每个网站的规则都不同,要实现并发爬取所有的代理网站,需要修改协程间传递的数据,为它们添加上各自对应的规则,这样最终页面解析函数就可以使用对应的规则来解析爬取到的页面内容了,使用一个命名元组来包装这2种数据:

    Result = namedtuple('Result', 'content rule')
    

    content字段是url和爬取到的页面,rule字段则是对应的规则。

    下面是支持并发的proxy_crawler的启动函数:

    async def start(self):
        to_crawl = [self._crawler(rule) for rule in self._rules] # 协程数等于规则数
        await asyncio.wait(to_crawl)
    

    现在可以并发地爬取所有的代理网站,而对于单个网站来说爬取过程依旧是顺序的(爬取页面的page_download函数的基本逻辑没变),因为爬取时没有使用代理,并发访问可能会被封IP。如果想要实现对单个代理网站的并发爬取,参考上面的例子也很容易实现。

    并发地验证

    之前实现的proxypool中最耗时的部分就是验证了,如果代理无效,需要等待其超时才能判断其无效,而免费的代理中绝大多数都是无效的,顺序验证就会非常耗时。
    下面是支持并发的proxy_validator的启动函数:

    async def start(self, proxies=None):
        if proxies is not None:
            to_validate = [self.validate_many(proxies) for _ in range(50)] # 建立 50 个协程,在爬取过程中验证代理
        else:
            proxies = await self._get_proxies()# 从代理池中获取若干代理,返回一个asyncio.Queue 对象
            to_validate = [self.validate_one(proxies) for _ in range(proxies.qsize())] # 协程数等于队列的长度,定期验证代理池中的代理
    
        await asyncio.wait(to_validate)
    

    这部分相较之前的版本变化较大,除了为了支持并发而做的修改外,还进行了一点优化,重用了验证代理的代码,现在爬取代理时的验证和对代理池中的代理的定期验证都使用相同的验证代码。

    防止日志阻塞事件循环

    因为默认日志是输出到文件的,而asyncio包目前没有提供异步文件系统API,为了不让日志的I/O操作阻塞事件循环,通过调用run_in_executor方法,把日志操作发给asyncio的事件循环背后维护着的ThreadPoolExecutor 对象执行。
    我定义了一个logger的代理,由于logger被托管到另一个线程中执行,会丢失当前的上下文信息,如果需要记录,可以使用traceback库获取它们并作为日志的msgexc_infostack_info都设置为False,这样就不需要修改现有的代码了:

    import logging
    import logging.config
    import yaml
    from pathlib import Path
    from functools import wraps
    
    
    PROJECT_ROOT = Path(__file__).parent
    
    def _log_async(func):
        """Send func to be executed by ThreadPoolExecutor of event loop."""
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            loop = asyncio.get_event_loop()
            return loop.run_in_executor(None, partial(func, *args, **kwargs)) # run_in_executor 本身不支持关键字参数,logger是有关键字参数(如 'extra')的,使用 'functools.partial'
    
        return wrapper
    
    
    class _LoggerAsync:
        """Logger's async proxy.
    
        Logging were executed in a thread pool executor to avoid blocking the event loop.
        """
    
        def __init__(self, *, is_server=False):
            logging.config.dictConfig(
                yaml.load(open(str(PROJECT_ROOT / 'logging.yaml'), 'r')))  # load config from YAML file
    
            if is_server:
                self._logger = logging.getLogger('server_logger')
            elif VERBOSE:
                self._logger = logging.getLogger('console_logger')  # output to both stdout and file
            else:
                self._logger = logging.getLogger('file_logger')
    
        def __getattr__(self, name):
            if hasattr(self._logger, name):
                return getattr(self._logger, name)
            else:
                msg = 'logger object has no attribute {!r}'
                raise AttributeError(msg.format(name))
    
        @_log_async
        def debug(self, msg, *args, **kwargs):
            self._logger.debug(msg, *args, exc_info=False, stack_info=False, **kwargs)
    
        @_log_async
        def info(self, msg, *args, **kwargs):
            self._logger.info(msg, *args, exc_info=False, stack_info=False, **kwargs)
    
        @_log_async
        def warning(self, msg, *args, **kwargs):
            self._logger.warning(msg, *args, exc_info=False, stack_info=False, **kwargs)
    
        @_log_async
        def error(self, msg, *args, **kwargs):
            self._logger.error(msg, *args, exc_info=False, stack_info=False, **kwargs)
    
        @_log_async
        def exception(self, msg, *args, exc_info=True, **kwargs):
            self._logger.exception(msg, *args, exc_info=False, stack_info=False, **kwargs) 
    
        @_log_async
        def critical(self, msg, *args, **kwargs):
            self._logger.critical(msg, *args, exc_info=False, stack_info=False, **kwargs)
    
    logger = _LoggerAsync()
    
  • 相关阅读:
    105个软件测试工具大放送
    2016年开源巨献:来自百度的71款开源项目
    开源代码:Http请求封装类库HttpLib介绍、使用说明
    C#的HTTP开发包 HttpLib
    dropzonejs中文翻译手册 DropzoneJS是一个提供文件拖拽上传并且提供图片预览的开源类库.
    Windows平台分布式架构实践
    Windows平台下利用APM来做负载均衡方案
    C# .net dotnet属性定义属性,以提供显示明称,默认值
    细说ASP.NET Forms身份认证
    IIS 7.5 Application Warm-Up Module
  • 原文地址:https://www.cnblogs.com/xmwd/p/python_asyncio_proxy_crawler_and_proxy_pool_2.html
Copyright © 2020-2023  润新知