Python抓取图片_hzp666的博客-程序员宝宝

技术标签: 爬虫  python  Python  

本文系作者「无名小妖」的第二篇原创投稿文章,作者通过用爬虫示例来说明并发相关的多线程、多进程、协程之间的执行效率对比。如果你喜欢写博客,想投稿可微信我,有稿费酬劳。

640?wx_fmt=png

假设我们现在要在网上下载图片,一个简单的方法是用 requests+BeautifulSoup。注:本文所有例子都使用python3.5)

单线程

示例 1:get_photos.py

import os
import time
import uuid

import requests
from bs4 import BeautifulSoup

def out_wrapper(func):  # 记录程序执行时间的简单装饰器
   def inner_wrapper():
       start_time = time.time()
       func()
       stop_time = time.time()
       print('Used time {}'.format(stop_time-start_time))
   return inner_wrapper

def save_flag(img, filename):  # 保存图片
   path = os.path.join('down_photos', filename)
   with open(path, 'wb') as fp:
       fp.write(img)

def download_one(url):  # 下载一个图片
   image = requests.get(url)
   save_flag(image.content, str(uuid.uuid4()))

def user_conf():  # 返回30个图片的url
   url = 'https://unsplash.com/'
   ret = requests.get(url)
   soup = BeautifulSoup(ret.text, "lxml")
   zzr = soup.find_all('img')
   ret = []
   num = 0
   for item in zzr:
       if item.get("src").endswith('80') and num < 30:
           num += 1
           ret.append(item.get("src"))
   return ret

@out_wrapper
def download_many():
   zzr = user_conf()
   for item in zzr:
       download_one(item)

if __name__ == '__main__':
   download_many()

示例1进行的是顺序下载,下载30张图片的平均时间在60s左右(结果因实验环境不同而不同)。

这个代码能用但并不高效,怎么才能提高效率呢?

参考开篇的示意图,有三种方式:多进程、多线程和协程。下面我们一一说明:

我们都知道 Python 中存在 GIL(主要是Cpython),但 GIL 并不影响 IO 密集型任务,因此对于 IO 密集型任务而言,多线程更加适合(线程可以开100个,1000个而进程同时运行的数量受 CPU 核数的限制,开多了也没用)

不过,这并不妨碍我们通过实验来了解多进程。

多进程

示例2

from multiprocessing import Process
from get_photos import out_wrapper, download_one, user_conf

@out_wrapper
def download_many():
   zzr = user_conf()
   task_list = []
   for item in zzr:
       t = Process(target=download_one, args=(item,))
       t.start()
       task_list.append(t)
   [t.join() for t in task_list]  # 等待进程全部执行完毕(为了记录时间)

if __name__ == '__main__':
   download_many()

本示例重用了示例1的部分代码,我们只需关注使用多进程的这部分。

笔者测试了3次(使用的机器是双核超线程,即同时只能有4个下载任务在进行),输出分别是:19.5s、17.4s和18.6s。速度提升并不是很多,也证明了多进程不适合io密集型任务。

还有一种使用多进程的方法,那就是内置模块futures中的ProcessPoolExecutor。

示例3

from concurrent import futures
from get_photos import out_wrapper, download_one, user_conf

@out_wrapper
def download_many():
   zzr = user_conf()
   with futures.ProcessPoolExecutor(len(zzr)) as executor:
       res = executor.map(download_one, zzr)
   return len(list(res))

if __name__ == '__main__':
   download_many()

使用 ProcessPoolExecutor 代码简洁了不少,executor.map 和标准库中的 map用法类似。耗时和示例2相差无几。多进程就到这里,下面来体验一下多线程。

多线程

示例4

import threading
from get_photos import out_wrapper, download_one, user_conf

@out_wrapper
def download_many():
   zzr = user_conf()
   task_list = []
   for item in zzr:
       t = threading.Thread(target=download_one, args=(item,))
       t.start()
       task_list.append(t)
   [t.join() for t in task_list]

if __name__ == '__main__':
   download_many()

threading 和 multiprocessing 的语法基本一样,但是速度在9s左右,相较多进程提升了1倍。

下面的示例5和示例6中分别使用内置模块 futures.ThreadPoolExecutor 中的 map 和submit、as_completed

示例5

from concurrent import futures
from get_photos import out_wrapper, download_one, user_conf

@out_wrapper
def download_many():
   zzr = user_conf()
   with futures.ThreadPoolExecutor(len(zzr)) as executor:
       res = executor.map(download_one, zzr)
   return len(list(res))

if __name__ == '__main__':
   download_many()

示例6:

  1. from concurrent import futures
  2. from get_photos import out_wrapper, download_one, user_conf
  3. @out_wrapper
  4. def download_many():
  5.    zzr = user_conf()
  6.     with futures.ThreadPoolExecutor(len(zzr)) as executor:
  7.        to_do = [executor.submit(download_one, item) for item in zzr]
  8.        ret = [future.result() for future in futures.as_completed(to_do)]
  9.     return ret
  10. if __name__ == '__main__':
  11.    download_many()

Executor.map 由于和内置的map用法相似所以更易于使用,它有个特性:返回结果的顺序与调用开始的顺序一致。不过,通常更可取的方式是,不管提交的顺序,只要有结果就获取。

为此,要把 Executor.submit 和 futures.as_completed结合起来使用。

最后到了协程,这里分别介绍 gevent 和 asyncio。

gevent

示例7

  1. from gevent import monkey
  2. monkey.patch_all()
  3. import gevent
  4. from get_photos import out_wrapper, download_one, user_conf
  5. @out_wrapper
  6. def download_many():
  7.    zzr = user_conf()
  8.    jobs = [gevent.spawn(download_one, item) for item in zzr]
  9.    gevent.joinall(jobs)
  10. if __name__ == '__main__':
  11.    download_many()

asyncio

示例8

import uuid
import asyncio

import aiohttp
from get_photos import out_wrapper, user_conf, save_flag

async def download_one(url):
   async with aiohttp.ClientSession() as session:
       async with session.get(url) as resp:
           save_flag(await resp.read(), str(uuid.uuid4()))

@out_wrapper
def download_many():
   urls = user_conf()
   loop = asyncio.get_event_loop()
   to_do = [download_one(url) for url in urls]
   wait_coro = asyncio.wait(to_do)
   res, _ = loop.run_until_complete(wait_coro)
   loop.close()
   return len(res)

if __name__ == '__main__':
   download_many()

协程的耗时和多线程相差不多,区别在于协程是单线程。具体原理限于篇幅这里就不赘述了。

但是我们不得不说一下asyncio,asyncio是Python3.4加入标准库的,在3.5为其添加async和await关键字。或许对于上述多线程多进程的例子你稍加研习就能掌握,但是想要理解asyncio你不得不付出更多的时间和精力。

另外,使用线程写程序比较困难,因为调度程序任何时候都能中断线程。必须保留锁以保护程序,防止多步操作在执行的过程中中断,防止数据处于无效状态。

而协程默认会做好全方位保护,我们必须显式产出才能让程序的余下部分运行。对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任意时刻只有一个协程运行。想交出控制权时,可以使用 yield 或 yield from(await) 把控制权交还调度程序。

总结

本篇文章主要是将python中并发相关的模块进行基本用法的介绍,全做抛砖引玉。而这背后相关的进程、线程、协程、阻塞io、非阻塞io、同步io、异步io、事件驱动等概念和asyncio的用法并未介绍。大家感兴趣的话可以自行google或者百度,也可以在下方留言,大家一起探讨。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hzp666/article/details/80858222

智能推荐

回避测试方法论_weixin_33972649的博客-程序员宝宝

为什么80%的码农都做不了架构师?&gt;&gt;&gt; ...

使用ParallelPeriod注意事项-31日返回空值_jzdzhiyun的博客-程序员宝宝

         如下面一个语句,查询的是2010-12-31日3个月前的那个日期,系统本应该返回的是2010-09-31,而我们知道9月份只有30没有31日的。结果系统实际最后返回给我们的是一个空值,所以碰到很多网友发现根据日期并用到了这个函数去查询结果时数据总不正确,很可能是这个潜在的问题导致的。ParallelPeriod([ABM RECORDDATE].[年-半年-季度-月-日].[The Month],3,[ABM RECORDDATE].[年-半年-季度-月-日].[The Date].&[2

电力系统潮流计算c加加语言指南,第三章简单电力系统的潮流计算汇总_工程师李涛的博客-程序员宝宝

1、第一章 简单电力系统的分析和计算一、 基本要求掌握电力线路中的电压降落和功率损耗的计算、变压器中的电压降落和功率损耗的计算;掌握辐射形网络的潮流分布计算;掌握简单环形网络的潮流分布计算;了解电力网络的简化。二、 重点内容1、 电力线路中的电压降落和功率损耗图3-1中,设线路末端电压为、末端功率为,则(1)计算电力线路中的功率损耗 线路末端导纳支路的功率损耗: (3-1)则阻抗支路末端的功率为:...

2020年天梯赛真题——L2-3 完全二叉树的层序遍历_白鹿贞松的博客-程序员宝宝

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是完美二叉树。对于深度为 D 的,有 N 个结点的二叉树,若其结点对应于相同深度完美二叉树的层序遍历的前 N 个结点,这样的树就是完全二叉树。给定一棵完全二叉树的后序遍历,请你给出这棵树的层序遍历结果。输入格式:输入在第一行中给出正整数 N(≤30),即树中结点个数。第二行给出后序遍历序列,为 N 个不超过 100 的正整数。同一行中所有数字都以空格分隔。输出格式:在一行中输出该树的层序遍历序列。所有数字都以 1 个空格分隔,行首尾不得有

黑马程序员--网络编程_weixin_30780221的博客-程序员宝宝

端口号的范围:0-65535,其中0-1024系统已经占用,例如:WEB端口:80;mysql端口:3306;tomcat:8080;在网络通信里,  1、传输层协议:    TCP协议,类似于打电话(下载)      1.建立连接,创建连接两端的通道(通过三次握手)      2.适合传输大数据      3.可靠地      4.由于需要建立连接,效率低...

淘宝在数据处理领域的项目及开源产品介绍_weixin_30739595的博客-程序员宝宝

淘宝在数据存储和处理领域在国内互联网公司中一直保持比较靠前的位置,而且由于电子商务领域独特的应用场景,淘宝在数据实时性和大规模计算及挖掘方面一直在国内保持着领先,因此积累了很多的实践的经验和产品。TimeTunnel基于Hbase打造的消息中间件,具有高可靠、消息顺序、事务等传统特性,还能按时间维度反复订阅最近历史的任意数据高性能的broker,单节点达2万TPS,实际支持上千长链...

随便推点

Invocation of init method failed; nested exception is org.hibernate.HibernateException: could not in..._weixin_30823683的博客-程序员宝宝

严重: Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener org.springframework.beans.factory.BeanCreationException: Error creat...

又一位!发40篇SCI,90后博士受聘985教授_算法与数学之美的博客-程序员宝宝

最近一段时间,连续多位“90后”博导刷爆了很多人的朋友圈。从三年前引发舆论热议的学霸博导杨树、刘明侦,到前几天的刘琬璐……据不完全统计,目前国内高校和科研单位至少有10位“90后”教授。...

Delta File Fomat 2:扩展Spark读取Delta文件_wankunde的博客-程序员宝宝

文章目录DataSourceSpark 对外暴漏的读写文件的入口:writer.save() 方法DataFrameReader.load() 方法java.util.ServiceLoader扩展Spark 支持的DataSourceDataSourceDataSource 是Spark用来描述对应的数据文件格式的入口,对应的Delta也是一种数据文件格式,所以了解DataSource实现原...

TWS耳机哪个牌子音质好?2021年TWS降噪耳机推荐_kissshuma的博客-程序员宝宝

降噪耳机已经成为市场上的主流,这几年无论是出货量还是品牌覆盖度有着爆炸式的发展,价格更是从百元到上千元不等,但对于大多人而言,想要的可能只是自由无束享受无线耳机的乐趣,但太多的款式琳琅满目,所以就让我来介绍几款降噪蓝牙耳机给大家。一、NANK南卡A2推荐理由:超高性价比,自适应降噪,高音质低延迟参考价格:399Nank南卡耳机完全拿国际大牌的生产标准和工艺来生产,同一批次只挑选80%产品上市开卖,相当严苛。也因此Nank南卡蓝牙耳机品质、性能极其强悍,尤其南卡A2耳机推出后,其40dB的

echarts 自定义配置带单位的 tooltip 提示框方法 和 圆环数据 tooltip 过长超出屏幕..._weixin_33889245的博客-程序员宝宝

-------tip1--------在 tooltip 里边配置:拼接字符串;tooltip : { trigger: 'axis', formatter:function(params) {     var relVal = params[0].name; for (var i = 0, l = params.length; i...

SVN使用教程之-分支/标记 合并 subeclipse_weixin_34072637的博客-程序员宝宝

http://energykey.iteye.com/blog/512745首先说下为什么我们需要用到分支-合并。比如项目demo下有两个小组,svn下有一个trunk版。由于客户需求突然变化,导致项目需要做较大改动,此时项目组决定由小组1继续完成原来正进行到一半的工作【某个模块】,小组2进行新需求的开发。那么此时,我们就可以为小组2建立一个分支,分支其实就是trunk版【主...

推荐文章

热门文章

相关标签