在Python爬虫下一代网络请求库httpx和parsel解析库测评一文中我们对比了requests的同步爬虫和httpx的异步协程爬虫爬取链家二手房信息所花的时间(如下所示:一共580条记录),结果httpx同步爬虫花了16.1秒,而httpx异步爬虫仅花了2.5秒。

那么问题来了。实现爬虫的高并发不仅仅只有协程异步这一种手段,传统的同步爬虫结合多进程和多线程也能大大提升爬虫工作效率,那么多进程, 多线程和异步协程爬虫到底谁更快呢? 当然对于现实中的爬虫,爬得越快,被封的可能性也越高。本次测评使用httpx爬取同样链家网数据,不考虑反爬因素,测评结果可能因个人电脑和爬取网站对象而异。

在我们正式开始前,你能预测下哪种爬虫更快吗?可能结果会颠覆你的观点。

传统爬虫 vs 协程异步爬虫

传统Python爬虫程序都是运行在单进程和单线程上的,包括httpx异步协程爬虫。如果你不清楚进程和线程的区别,以及Python如何实现多进程和多线程编程,请阅读下面这篇知乎上收藏过1000的文章。

  • 一文看懂Python多进程与多线程编程(工作学习面试必读)

一个传统的web爬虫代码可能如下所示,先用爬虫获取目标页面中显示的最大页数,然后循环爬取每个单页数据并解析。单进程、单线程同步爬虫的请求是阻塞的,在一个请求处理完全结束前不会发送一个新的请求,中间浪费了很多等待时间。

httpx异步协程爬虫虽然也是运行在单进程单线程上的,但是所有异步任务都会加到事件循环(loop)中运行,可以一次有上百或上千个活跃的任务,一旦某个任务需要等待,loop会快速切换到下面一个任务,所以协程异步要快很多。

要把上面的同步爬虫变为异步协程爬虫,我们首先要使用async将单个页面的爬取和解析过程包装成异步任务,使用httpx提供的AsyncClient发送异步请求。

接着我们使用asyncio在主函数parse_page里获取事件循环(loop), 并将爬取单个页面的异步任务清单加入loop并运行。

多进程爬虫

对于多线程爬虫,我们首先定义一个爬取并解析单个页面的同步任务。

接下来我们在主函数parse_page里用multiprocessing库提供的进程池Pool来管理多进程任务。池子里进程的数量,一般建议为CPU的核数,这是因为一个进程需要一个核,你设多了也没用。我们使用map方法创建了多进程任务,你还可以使用apply_async方法添加多进程任务。任务创建好后,任务的开始和结束都由进程池来管理,你不需要进行任何操作。这样我们一次就有4个进程同时在运行了,一次可以同时处理4个请求。

那用这个多进程爬虫爬取链家580条数据花了多长时间呢? 答案是7.6秒,比单进程单线程的httpx同步爬虫16.1秒还是要快不少的。

项目完整代码如下所示:

from fake_useragent import UserAgent
import csv
import re
import time
from parsel import Selector
import httpx
from multiprocessing import Pool, cpu_count, Queue, Managerclass HomeLinkSpider(object):def __init__(self):# 因为多进程之间不能共享内存,需使用队列Queue共享数据进行通信# 每个进程爬取的数据都存入这个队列,不能使用self.data列表# 子进程获取不到self.headers这个变量,需要直接生成# self.ua = UserAgent()# self.headers = {"User-Agent": self.ua.random}self.q = Manager().Queue()self.path = "浦东_三房_500_800万.csv"self.url = "https://sh.lianjia.com/ershoufang/pudong/a3p5/"def get_max_page(self):response = httpx.get(self.url, headers={"User-Agent": UserAgent().random})if response.status_code == 200:# 创建Selector类实例selector = Selector(response.text)# 采用css选择器获取最大页码div Boxla = selector.css('div[class="page-box house-lst-page-box"]')# 使用eval将page-data的json字符串转化为字典格式max_page = eval(a[0].xpath('//@page-data').get())["totalPage"]print("最大页码数:{}".format(max_page))return max_pageelse:print("请求失败 status:{}".format(response.status_code))return None# 解析单页面,需传入单页面url地址def parse_single_page(self, url):print("子进程开始爬取:{}".format(url))response = httpx.get(url, headers={"User-Agent": UserAgent().random})selector = Selector(response.text)ul = selector.css('ul.sellListContent')[0]li_list = ul.css('li')for li in li_list:detail = dict()detail['title'] = li.css('div.title a::text').get()#  2室1厅 | 74.14平米 | 南 | 精装 | 高楼层(共6层) | 1999年建 | 板楼house_info = li.css('div.houseInfo::text').get()house_info_list = house_info.split(" | ")detail['bedroom'] = house_info_list[0]detail['area'] = house_info_list[1]detail['direction'] = house_info_list[2]floor_pattern = re.compile(r'\d{1,2}')match1 = re.search(floor_pattern, house_info_list[4])  # 从字符串任意位置匹配if match1:detail['floor'] = match1.group()else:detail['floor'] = "未知"# 匹配年份year_pattern = re.compile(r'\d{4}')match2 = re.search(year_pattern, house_info_list[5])if match2:detail['year'] = match2.group()else:detail['year'] = "未知"# 文兰小区 - 塘桥    提取小区名和哈快position_info = li.css('div.positionInfo a::text').getall()detail['house'] = position_info[0]detail['location'] = position_info[1]# 650万,匹配650price_pattern = re.compile(r'\d+')total_price = li.css('div.totalPrice span::text').get()detail['total_price'] = re.search(price_pattern, total_price).group()# 单价64182元/平米, 匹配64182unit_price = li.css('div.unitPrice span::text').get()detail['unit_price'] = re.search(price_pattern, unit_price).group()self.q.put(detail)def parse_page(self):max_page = self.get_max_page()print("CPU内核数:{}".format(cpu_count()))# 使用进程池管理多进程任务with Pool(processes=4) as pool:urls = ['https://sh.lianjia.com/ershoufang/pudong/pg{}a3p5/'.format(i) for i in range(1, max_page + 1)]# 也可以使用pool.apply_async(self.parse_single_page, args=(url,))pool.map(self.parse_single_page, urls)def write_csv_file(self):head = ["标题", "小区", "房厅", "面积", "朝向", "楼层", "年份", "位置", "总价(万)", "单价(元/平方米)"]keys = ["title", "house", "bedroom", "area", "direction", "floor", "year", "location","total_price", "unit_price"]try:with open(self.path, 'w', newline='', encoding='utf_8_sig') as csv_file:writer = csv.writer(csv_file, dialect='excel')if head is not None:writer.writerow(head)# 如果队列不为空,写入每行数据while not self.q.empty():item = self.q.get()if item:row_data = []for k in keys:row_data.append(item[k])writer.writerow(row_data)print("Write a CSV file to path %s Successful." % self.path)except Exception as e:print("Fail to write CSV to path: %s, Case: %s" % (self.path, e))if __name__ == '__main__':start = time.time()home_link_spider = HomeLinkSpider()home_link_spider.parse_page()home_link_spider.write_csv_file()end = time.time()print("耗时:{}秒".format(end-start))

注意: 多个进程之间内存是不共享的,需要使用Python多进程模块提供的Manager.Queue()实现多个进程的数据共享,比如把不同进程爬取的数据存到一个地方。

多线程爬虫

爬取解析单个页面的函数和多进程爬虫里的代码是一样的,不同的是我们在parse_page主函数里使用threading模块提供的方法创建多线程任务,如下所示:

我们也不需要使用Queue()类存储各个线程爬取的数据,因为各个线程内存是可以共享的。多线程同步爬虫运行结果如下所示,爬取580条数据总共耗时只有短短的2.2秒,几乎秒开,甚至比httpx异步协程的还快!

结果为什么是这样呢?其实也不难理解。对于爬虫这种任务,大部分消耗时间其实是等等待时间,在等待时间中CPU是不需要工作的,那你在此期间提供双核或4核CPU进行多进程编程是没有多大帮助的。那么为什么多线程会对爬虫代码有用呢?这时因为Python碰到等待会立即释放GIL供新的线程使用,实现了线程间的快速切换,这跟协程异步任务的切换一个道理,只不过多线程任务的切换由操作系统进行,而协程异步任务的切换由loop进行。

多线程完整代码如下所示:

from fake_useragent import UserAgent
import csv
import re
import time
from parsel import Selector
import httpx
import threadingclass HomeLinkSpider(object):def __init__(self):self.data = list()self.path = "浦东_三房_500_800万.csv"self.url = "https://sh.lianjia.com/ershoufang/pudong/a3p5/"def get_max_page(self):response = httpx.get(self.url, headers={"User-Agent": UserAgent().random})if response.status_code == 200:# 创建Selector类实例selector = Selector(response.text)# 采用css选择器获取最大页码div Boxla = selector.css('div[class="page-box house-lst-page-box"]')# 使用eval将page-data的json字符串转化为字典格式max_page = eval(a[0].xpath('//@page-data').get())["totalPage"]print("最大页码数:{}".format(max_page))return max_pageelse:print("请求失败 status:{}".format(response.status_code))return None# 解析单页面,需传入单页面url地址def parse_single_page(self, url):print("多线程开始爬取:{}".format(url))response = httpx.get(url, headers={"User-Agent": UserAgent().random})selector = Selector(response.text)ul = selector.css('ul.sellListContent')[0]li_list = ul.css('li')for li in li_list:detail = dict()detail['title'] = li.css('div.title a::text').get()#  2室1厅 | 74.14平米 | 南 | 精装 | 高楼层(共6层) | 1999年建 | 板楼house_info = li.css('div.houseInfo::text').get()house_info_list = house_info.split(" | ")detail['bedroom'] = house_info_list[0]detail['area'] = house_info_list[1]detail['direction'] = house_info_list[2]floor_pattern = re.compile(r'\d{1,2}')match1 = re.search(floor_pattern, house_info_list[4])  # 从字符串任意位置匹配if match1:detail['floor'] = match1.group()else:detail['floor'] = "未知"# 匹配年份year_pattern = re.compile(r'\d{4}')match2 = re.search(year_pattern, house_info_list[5])if match2:detail['year'] = match2.group()else:detail['year'] = "未知"# 文兰小区 - 塘桥    提取小区名和哈快position_info = li.css('div.positionInfo a::text').getall()detail['house'] = position_info[0]detail['location'] = position_info[1]# 650万,匹配650price_pattern = re.compile(r'\d+')total_price = li.css('div.totalPrice span::text').get()detail['total_price'] = re.search(price_pattern, total_price).group()# 单价64182元/平米, 匹配64182unit_price = li.css('div.unitPrice span::text').get()detail['unit_price'] = re.search(price_pattern, unit_price).group()self.data.append(detail)def parse_page(self):max_page = self.get_max_page()thread_list = []for i in range(1, max_page + 1):url = 'https://sh.lianjia.com/ershoufang/pudong/pg{}a3p5/'.format(i)t = threading.Thread(target=self.parse_single_page, args=(url,))thread_list.append(t)for t in thread_list:t.start()for t in thread_list:t.join()def write_csv_file(self):head = ["标题", "小区", "房厅", "面积", "朝向", "楼层", "年份", "位置", "总价(万)", "单价(元/平方米)"]keys = ["title", "house", "bedroom", "area", "direction", "floor", "year", "location","total_price", "unit_price"]try:with open(self.path, 'w', newline='', encoding='utf_8_sig') as csv_file:writer = csv.writer(csv_file, dialect='excel')if head is not None:writer.writerow(head)for item in self.data:row_data = []for k in keys:row_data.append(item[k])# print(row_data)writer.writerow(row_data)print("Write a CSV file to path %s Successful." % self.path)except Exception as e:print("Fail to write CSV to path: %s, Case: %s" % (self.path, e))if __name__ == '__main__':start = time.time()home_link_spider = HomeLinkSpider()home_link_spider.parse_page()home_link_spider.write_csv_file()end = time.time()print("耗时:{}秒".format(end-start))

结论

多进程, 多线程和异步协程均可以提高Python爬虫的工作效率。对于爬虫这种非计算密集型的工作,多进程编程对效率的提升不如多线程和异步协程。异步爬虫不总是最快的,同步爬虫+多线程也同样可以很快,有时甚至更快。

  • httpx 同步 + parsel: 16.1秒

  • httpx 异步 + parsel: 2.5秒

  • http 同步多进程 + parsel: 7.6秒

  • http 同步多线程 + parsel: 2.2秒

对于这样的结果,你满意吗? 欢迎留言!

大江狗

2021.5

推荐阅读

神文必读: 同步Python和异步Python的区别在哪里?

Python爬虫下一代网络请求库httpx和parsel解析库测评

一文看懂Python多进程与多线程编程(工作学习面试必读)

非常适合小白的 Asyncio 教程

Python高并发爬虫测评(2): 多进程, 多线程和异步协程哪个快?相关推荐

  1. python获取get请求的耗时时间_突破python爬取极限,超牛逼的异步协程爬虫

    异步协程 1. event_loop 2. coroutine 中文翻译叫协程,在 Python 中昌指代为协程对象类型,可以将协程对象注册到时间循环中被调用.使用 async 关键字来定义的方法在调 ...

  2. python爬虫 asyncio aiohttp aiofiles 单线程多任务异步协程爬取图片

    python爬虫 asyncio aiohttp aiofiles 多任务异步协程爬取图片 main.py """=== coding: UTF8 ==="&q ...

  3. Python分别用单线程,多线程,异步协程爬取一部小说,最快仅需要5s

    文章目录 单线程爬取 多线程爬取 异步协程爬取 本文运用了三种方式爬取一整部小说,分别运用了单线程爬取,多线程爬取和异步协程爬取. 小说网址:` http://www.doupo321.com/dou ...

  4. 爬虫第四章 单线程+多任务异步协程

    单线程+多任务异步协程: asyncio 事件循环 loop: 无限循环的对象,事件循环中最终需要将一些特殊的函数注册到该事件循环中特殊的函数: 被ansyc关键字修饰的函数协程: 本质上是一个对象, ...

  5. asyncio 文件io高并发_python教程:使用 async 和 await 协程进行并发编程

    python 一直在进行并发编程的优化, 比较熟知的是使用 thread 模块多线程和 multiprocessing 多进程,后来慢慢引入基于 yield 关键字的协程. 而近几个版本,python ...

  6. python:一文搞懂多线程,多进程,异步协程的使用场景

    本文将使用场景化为案例,将单线程,多线程,多进程,异步协程的速度进行对比 对比速度时,   >表示 速度快于  >>表示速度远快于  =表示速度差不多 >>>> ...

  7. 小爬爬4.协程基本用法多任务异步协程爬虫示例(大数据量)

    1.测试学习 (2)单线程: from time import sleep import time def request(url):print('正在请求:',url)sleep(2)print(' ...

  8. python爬虫02-提升爬取效率、多线程,多线程传参,多进程,线程及线程池概念,协程,多任务异步协程,异步请求aiohttp模块,视频站工作原理

    1.提升爬取效率 使用多线程,多进程,携程,异步 2.多线程 进程是资源单位,每个进程,都会有一个默认的主线程 线程是执行单位 执行多线程需要导包: from threading import Thr ...

  9. python高并发的解决方案

    python高并发的解决方案 参考文章: (1)python高并发的解决方案 (2)https://www.cnblogs.com/angelyan/p/10439475.html 备忘一下.

最新文章

  1. 波卡链Substrate (5)托盘Pallets进阶
  2. 生产企业开具加工费发票的问题
  3. 如何在endnote列表中显示research note
  4. 我们学的技术会过时吗?甚至被淘汰?
  5. 2021 ISC会上山石网科重磅发布智能下一代防火墙A系列,重新定义边界安全防御
  6. SVM与感知机的异同点
  7. EasyUI DataGrid 可编辑列级联操作
  8. android mvp模式例子_[Android] MVP设计模式及实例
  9. 微信ubuntu版服务器,Ubuntu 18.04 安装微信(Linux通用)
  10. 自适应求积算法 MatLab版
  11. [转]Maven查看当前生效配置、pom、环境变量等命令(mvn help用法)
  12. 如何解决移动端300ms延迟
  13. 【论文解读IJCAI 2019】Extracting Entities and Events as a Single Task Using a Transition-Based NeuralModel
  14. 根据观测时间,经纬度,求太阳高度角
  15. Python利用Opencv读取图片
  16. 双非长沙理工排名飙升176反超吉大,湖大连续2年超哈工大 | US News 2023
  17. 南大周志华postgr
  18. 对电脑十分有用的100招。
  19. 何以解忧,唯有健康,微养生开启了年轻化的养生热潮
  20. 戴文的Linux内核专题:03驱动程序

热门文章

  1. 一个int类型到底占多少个字节
  2. leeCode350_两个数组的交集
  3. linux vi 报错 E37: No write since last change (add to override)
  4. #define的用法
  5. 如何开启任务计划程序
  6. java中文转换数字_Java 中文数字转换为阿拉伯数字
  7. TcPlayer.js 实现文字朗读
  8. 详解磁盘配额的设置方法
  9. 靓号,java正则表达式的实现
  10. 一个啥也不是的鬼畜视频创作示例