异步爬虫不同于多进程爬虫,它使用单线程(即仅创建一个事件循环,然后把所有任务添加到事件循环中)就能并发处理多任务。在轮询到某个任务后,当遇到耗时操作(如请求URL)时,挂起该任务并进行下一个任务,当之前被挂起的任务更新了状态(如获得了网页响应),则被唤醒,程序继续从上次挂起的地方运行下去。极大的减少了中间不必要的等待时间。

有了Asyncio异步IO库实现协程后,我们还需要实现异步网页请求。普通的爬虫程序会经常用到requests库用以请求网页并获得服务器响应。而在协程中,由于requests库提供的相关方法不是可等待对象(awaitable),使得无法放在await后面,因此无法使用requests库在协程程序中实现请求。

官方专门提供了一个aiohttp库,用来实现异步网页请求等功能,简直就是异步版的requests库。在协程中使用ClientSession()get()request()方法来请求网页(其中async with是异步上下文管理器,其封装了异步实现等功能),完整的aiohttp使用方法,请见官方文档

1
2
3
4
5
import aiohttp
async with aiohttp.ClientSession() as session:
    async with session.get('http://httpbin.org/get') as resp:
        print(resp.status)
        print(await resp.text())

官方API提供的HTTP常见方法:

1
2
3
4
5
6
session.post('http://httpbin.org/post', data=b'data')
session.put('http://httpbin.org/put', data=b'data')
session.delete('http://httpbin.org/delete')
session.head('http://httpbin.org/get')
session.options('http://httpbin.org/get')
session.patch('http://httpbin.org/patch', data=b'data')

实例:获取各主要集团招投标平台的标题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from bs4 import BeautifulSoup as bs
import requests
import time
import aiohttp
import asyncio

allBids = {
    '采购与招标网': 'https://www.chinabidding.cn/',
    '招投标平台': 'http://bulletin.cebpubservice.com/',
    '华能招标网': 'http://ec.chng.com.cn/ecmall/',
    '神华招标网': 'http://www.shenhuabidding.com.cn/bidweb/',
    '中广核招标网': 'https://ecp.cgnpc.com.cn/',
    '三峡招标网': 'http://epp.ctg.com.cn/',
    '大唐招标网': 'http://www.cdt-ec.com/home/',
    '守正招标网': 'https://szecp.crc.com.cn/',
    '国电投招标网': 'http://www.cpeinet.com.cn/',
    '国电招标网': 'http://www.cgdcbidding.com/',
    '华电招标网': 'https://www.chdtp.com/',
    '河北招标网': 'http://hebeibidding.com/TPFront/default.aspx',
    '协合招标网': 'http://www.cnegroup.com/'
}
headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}
sem = asyncio.Semaphore(5)  # 限制同时运行协程数量

# 按顺序获取标题
def get_title_normal():
    session = requests.session()
    for name, url in allBids.items():
        html = session.get(url, headers=headers)
        html.encoding = 'utf-8'
        title = bs(html.text, 'lxml').find('title').text
        print(title)

# 协程异步获取标题
async def get_title_asyn(url):
    with (await sem):
        async with aiohttp.ClientSession() as session:  # 获取session
            async with session.get(url, headers=headers) as resp:  # 提出请求
                # 断言,判断网站状态
                assert resp.status == 200
                html = await resp.text()
                soup = bs(html, 'lxml')
                title = soup.find('title').text
                print(''.join(title))


def loops():
    loop = asyncio.get_event_loop()  # 获取事件循环
    # 把所有任务放到一个列表中
    tasks = [get_title_asyn(url) for name, url in allBids.items()]
    loop.run_until_complete(asyncio.wait(tasks))  # 激活协程
    loop.close()  # 关闭事件循环


if __name__ == '__main__':
    print(time.strftime('%Y-%m-%d %H:%M:%S'))
    start = time.time()
    # get_title_normal()
    loops()
    print('总耗时: %.2f秒' % float(time.time()-start))