Days
- 00. 简介
- 01. 初识 Python
- 02. 语言元素
- 03. 分支结构
- 04. 循环结构
- 05. 构造程序逻辑
- 06. 函数和模块的使用
- 07. 字符串和常用数据结构
- 08. 面向对象编程基础
- 09. 面向对象进阶
- 10. 图形用户界面和游戏开发
- 11. 文件和异常
- 12. 字符串和正则表达式
- 13. 进程和线程
- 14. 网络编程入门和网络应用开发
- 15. 图像和办公文档处理
- 16 20. Python 语言进阶
- 21 30. Web 前端概述
- 31 35. 玩转 Linux 操作系统
- 36. 关系型数据库和 MySQL 概述
- 37. SQL 详解之 DDL
- 38. SQL 详解之 DML
- 39. SQL 详解之 DQL
- 40. SQL 详解之 DCL
- 41. MySQL 新特性
- 42. 视图、函数和过程
- 43. 索引
- 44. Python接入MySQL数据库
- 45. 大数据平台和HiveSQL
- 46. Django快速上手
- 47. 深入模型
- 48. 静态资源和 Ajax 请求
- 49. Cookie 和 Session
- 50. 制作报表
- 51. 日志和调试工具栏
- 52. 中间件的应用
- 53. 前后端分离开发入门
- 54. RESTful 架构和 DRF 入门
- 55. RESTful 架构和 DRF 进阶
- 56. 使用缓存
- 57. 接入三方平台
- 58. 异步任务和定时任务
- 59. 单元测试
- 60. 项目上线
- 61. 网络数据采集概述
- 62. 用 Python 获取网络资源 1
- 62. 用 Python 解析 HTML 页面 2
- 63. Python 中的并发编程 1
- 63. Python 中的并发编程 2
- 63. Python 中的并发编程 3
- 63. 并发编程在爬虫中的应用
- 64. 使用 Selenium 抓取网页动态内容
- 65. 爬虫框架 Scrapy 简介
- 66. 数据分析概述
- 67. 环境准备
- 68. NumPy 的应用 1
- 69. NumPy 的应用 2
- 70. NumPy 的应用 3
- 71. NumPy 的应用 4
- 72. 深入浅出 pandas 1
- 73. 深入浅出 pandas 2
- 74. 深入浅出 pandas 3
- 75. 深入浅出 pandas 4
- 76. 深入浅出 pandas 5
- 77. 深入浅出 pandas 6
- 78. 数据可视化 1
- 79. 数据可视化 2
- 80. 数据可视化 3
- 81. 人工智能和机器学习概述
- 82. k 最近邻分类
- 83. 决策树
- 83. 推荐系统实战 1
- 84. 贝叶斯分类
- 85. 支持向量机
- 86. K 均值聚类
- 87. 回归分析
- 88. 深度学习入门
- 89. PyTorch 概述
- 90. PyTorch 实战
- 91. 团队项目开发的问题和解决方案
- 92. Docker 容器技术详解
- 93. MySQL 性能优化
- 94. 网络 API 接口设计
- 95. 使用 Django 开发商业项目
- 96. 软件测试和自动化测试
- 97. 电商网站技术要点剖析
- 98. 项目部署上线和性能调优
- 99. 面试中的公共问题
- 100. Python 面试题实录
公开课
番外篇
63. 并发编程在爬虫中的应用
之前的课程,我们已经为大家介绍了 Python 中的多线程、多进程和异步编程,通过这三种手段,我们可以实现并发或并行编程,这一方面可以加速代码的执行,另一方面也可以带来更好的用户体验。爬虫程序是典型的 I/O 密集型任务,对于 I/O 密集型任务来说,多线程和异步 I/O 都是很好的选择,因为当程序的某个部分因 I/O 操作阻塞时,程序的其他部分仍然可以运转,这样我们不用在等待和阻塞中浪费大量的时间。下面我们以爬取“360图片”网站的图片并保存到本地为例,为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别,同时也对它们的执行效率进行简单的对比。
“360图片”网站的页面使用了 Ajax 技术,这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说,页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的,而且整个页面还使用了瀑布式加载(一边向下滚动,一边加载更多的图片)。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口,如下图所示,我们需要的图片信息就在服务器返回的 JSON 数据中。
例如,要获取“美女”频道的图片,我们可以请求如下所示的URL,其中参数ch
表示请求的频道,=
后面的参数值beauty
就代表了“美女”频道,参数sn
相当于是页码,0
表示第一页(共30
张图片),30
表示第二页,60
表示第三页,以此类推。
https://image.so.com/zjl?ch=beauty&sn=0
单线程版本
通过上面的 URL 下载“美女”频道共90
张图片。
"""
example04.py - 单线程版本爬虫
"""
import os
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
download_picture(pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
在 macOS 或 Linux 系统上,我们可以使用time
命令来了解上面代码的执行时间以及 CPU 的利用率,如下所示。
time python3 example04.py
下面是单线程爬虫代码在我的电脑上执行的结果。
python3 example04.py 2.36s user 0.39s system 12% cpu 21.578 total
这里我们只需要关注代码的总耗时为21.578
秒,CPU 利用率为12%
。
多线程版本
我们使用之前讲到过的线程池技术,将上面的代码修改为多线程版本。
"""
example05.py - 多线程版本爬虫
"""
import os
from concurrent.futures import ThreadPoolExecutor
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
with ThreadPoolExecutor(max_workers=16) as pool:
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
pool.submit(download_picture, pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
执行如下所示的命令。
time python3 example05.py
代码的执行结果如下所示:
python3 example05.py 2.65s user 0.40s system 95% cpu 3.193 total
异步I/O版本
我们使用aiohttp
将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库aiohttp
和aiofile
,命令如下所示。
pip install aiohttp aiofile
aiohttp
的用法在之前的课程中已经做过简要介绍,aiofile
模块中的async_open
函数跟 Python 内置函数open
的用法大致相同,只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。
"""
example06.py - 异步I/O版本爬虫
"""
import asyncio
import json
import os
import aiofile
import aiohttp
async def download_picture(session, url):
filename = url[url.rfind('/') + 1:]
async with session.get(url, ssl=False) as resp:
if resp.status == 200:
data = await resp.read()
async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file:
await file.write(data)
async def fetch_json():
async with aiohttp.ClientSession() as session:
for page in range(3):
async with session.get(
url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}',
ssl=False
) as resp:
if resp.status == 200:
json_str = await resp.text()
result = json.loads(json_str)
for pic_dict in result['list']:
await download_picture(session, pic_dict['qhimg_url'])
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
loop = asyncio.get_event_loop()
loop.run_until_complete(fetch_json())
loop.close()
if __name__ == '__main__':
main()
执行如下所示的命令。
time python3 example06.py
代码的执行结果如下所示:
python3 example06.py 0.82s user 0.21s system 27% cpu 3.782 total
总结
通过上面三段代码执行结果的比较,我们可以得出一个结论,使用多线程和异步 I/O 都可以改善爬虫程序的性能,因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上,而time
命令的执行结果也告诉我们,单线程的代码 CPU 利用率仅仅只有12%
,而多线程版本的 CPU 利用率则高达95%
;单线程版本的爬虫执行时间约21
秒,而多线程和异步 I/O 的版本仅执行了3
秒钟。另外,在运行时间差别不大的情况下,多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源,这是因为多线程的调度和切换也需要花费 CPU 时间。至此,三种方式在 I/O 密集型任务上的优劣已经一目了然,当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢,那么使用多线程和异步 I/O 的优势将更为明显,有兴趣的读者可以自行试验。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论