文章

Python爬虫基础之广西人才网的信息爬取(2) - 异步爬虫

经过之前的爬取,已经了解了网页的爬取流程,现在来了解异步爬取

为什么要进行异步爬取

上一篇中,我们的代码是从上往下写的,逻辑比较清晰,先获取到每一页岗位的url,然后在for循环中获取每一页的岗位列表,再进行分析与统计,这样的代码虽然没有问题,但只是相当于一个人在飞快浏览网站内容然后做记录,我们的代码只是加速了这个过程,没有发挥爬虫的最大优势,如果能让程序变成很多人在同时帮我浏览并分析数据,那速度肯定能提高很多,带着这种想法,我们来试试用异步的方法执行爬取

了解需要用到的库

asynico库

这次我们需要用到的库主要有asyncioaiohttp两个库,其中asyncio库的官方文档简介如下

asyncio 是用来编写 并发 代码的库,使用 async/await 语法。 asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。 asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。

1.首先来了解asyncio,我们用下面这段代码来帮助我们理解

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
import asyncio
import time

time_format = '%Y-%m-%d %X'


async def printTime(taskname):
    print(f"{taskname}开始时间:{time.strftime('%X')}")
    await asyncio.sleep(10)


async def main():
    print(f"开始时间:{time.strftime('%X')}")
    task1 = asyncio.create_task(printTime("任务一"))
    task2 = asyncio.create_task(printTime("任务二"))
    task3 = asyncio.create_task(printTime("任务三"))
    await task1
    task4 = asyncio.create_task(printTime("任务四"))
    await task2
    await task3
    await task4
    print(f"结束时间:{time.strftime('%X')}")

# 输出结果
# 开始时间:18:10:39
# 任务一开始时间:18:10:39
# 任务二开始时间:18:10:39
# 任务三开始时间:18:10:39
# 任务四开始时间:18:10:49
# 结束时间:18:10:59

asyncio.run(main())

可以看到,我们定义了一个函数printTime(),函数功能是输出当前任务的开始时间并等待十秒,从输出的结果可以看到,前面三个任务被同时开始了,第四个任务在他们开始的十秒之后也开始了,也就是说在这三个任务同时完成并且结束之后,任务四开始执行

如何来理解这样的运行结果呢,通过分析代码中的关键字和asyncio库的用法就可以很容易明白:

首先,在要运行的函数前面使用async来进行声明,可以将其指定为协程协程不能直接调用并执行,在程序的入口部分,我们用asyncio.run()来异步执行被标记为协程main(),在内部,使用await来对可等待对象,可等待对象包括协程任务Futures

每当程序运行到用await来进行等待的地方,会立即并发执行当前所有被创建的任务协程,所以其实上面的代码可以简写,只要任务被创建,就在等待运行,使用一次await进行等待,所有任务就会并发执行,我们调整之前代码中的main()函数,可以看到输出结果发生了改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async def main():
    print(f"开始时间:{time.strftime('%X')}")
    asyncio.create_task(printTime("任务一"))
    asyncio.create_task(printTime("任务二"))
    asyncio.create_task(printTime("任务三"))
    await printTime("任务四")
    print(f"结束时间:{time.strftime('%X')}")

# 输出结果
# 开始时间:18:10:39
# 任务四开始时间:18:10:39
# 任务一开始时间:18:10:39
# 任务二开始时间:18:10:39
# 任务三开始时间:18:10:39
# 结束时间:18:10:49

当使用await时,马上调用任务四,并且之前队列中被创建的任务也全部异步开始,所以会出现这样的输出结果

aiohttp库

关于aiohttp库,其主要是配合asycio使用,达到异步请求的目的,官方示例已经用比较直观的代码介绍了用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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://python.org')
        print(html)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

这里main()函数的调用可以不用太在意,是另一种异步启动协程的办法,这里用我们之前的写法asyncio.run(main())也是一样的效果,aiohttp库内的函数支持协程的特性,让其可以很好和异步编程配合

代码中使用aiohttp.ClientSession()创建了一个session对象,对于这个对象,官方文档这样说:

不要为每个请求都创建一个会话。大多数情况下每个应用程序只需要一个会话就可以执行所有的请求。 每个会话对象都包含一个连接池,可复用的连接和持久连接状态(keep-alives,这两个是默认的)可提升总体的执行效率。

可以理解成,通过反复使用创建的session对象并keep-alives,就能在单次会话中提高整体请求页面的速率,因为不用在每次请求时再建立新连接

开始编写代码

在了解了这两个库之后,我们对之前做的爬虫代码进行改善,这里直接上代码:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
from bs4 import BeautifulSoup
import urllib.request
import aiohttp
import asyncio
from collections import OrderedDict
import time

# IT类工作地址
listTypes = ['5480', '5484']
jobsNum = []
jobList = ["c#/.net", "java", "php", "web"]
dicResult = OrderedDict()
urls = []

# 伪装浏览器头部
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                  'AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/81.0.4044.138 Safari/537.36'
}

# 获取网页(文本信息)


async def fetch(session, url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
    }
    async with session.get(url, headers=headers) as response:
        return await response.text()


def getData(page):
    soup = BeautifulSoup(page, 'lxml')
    searchResultPage = soup.find_all(name='div', attrs='rlOne')
    bolTypeRight = False
    for job in searchResultPage:
        tag_a = job.find('a')
        href = tag_a.get('href')
        company = job.find('li', 'w2').text
        salary = job.find('li', 'w3').text
        jobName = tag_a.text.lower()
        if href not in jobsNum:
            jobsNum.append(href)
            for jobType in jobList:
                if '-' in salary:
                    if '/' in jobType:
                        bolTypeRight = jobType.split(
                            '/')[0] in jobName or jobType.split('/')[1] in jobName
                    else:
                        bolTypeRight = jobType in jobName

                    if bolTypeRight:
                        if jobType not in dicResult.keys():
                            dicResult[jobType] = [int(salary.split('-')[0])]
                        else:
                            dicResult[jobType].append(
                                int(salary.split('-')[0]))

            print(company + " " + jobName + ":" + salary + " " + href)


# 处理网页
async def download(url):
    async with aiohttp.ClientSession() as session:
        page = await fetch(session, url)
        getData(page)

time_start = time.time()

for type_number in listTypes:
    url_prefix = 'https://s.gxrc.com/sJob?schType=1&expend=1&PosType=' + \
        type_number + '&page='

    # Request类的实例,构造时需要传入Url,Data,headers等等的内容
    request = urllib.request.Request(url=url_prefix + '1', headers=headers)
    first_page = urllib.request.urlopen(request)
    soup = BeautifulSoup(first_page, 'lxml')
    intLastPageNumber = int(soup.find('i', {"id": "pgInfo_last"}).text)
    urls.extend([url_prefix + str(i) for i in range(1, intLastPageNumber + 1)])


async def main():
    tasks = []
    for url in urls:
        tasks.append(asyncio.create_task(download(url)))

    for task in tasks:
        await task

    print('广西人才网IT岗统计结果(按岗位标题)')
    print('总岗位数量:' + str(len(jobsNum)))
    for resultKey, value in dicResult.items():
        print(resultKey + '岗位总数量:' + str(len(value)) +
              ',平均工资(按岗位最低工资为准):' + str(sum(value) / len(value)))

    time_end = time.time()
    print('time cost', time_end-time_start, 's')

asyncio.run(main())

值得注意的是,对于其中的这一段代码:

1
2
3
4
5
6
7
async def main():
    tasks = []
    for url in urls:
        tasks.append(asyncio.create_task(download(url)))

    for task in tasks:
        await task

可以写成:

1
2
3
4
5
6
async def main():
    tasks = []
    for url in urls:
        tasks.append(asyncio.create_task(download(url)))

    await asyncio.gather(*tasks)

这里不需要太过在意,把asyncio.gather(*tasks)起到的作用当作和第一种写法实现的相同即可,减少代码量,并发运行所有列表中的协程

但是如果写成:

1
2
3
4
5
    tasks = []
    for url in urls:
        tasks.append(asyncio.create_task(download(url)))

    await(tasks[0])

发现程序运行到一半就停止了,只处理了一部分url,这是因为,虽然使用await时所有任务是会同时并发运行,但是我这里只等待了第一项,所以当第一条url完全处理完时,之后程序将不会再等待剩余协程的情况

对比非异步的版本

终于,我们的异步爬虫编写好了,我们在之前编写的代码和现在编写的代码文件中import time,在循环开始前加上time_start = time.time(),在统计完成后加上

1
2
time_end=time.time()
print('time cost',time_end-time_start,'s')

通过对比两次的结果可以看到,爬虫的速度从90s左右变成了20s左右,提升非常巨大,由此可以可以看到,协程配合aiohttp提供的session,可以让爬虫的效率有质的飞跃!

本文由作者按照 CC BY 4.0 进行授权