爬虫性能相关解释、补充

使用requests模块进行爬虫时如何做到高性能,高效利用资源,爬虫其实就是请求网页,那就要合理利用多进程、多线程已经协程了,简单举个例子,比如需要爬取几个网站首页('http://www.baidu.com/','https://www.cnblogs.com/','https://www.cnblogs.com/news/', 'https://cn.bing.com/','https://stackoverflow.com/',),那么如何最大限度利用资源,最快爬取到结果呢?

最普通写法

先给出最没有性能的普通写法:

"""
相当于串行实现

"""
import requests

urls = [
    'http://www.baidu.com/',
    'https://www.cnblogs.com/',
    'https://www.cnblogs.com/news/',
    'https://cn.bing.com/',
    'https://stackoverflow.com/',
]

for url in urls:
    response = requests.get(url)
    print(response)

上面的五个网址,运行之后,在同一个进程中的同一个线程中进行,由于网络连接默认是阻塞的,相当于请求之间是串行的,如果一个请求花费两秒的话,那么总共会花费十秒钟,速度非常慢,如果是多核服务器,这个无法最大限度利用资源了。

多线程写法

再给出多线程的写法如下(需要使用线程池的话参考本博客的完整版爬虫):、

"""
多线程实现

"""
import requests
import threading


urls = [
    'http://www.baidu.com/',
    'https://www.cnblogs.com/',
    'https://www.cnblogs.com/news/',
    'https://cn.bing.com/',
    'https://stackoverflow.com/',
]

def task(url):
    response = requests.get(url)
    print(response)

for url in urls:
    t = threading.Thread(target=task,args=(url,))
    t.start()

上面明显能够大幅度提高性能,如果一个两秒的话,这里使用了多线程,最快的话,五个请求总共花两秒也是可能的;但是这不是最好的选择,因为发请求非常快,几乎不占用什么资源,但是等待请求的响应是要话时间的,这就是典型的IO操作了(IO操作相当消耗性能);这里相当于将五个请求同时发出去了,然后大家同时等待响应回来;其实开五个线程也是会多占用一下内存的,这里就不讲开多进程去解决问题了,那样会占用更多的内存空间;那么如何能够即占用少量的内存,又能减少IO的消耗呢,协程将是一个很好的选择。

协程写法:

"""
协程实现IO切换
pip3 install gevent
gevent内部调用greenlet(实现了协程)。
"""
from gevent import monkey; monkey.patch_all()
import gevent
import requests


def func(url):
    response = requests.get(url)
    print(response)

urls = [
    'http://www.baidu.com/',
    'https://www.cnblogs.com/',
    'https://www.cnblogs.com/news/',
    'https://cn.bing.com/',
    'https://stackoverflow.com/',
]
spawn_list = []
for url in urls:
    spawn_list.append(gevent.spawn(func, url))

gevent.joinall(spawn_list)

协程本质上是一个线程,能够在多个任务之间切换来节省一些IO时间,协程中任务之间的切换也消耗时间,但是开销要远远小于进程线程之间的切换,相当于将同一个线程切片,当线程中遇到IO操作时就会切换到其他任务,也就是发其他请求;也就是一瞬间能够将所有请求发出去,然后等待响应回来,这样及节省了内存,又减少了IO消耗性能;但是在处理响应的时候,可能要做运算的东西,那时候就不能够使用协程了,反而会将低效率,因为协程并不能利用多核,这是使用多线程比较好;最后写入文件的时候又是典型的IO操作,这时还是用协程合适。类似的提高爬虫性能的与之有一拼的还可以使用Twisted模块实现。

基于事件循环的异步非阻塞模块Twisted

Twisted是用Python实现的基于事件驱动的异步非阻塞网络引擎框架,其实scrapy框架能实现高并发也是基于该模块来实现的;这里给出如何基于Twisted模块解决上面问题的代码:

"""
基于事件循环的异步非阻塞模块:Twisted
"""
from twisted.web.client import getPage, defer
from twisted.internet import reactor

def stop_loop(arg):
    reactor.stop()


def get_response(contents):
    print(contents)

deferred_list = []

url_list = [
    'http://www.baidu.com/',
    'https://www.cnblogs.com/',
    'https://www.cnblogs.com/news/',
    'https://cn.bing.com/',
    'https://stackoverflow.com/',
]

for url in url_list:
    deferred = getPage(bytes(url, encoding='utf8'))
    deferred.addCallback(get_response)
    deferred_list.append(deferred)


dlist = defer.DeferredList(deferred_list)
dlist.addBoth(stop_loop)

reactor.run()

模仿版写法

上述的协程和Twisted内部是如何实现能够切换IO操作的呢;这里给出一个模仿的版本,给出了内部如何建立连接并能够不阻塞还能切换IO操作的方法,这就要使用到IO多路复用了(Python中的select模块):

# 自定义的异步非阻塞模块
import socket
import select

class ChunSheng(object):

    def __init__(self):
        self.socket_list = []
        self.conn_list = []

        self.conn_func_dict = {}

    def add_request(self,url_func):
        conn = socket.socket()
        conn.setblocking(False)  # 将所有连接改为非阻塞的
        try:
            conn.connect((url_func[0],80))
        except BlockingIOError as e:
            pass
        self.conn_func_dict[conn] = url_func[1]

        self.socket_list.append(conn)
        self.conn_list.append(conn)

    def run(self):
        """
        检测self.socket_list中的5个socket对象是否连接成功
        :return:
        """
        while True:
            #   select.select
            #   第一个参数: 用于检测其中socket是否已经获取到响应内容
            #   第二个参数: 用于检测其中socket是否已经连接成功

            # 第一个返回值 r:具体是那一个socket获取到结果
            # 第二个返回值 r:具体是那一个socket连接成功
            r,w,e = select.select(self.socket_list,self.conn_list,[],0.05)
            for sock in w: # [socket1,socket2]
                sock.send(b'GET / http1.1
host:xxxx.com

')
                self.conn_list.remove(sock)

            for sock in r:
                data = sock.recv(8096)
                func = self.conn_func_dict[sock]
                func(data)
                sock.close()
                self.socket_list.remove(sock)

            if not self.socket_list:
                break

向使用协程和Twisted模块一样使用自定义的模块来解决问题的用法:

from chun import ChunSheng

def callback1(data):
    print('下载完成',data)

def callback2(data):
    print('下载完成',data)

chun = ChunSheng()
urls = [
    ('www.baidu.com',callback1),
    ('www.cnblogs.com',callback1),
    ('www.pythonav.com',callback2),
    ('www.bing.com',callback2),
    ('www.stackoverflow.com',callback2),
]
for url in urls:
    chun.add_request(url)

chun.run()
作者:E-QUAL
出处:https://www.cnblogs.com/liujiajia_me/
本文版权归作者和博客园共有,不得转载,未经作者同意参考时必须保留此段声明,且在文章页面明显位置给出原文连接。
                                            本文内容参考如下网络文献得来,用于个人学习,如有侵权,请您告知删除修改。
                                            参考链接:https://www.cnblogs.com/linhaifeng/
                                                             https://www.cnblogs.com/yuanchenqi/
                                                             https://www.cnblogs.com/Eva-J/
                                                             https://www.cnblogs.com/jin-xin/
                                                             https://www.cnblogs.com/liwenzhou/
                                                             https://www.cnblogs.com/wupeiqi/
原文地址:https://www.cnblogs.com/liujiajia_me/p/12528958.html