tongsiying

阅读|运动|自律

0%

爬虫实战:线程协程性能对比

## 一、前言

今天我要给大家分享的是如何爬取中农网产品报价数据,并分别用普通的单线程、多线程和协程来爬取,从而对比单线程、多线程和协程在网络爬虫中的性能。

目标URLhttps://www.zhongnongwang.com/quote/product-htm-page-1.html

爬取产品品名、最新报价、单位、报价数、报价时间等信息,保存到本地Excel。

文档相关代码:git仓库

二、爬取测试

翻页查看 URL 变化规律:

1
2
3
4
5
6
https://www.zhongnongwang.com/quote/product-htm-page-1.html
https://www.zhongnongwang.com/quote/product-htm-page-2.html
https://www.zhongnongwang.com/quote/product-htm-page-3.html
https://www.zhongnongwang.com/quote/product-htm-page-4.html
https://www.zhongnongwang.com/quote/product-htm-page-5.html
https://www.zhongnongwang.com/quote/product-htm-page-6.html

检查网页,可以发现网页结构简单,容易解析和提取数据。

思路:每一条产品报价信息在 class 为 tb 的 table 标签下的 tbody 下的 tr 标签里,获取到所有 tr 标签的内容,然后遍历,从中提取出每一个产品品名、最新报价、单位、报价数、报价时间等信息。

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
import logging

import requests
from zyf.user_agent import UserAgent
from lxml import etree

# 日志输出的基本配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
# 随机产生请求头
ua = UserAgent()
url = 'https://www.zhongnongwang.com/quote/product-htm-page-1.html'
# 伪装请求头
headers = {
"Accept-Encoding": "gzip", # 使用gzip压缩传输数据让访问更快
"User-Agent": ua.random
}
# 发送请求 获取响应
rep = requests.get(url, headers=headers)
print(rep.status_code) # 200
# Xpath定位提取数据
html = etree.HTML(rep.text)
items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
logging.info(f'该页有多少条信息:{len(items)}') # 一页有20条信息
# 遍历提取出数据
for item in items:
name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
price = ''.join(item.xpath('.//td[3]/text()')) # 最新报价
unit = ''.join(item.xpath('.//td[4]/text()')) # 单位
nums = ''.join(item.xpath('.//td[5]/text()')) # 报价数
time_ = ''.join(item.xpath('.//td[6]/text()')) # 报价时间
logging.info([name, price, unit, nums, time_])

运行结果如下:

1
2
3
4
5
6
7
200
2021-03-14 11:47:42,781 - INFO: 该页有多少条信息:20
2021-03-14 11:47:42,782 - INFO: ['芥蓝种子', '¥6.20', '斤', '1', '2021-03-14 11:39']
2021-03-14 11:47:42,783 - INFO: ['鸡毛菜种子', '¥4.00', '斤', '1', '2021-03-14 11:39']
...
2021-03-14 11:47:42,800 - INFO: ['酸刀豆', '¥85.90', '斤', '1', '2021-03-14 11:39']
2021-03-14 11:47:42,802 - INFO: ['糟菜', '¥3.50', '斤', '1', '2021-03-14 11:39']

可以成功爬取到数据,接下来分别用普通的单线程、多线程和协程来爬取 50 页的数据、保存到Excel。

三、单线程爬虫

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
import requests
import logging
from zyf.user_agent import UserAgent
from lxml import etree
import openpyxl
from datetime import datetime

# 日志输出的基本配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
# 随机产生请求头
ua = UserAgent()
wb = openpyxl.Workbook()
sheet = wb.active
sheet.append(['品名', '最新报价', '单位', '报价数', '报价时间'])
start = datetime.now()

for page in range(1, 51):
# 构造URL
url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
# 伪装请求头
headers = {
"Accept-Encoding": "gzip", # 使用gzip压缩传输数据让访问更快
"User-Agent": ua.random
}
# 发送请求 获取响应
rep = requests.get(url, headers=headers)
# print(rep.status_code)
# Xpath定位提取数据
html = etree.HTML(rep.text)
items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
logging.info(f'该页有多少条信息:{len(items)}') # 一页有20条信息
# 遍历提取出数据
for item in items:
name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
price = ''.join(item.xpath('.//td[3]/text()')) # 最新报价
unit = ''.join(item.xpath('.//td[4]/text()')) # 单位
nums = ''.join(item.xpath('.//td[5]/text()')) # 报价数
time_ = ''.join(item.xpath('.//td[6]/text()')) # 报价时间
sheet.append([name, price, unit, nums, time_])
logging.info([name, price, unit, nums, time_])


wb.save(filename='data1.xlsx')
delta = (datetime.now() - start).total_seconds()
logging.info(f'用时:{delta}s')

运行结果如下:

1
2
3
4
5
6
2021-03-14 11:18:24,119 - INFO: 该页有多少条信息:20
2021-03-14 11:18:24,121 - INFO: ['芥蓝种子', '¥6.20', '斤', '1', '2021-03-14 11:10']
2021-03-14 11:18:24,122 - INFO: ['鸡毛菜种子', '¥4.00', '斤', '1', '2021-03-14 11:10']
2021-03-14 11:18:24,123 - INFO: ['芝麻种子', '¥60.00', '斤', '1', '2021-03-14 11:10']
...
2021-03-14 11:18:42,014 - INFO: 用时:18.245532s

单线程爬虫必须上一个页面爬取完成才能继续爬取,还可能受当时网络状态影响,用时18.245532s,才将数据爬取完,速度比较慢。

四、多线程爬虫

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
import requests
import logging
from zyf.user_agent import UserAgent
from lxml import etree
import openpyxl
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
from datetime import datetime

# 日志输出的基本配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
# 随机产生请求头
ua = UserAgent()
wb = openpyxl.Workbook()
sheet = wb.active
sheet.append(['品名', '最新报价', '单位', '报价数', '报价时间'])
start = datetime.now()


def get_data(page):
# 构造URL
url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
# 伪装请求头
headers = {
"Accept-Encoding": "gzip", # 使用gzip压缩传输数据让访问更快
"User-Agent": ua.random
}
# 发送请求 获取响应
rep = requests.get(url, headers=headers)
# print(rep.status_code)
# Xpath定位提取数据
html = etree.HTML(rep.text)
items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
logging.info(f'该页有多少条信息:{len(items)}') # 一页有20条信息
# 遍历提取出数据
for item in items:
name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
price = ''.join(item.xpath('.//td[3]/text()')) # 最新报价
unit = ''.join(item.xpath('.//td[4]/text()')) # 单位
nums = ''.join(item.xpath('.//td[5]/text()')) # 报价数
time_ = ''.join(item.xpath('.//td[6]/text()')) # 报价时间
sheet.append([name, price, unit, nums, time_])
logging.info([name, price, unit, nums, time_])


def run():
# 爬取1-50页
with ThreadPoolExecutor(max_workers=6) as executor:
future_tasks = [executor.submit(get_data, i) for i in range(1, 51)]
wait(future_tasks, return_when=ALL_COMPLETED)

wb.save(filename='data2.xlsx')
delta = (datetime.now() - start).total_seconds()
print(f'用时:{delta}s')


run()

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-03-14 11:31:39,209 - INFO: 该页有多少条信息:20
2021-03-14 11:31:39,211 - INFO: ['芥蓝种子', '¥6.20', '斤', '1', '2021-03-14 11:23']
2021-03-14 11:31:39,213 - INFO: ['鸡毛菜种子', '¥4.00', '斤', '1', '2021-03-14 11:23']
2021-03-14 11:31:39,214 - INFO: ['芝麻种子', '¥60.00', '斤', '1', '2021-03-14 11:23']
2021-03-14 11:31:39,215 - INFO: ['雪玉菜花种子', '¥60.00', '袋', '2', '2021-03-14 11:23']
2021-03-14 11:31:39,216 - INFO: ['箭毒木', '¥20.00', '棵', '1', '2021-03-14 11:23']
2021-03-14 11:31:39,218 - INFO: ['风箱果树', '¥0.13', '棵', '1', '2021-03-14 11:23']
2021-03-14 11:31:39,219 - INFO: ['木莲', '¥0.38', '棵', '2', '2021-03-14 11:23']
2021-03-14 11:31:39,220 - INFO: ['六道木', '¥15.00', '棵', '2', '2021-03-14 11:23']
2021-03-14 11:31:39,222 - INFO: ['刺桐', '¥3.00', '棵', '2', '2021-03-14 11:23']
2021-03-14 11:31:39,223 - INFO: ['剑麻', '¥6.80', '棵', '1', '2021-03-14 11:23']
...
2021-03-14 11:31:44,379 - INFO: ['风流果', '¥8.50', '斤', '23', '2021-03-14 11:23']
用时:6.285389s

多线程爬虫爬取效率提升非常可观,用时 6.2853s,爬取速度很快。

五、异步协程爬虫

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
import aiohttp
import asyncio
import logging
from zyf.user_agent import UserAgent
from lxml import etree
import openpyxl
from datetime import datetime

# 日志输出的基本配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
# 随机产生请求头
ua = UserAgent()
wb = openpyxl.Workbook()
sheet = wb.active
sheet.append(['品名', '最新报价', '单位', '报价数', '报价时间'])
start = datetime.now()


class Spider(object):
def __init__(self):
# self.semaphore = asyncio.Semaphore(6) # 信号量,有时候需要控制协程数,防止爬的过快被反爬
self.header = {
"Accept-Encoding": "gzip", # 使用gzip压缩传输数据让访问更快
"User-Agent": ua.random
}

async def scrape(self, url):
# async with self.semaphore: # 设置最大信号量,有时候需要控制协程数,防止爬的过快被反爬
session = aiohttp.ClientSession(headers=self.header, connector=aiohttp.TCPConnector(ssl=False))
response = await session.get(url)
result = await response.text()
await session.close()
return result

async def scrape_index(self, page):
url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
text = await self.scrape(url)
await self.parse(text)

async def parse(self, text):
# Xpath定位提取数据
html = etree.HTML(text)
items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
logging.info(f'该页有多少条信息:{len(items)}') # 一页有20条信息
# 遍历提取出数据
for item in items:
name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
price = ''.join(item.xpath('.//td[3]/text()')) # 最新报价
unit = ''.join(item.xpath('.//td[4]/text()')) # 单位
nums = ''.join(item.xpath('.//td[5]/text()')) # 报价数
time_ = ''.join(item.xpath('.//td[6]/text()')) # 报价时间
sheet.append([name, price, unit, nums, time_])
logging.info([name, price, unit, nums, time_])

def main(self):
# 50页的数据
scrape_index_tasks = [asyncio.ensure_future(self.scrape_index(page)) for page in range(1, 51)]
loop = asyncio.get_event_loop()
tasks = asyncio.gather(*scrape_index_tasks)
loop.run_until_complete(tasks)


if __name__ == '__main__':
spider = Spider()
spider.main()
wb.save('data3.xlsx')
delta = (datetime.now() - start).total_seconds()
print("用时:{:.3f}s".format(delta))

运行结果如下:

1
2
3
4
5
6
7
8
9
2021-03-14 11:43:37,704 - INFO: 该页有多少条信息:20
2021-03-14 11:43:37,706 - INFO: ['猪油糕', '¥60.00', '公斤', '1', '2021-03-14 11:35']
2021-03-14 11:43:37,707 - INFO: ['黄菜', '¥2000.00', '斤', '1', '2021-03-14 11:35']
2021-03-14 11:43:37,709 - INFO: ['切糕脆', '¥68.19', '件', '1', '2021-03-14 11:35']
2021-03-14 11:43:37,710 - INFO: ['醪糟酒酿', '¥2.70', '斤', '1', '2021-03-14 11:35']
2021-03-14 11:43:37,711 - INFO: ['姜汁糖', '¥6.00', '斤', '1', '2021-03-14 11:35']
...
2021-03-14 11:43:40,240 - INFO: ['风味玫瑰李子', '¥1.60', '斤', '1', '2021-03-14 11:35']
用时:3.826s

而到了协程异步爬虫,爬取速度更快,用时 3.826s 就爬取完 50 页数据,aiohttp + asyncio 异步爬虫竟恐怖如斯。异步爬虫在服务器能承受高并发的前提下增加并发数量,爬取效率提升是非常可观的,比多线程还要快一些。

三种爬虫都将 50 页的数据爬取下来保存到了本地,结果如下:

六、总结回顾

今天演示了简单的单线程爬虫、多线程爬虫和协程异步爬虫。可以看到一般情况下异步爬虫速度最快,多线程爬虫稍慢一点,单线程爬虫速度较慢,必须上一个页面爬取完成才能继续爬取。

但协程异步爬虫相对来说并不是那么好编写,数据抓取无法使用 request 库,只能使用aiohttp,而且爬取数据量大时,异步爬虫需要设置最大信号量来控制协程数,防止爬得过快被反爬。所以在实际编写 Python 爬虫时,我们一般都会使用多线程爬虫来提速,但必须注意的是网站一般都有 ip 访问频率限制,爬的过快可能会被封ip,所以一般我们在多线程提速的同时可以使用代理 ip 来并发地爬取数据。

  • 多线程(multithreading):是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作 “线程” (Thread),利用它编程的概念就叫作 “多线程处理”。
  • 异步(asynchronous):为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的。例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。简言之,异步意味着无序
  • 协程(coroutine),又称微线程、纤程,协程是一种用户态的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是协程的优势
赞赏一下吧~