Python爬虫-速度(2)

Python爬虫-速度(2)


018.9.17

Python爬虫-速度(1)
Python爬虫-速度(3)

前言

我原有个习惯,就是每写个什么东西,都会在开头记下日期。今天得空复查这篇内容的时候,发现居然赫然显示着:018.9.17

十天就这么过去了。

很难说我这10天里到底做了什么,收获了什么。因为我确实未发现自己在这时间里有如何的长进。倒是再一次加深了对时间飞速的体会。

继而说说这次的内容吧。由于进程线程协程涉及到的知识点很多,如果细讲起来,够写好几篇了。所以只好提供自己学习时用到的资料,包括书籍《Python Cook》,以及一些博客文章。为了方便你我他,是全部附上了链接的。

并发与并行

我们先来说说什么是并发,什么又是并行。

并发:指程序有处理多个任务的能力
并行:指程序有同时处理多个任务的能力

二者之间的差别,就在同时二字。可以举个例子:并发就是一群人从一个独木桥上过,而并行则是一群人从多个独木桥上过。
在这里插入图片描述
在这里插入图片描述

其实网上我看到许多人爱用“一个人吃十个馒头与十个人吃十个馒头”的例子,倒是很好的解释了并发与并行的区别,然而乍一看,似乎并发没啥用呀!所以庆幸自己想出一个过独木桥的例子【不要脸.jpg】,这样一来不但给了二者区别,还可以体现出并发的提速作用:一群人过独木桥(所有人人可以都在桥上)肯定是比这群人挨个挨个的过独木桥(桥上最多允许一个人存在)要快许多。

异步与同步/阻塞和非阻塞

可以用一个在知乎看到的段子来理解:

老张爱喝茶,废话不说,煮开水。
出场人物:老张。
道具:水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

(1) 老张把水壶放到火上,立等水开。(同步阻塞)
【老张觉得自己有点傻】
(2) 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
【老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。】

(3) 老张把响水壶放到火上,立等水开。(异步阻塞)
【老张觉得这样傻等意义不大。】
(4) 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
【老张觉得自己聪明了。】

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。情况(1)和情况(3)中老张就是阻塞的,媳妇喊他都不知道。虽然(3)中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

——来源网络,作者不明。

作者:愚抄
链接:https://www.zhihu.com/question/19732473/answer/23434554
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

多进程

由于主要是为爬虫程序提速,所以过多的原理暂不讲的。但需要记得的是:多进程是并行的

我们可以通过实例来观察,首先来一个单进程单线程的爬虫程序:

import requests
import time

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()
    results = map(get_one_html, urls)
    for result in results:
        print(result)
    end = time.time() - start
    print(end)

多次试验,耗时在2.8~3.6s范围。如果我们改写为多进程呢?

可参见《Python Cook》中简单的并行编程,链接:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p08_perform_simple_parallel_programming.html

import requests
import time
from concurrent.futures import ProcessPoolExecutor

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()

	# 利用进程池
    with ProcessPoolExecutor() as pool:
	    # 往进程池添加任务
        pool.map(get_one_html, urls)

    end = time.time() - start
    print(end)

多次试验,耗时在1.2~1.7s范围。

可以看到多进程的确可以实现程序提速,但需要的注意的是,只有在需要多次使用get_one_html()函数时,多进程的优势才能体现;如果仅仅用两三次,反而是单进程单线程运行速度快。这是因为系统在为实现多进程前需要一些准备工作,这个准备将耗费大量时间。

多线程

多线程是并发的。尽管Python中有GIL锁,但针对I/O操作时,仍有提速效果。

可参见《Python Cook》中创建一个线程池,链接:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p07_creating_thread_pool.html

import requests
import time
from concurrent.futures import ThreadPoolExecutor

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()

	# 线程池
    with ThreadPoolExecutor() as pool:
        pool.map(get_one_html, urls)

    end = time.time() - start
    print(end)

多次试验,耗时在0.86~1.0s范围。

可以看到提速比多进程会好一些。

协程+异步

关于这部分的知识,讲起来都可以单独撑开一个篇幅了,所以这里我就“拿来主义”:
在这里插入图片描述
崔大神的一篇关于异步提速,链接:https://mp.weixin.qq.com/s/jCc1jIHxU_p6bUlwijkwyQ
以及CSDN上某友的文章,链接:https://blog.csdn.net/sinat_26917383/article/details/79246632

import aiohttp
import time
import asyncio


async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=HEADERS) as response:
            assert response.status == 200

    return response.url

async def get_one_html(url):
    result = await get(url) # 由于asyncio不支持http请求,所以需要用aiohttp来封装一个get
    return result

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]
    # 构建tasks列表
    tasks = [asyncio.ensure_future(get_one_html(url)) for url in urls]

    start = time.time()

    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    end = time.time() - start
    print(end)

    for task in tasks:
        print(task.result())

多次试验,耗时在1.4~1.8s范围。尽管速度上不及多进程与多线程,但我们知道它节省了配置,如果请求的任务量继续加大,做相应调整是可以超越前面两种方法的。

猜你喜欢

转载自blog.csdn.net/qq_41359051/article/details/83990892