强大的异步爬虫 with aiohttp

综合编程 2018-05-16 阅读原文

这是崔斯特的第四十七篇原创文章

异步了解下 (๑• . •๑)

看到现在网络上大多讲的都是requests、scrapy,却没有说到爬虫中的神器: aiohttp

aiohttp 介绍

aiohttp是什么,官网上有这样一句话介绍: Async HTTP client/server for asyncio and Python ,翻译过来就是 asyncio和Python的异步HTTP客户端/服务器

主要特点是:

  1. 支持客户端和HTTP服务器。
  2. 无需使用Callback Hell即可支持Server WebSockets和Client WebSockets。
  3. Web服务器具有中间件,信号和可插拔路由。

emmmm,好吧,还是来看代码吧

Client example:

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://httpbin.org/headers')
        print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

output:

{"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Connection":"close","Host":"httpbin.org","User-Agent":"Python/3.6 aiohttp/3.2.1"}}

Server example:

from aiohttp import web

async def handle(request):
    name = request.match_info.get('name', "Anonymous")
    text = "Hello, " + name
    return web.Response(text=text)

app = web.Application()
app.add_routes([web.get('/', handle),
                web.get('/{name}', handle)])

web.run_app(app)

output:

======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)

aiohttp 与 requests

去翻一下官方文档 Client Quickstart ,让我感觉非常熟悉,很多用法和requests相似。

async with aiohttp.ClientSession() as session:
    async with session.get('http://httpbin.org/get') as resp:
        print(resp.status)
        print(await resp.text())

首先,官方推荐使用 ClientSession 来管理会话,这不就是 requests 中的 session 吗?用法也类似,使用 session.get() 去发送 get 请求,返回的 resp 中就有我们所需要的数据了,用法也和 requests 一样, text() 文本, .json() 直接打印返回的 json 数据, headers 什么的也一样,更多内容参考官方文档 Response object

既然已经有 requests 了,那为什么还要说 aiohttp 了?重点来了, aiohttp 是异步的。在python3.5中,加入了 asyncio/await 关键字,使得回调的写法更加直观和人性化。而 aiohttp 是一个提供异步web服务的库, asyncio 可以实现单线程并发IO操作。

requests 写爬虫是同步的,是等待网页下载好才会执行下面的解析、入库操作,如果在下载网页时间太长会导致阻塞,使用 multiprocessing 或者 threading 加速爬虫也是一种方法。

我们现在使用的 aiohttp 是异步的,简单来说,就是不需要等待,你尽管去下载网页就好了,我不用傻傻的等待你完成才进行下一步,我还有别的活要干。这样就极大的提高了下载网页的效率。

另外, Scrapy 也是异步的,是基于Twisted事件驱动的。在任何情况下,都不要写阻塞的代码。阻塞的代码包括:

  1. 访问文件、数据库或者Web
  2. 产生新的进程并需要处理新进程的输出,如运行shell命令
  3. 执行系统层次操作的代码,如等待系统队列

代码实例

这里是使用 aiohttp 的一个爬虫实例

import asyncio

import aiohttp
from bs4 import BeautifulSoup

import logging

class AsnycGrab(object):

    def __init__(self, url_list, max_threads):

        self.urls = url_list
        self.results = {}
        self.max_threads = max_threads

    def __parse_results(self, url, html):

        try:
            soup = BeautifulSoup(html, 'html.parser')
            title = soup.find('title').get_text()
        except Exception as e:
            raise e

        if title:
            self.results[url] = title

    async def get_body(self, url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url, timeout=30) as response:
                assert response.status == 200
                html = await response.read()
                return response.url, html

    async def get_results(self, url):
        url, html = await self.get_body(url)
        self.__parse_results(url, html)
        return 'Completed'

    async def handle_tasks(self, task_id, work_queue):
        while not work_queue.empty():
            current_url = await work_queue.get()
            try:
                task_status = await self.get_results(current_url)
            except Exception as e:
                logging.exception('Error for {}'.format(current_url), exc_info=True)

    def eventloop(self):
        q = asyncio.Queue()
        [q.put_nowait(url) for url in self.urls]
        loop = asyncio.get_event_loop()
        tasks = [self.handle_tasks(task_id, q, ) for task_id in range(self.max_threads)]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()


if __name__ == '__main__':
    async_example = AsnycGrab(['http://edmundmartin.com',
               'https://www.udemy.com',
               'https://github.com/',
               'https://zhangslob.github.io/',
               'https://www.zhihu.com/'], 5)
    async_example.eventloop()
    print(async_example.results)

需要注意的是,你需要时刻在你的代码中使用异步操作,你如果在代码中使用同步操作,爬虫并不会报错,但是速度可能会受影响。

其他异步库

因为爬虫不仅仅只有下载这块,还会有操作数据库,这里提供两个异步库: aioredismotor

import asyncio
import aioredis

loop = asyncio.get_event_loop()

async def go():
    conn = await aioredis.create_connection(
        'redis://localhost', loop=loop)
    await conn.execute('set', 'my-key', 'value')
    val = await conn.execute('get', 'my-key')
    print(val)
    conn.close()
    await conn.wait_closed()
loop.run_until_complete(go())
# will print 'value'

文档: aioredis

import motor.motor_asyncio

client = motor.motor_asyncio.AsyncIOMotorClient('mongodb://localhost:27017')

db = client['test_database']
collection = db['test_collection']

async def do_insert():
    document = {'key': 'value'}
    result = await db.test_collection.insert_one(document)
    print('result %s' % repr(result.inserted_id))
    
async def do_find_one():
    document = await db.test_collection.find_one({'i': {'$lt': 1}})
    pprint.pprint(document)

文档: motor

本文仅仅介绍了 aiohttp 作为 Client 的用法, 有兴趣的朋友可以去研究下作为 Server 的用法,同样很强大。

责编内容by:崔斯特的博客 【阅读原文】。感谢您的支持!

您可能感兴趣的

Demystifying Microservices – A Fully Functional Pr... Recently, I realized that I wasn't as hands-on with microservices as I needed...
20180818_ARTS_week08 本周 ARTS,做了算法题 String to Integer (atoi),看了一篇介绍 BEM 命名方式的文章,Tip 记录了 css 属性 pointer...
3.1.2 选择结构的几种形式 选择结构通过判断某些特定条件是否满足来决定下一步的执行流程,是非常重要的控制结构。常见的有单分支选择结构、双分支选择结构、多分支选择结构以及嵌套的分支结构,形式...
On the uses and misuses of panics in Go Go has a unique approach to error handling, with a combination of explicit erro...
Python渗透测试框架:PytheM PytheM是一个Python渗透测试框架。它只能在osnGNU/Linux OS系统上运行。 安装 $sudo apt-get update $...