Web 请求之--性能相关

本博客代码运行环境

     
     python :     Python 3.7.1rc1   version
     pip :        pip 19.1.1        version
     Scrapy:      scrapy 1.6.0      version
     asyncio:     asyncio 3.4.3     version
     aiohttp:     aiohttp 3.5.4     version
     gevent:      gevent 1.4.0      version
grequests: grequests 0.3.0 version greenlet: greenlet 0.4.15 version requests: requests 2.21.0 version urllib3: urllib3 1.24.1 version tornado: tornado 6.0.2 version twisted: twisted 19.2.0 version

以下所有代码都以 爬虫 为例, 发送 http 请求,获取页面源码。

概念回顾

操作系统(OS):
是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间。
进程(Process):------------》》是 OS 进行资源分配和调度的一个基本独立单位
    进程就是一个程序在一个数据集上的一次动态执行过程。
    进程一般由程序、数据集、进程控制块三部分组成。
    程序:       由我们编写用来描述进程要完成哪些功能以及如何完成,是指令、数据及其组织形式的描述,进程是程序的实体
    数据集:     则是程序在执行过程中所需要使用的资源;
    进程控制块: 用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
多进程:
指操作系统能同时运行多个任务(程序)。
总结来说:
     进程是线程的容器

每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位
适用于: 计算密集型

线程(Thread):------------》》是cpu调度和分派的最小单位
  有时被称为轻量级进程(Lightweight Process,LWP)

为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。
是程序执行流的最小单元。 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成
线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
由于线程之间的相互制约,致使线程在运行中呈现出间断性。
线程也有就绪、阻塞和运行三种基本状态
就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待 CPU 调度;
阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
运行状态是指线程占有 CPU 正在运行;
多线程:
在单个程序中同时运行多个线程完成不同的工作,即具有多个顺序流在执行。
  总结来说:
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
适用于: IO 密集型
GIL全局解释器锁(Global Interpreter Lock):
无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行
机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作
协程(Coroutine):
又称微线程,纤程。协程是一种用户态的轻量级线程。
线程/进程:操作系统提供的一种并发处理任务的能力,操作是由程序触发系统接口, 操作者:系统;
协程:程序员通过高超的代码能力,在代码执行流程中人为的实现多任务并发,是单个线程内的任务调度技巧。操作者:程序员;

没有切换的开销。协程不需要多线程的锁机制,因为都是在同一个线程中运行,所以没有同时访问数据的问题,执行效率比多线程高很多
协程存在的意义:对于多线程应用,CPU通过切片(轮询)的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。
协程切换时,因为只使用一个线程,是由程序自身控制的,体现的是程序员的流程控制能力,通过一定的语法规定在一个线程中所有代码块的执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程, 即: IO 密集型

综上: 最佳方案: 协程 + 多进程

线程与进程的共同点和区别 

(1)共同点:

     线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

(2)区别:

          线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了充分利用 CPU,提高效率。
     线程与进程的区别可以归纳为以下几点:
           1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

      2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

      3)调度和切换:线程上下文切换比进程上下文切换要快得多。故所消耗的资源比进程要少的多多。

      4)在多线程OS中,进程不是一个可执行的实体。
(3)线程的属性(特点):
       1)轻型实体
      线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如:
          在每个线程中都具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

    2)独立调度和分派的基本单位。
      在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。

    3)可并发执行。
      在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行。

    4)共享进程资源。
      在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:          
          所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。

HTTP 协议

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

HTTP是一个属于应用层的面向对象的协议,目前在WWW中使用的是 HTTP/1.0的第六版,

HTTP协议工作于客户端-服务端架构为上。基于 : 请求-----响应 ,模式

HTTP 特点

1、简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。                         
           每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

2、灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。

3.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

4.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。
         缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

HTTP 协议 数据发送格式

HTTP 请求协议

请求首行;         // 请求方式 请求路径 协议和版本,例如:GET /index.html HTTP/1.1
请求头信息;       // 请求头名称:请求头内容,即为key:value格式,例如:Host:localhost
空行;            // 用与分隔 请求头 和 请求体(\r\n\r\n)
请求体。          // GET没有请求体,只有POST有请求体。
GET:
HTTP默认的请求方法就是GET
      * 没有请求体
      * 数据量有限制!
      * GET请求数据会暴露在浏览器的地址栏中
GET请求常用的操作:
       1. 在浏览器的地址栏中直接给出URL,那么就一定是GET请求
       2. 点击页面上的超链接也一定是GET请求
       3. 提交表单时,表单默认使用GET请求,但可以设置为POST
例如:
GET 请求: GET /338.jpg HTTP/1.1\r\nHost:img.mukewang.com\r\nContent-Type:multipart/form-data\r\nReferer:http://www.imooc.com/\r\n\r\n POST 请求:
POST / HTTP/1.1\r\nHost: img.mukewang.com\r\nConnection: Keep-Alive\r\n\r\nname=Professional&publisher=Wiley

请求头:

1、Host

请求的web服务器域名地址

2、User-Agent

HTTP客户端运行的浏览器类型的详细信息。通过该头部信息,web服务器可以判断出http请求的客户端的浏览器的类型。

3、Accept

指定客户端能够接收的内容类型,内容类型的先后次序表示客户都接收的先后次序

4、Accept-Lanuage

指定HTTP客户端浏览器用来展示返回信息优先选择的语言

5、Accept-Encoding

指定客户端浏览器可以支持的web服务器返回内容压缩编码类型。表示允许服务器在将输出内容发送到客户端以前进行压缩,以节约带宽。
而这里设置的就是客户端浏览器所能够支持的返回压缩格式。

6、Accept-Charset

HTTP客户端浏览器可以接受的字符编码集

7、Content-Type

显示此HTTP请求提交的内容类型。一般只有post提交时才需要设置该属性

有关Content-Type属性值有如下两种编码类型:

(1)“application/x-www-form-urlencoded”: 表单数据向服务器提交时所采用的编码类型,默认的缺省值就是“application/x-www-form-urlencoded”。
     然而,在向服务器发送大量的文本、包含非ASCII字符的文本或二进制数据时这种编码方式效率很低。

(2)“multipart/form-data”: 在文件上载时,所使用的编码类型应当是“multipart/form-data”,它既可以发送文本数据,也支持二进制数据上载。

当提交为表单数据时,可以使用“application/x-www-form-urlencoded”;当提交的是文件时,就需要使用“multipart/form-data”编码类型。

HTTP 响应协议

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
例如:
HTTP/1.1 200 OK
    Date: Fri, 22 May 2009 06:07:21 GMT
    Content-Type: text/html; charset=UTF-8

   <html>
        <head></head>
           <body>
              <!--body goes here-->
          </body>
    </html>

 例:

         HTTP/1.1 200 OK\r\nDate:Fri, 22 May 2009 06:07:21 GMT\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><head</head><body>shiwei</body></html>

第一部分:状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。

             第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为(ok)

第二部分:消息报头,用来说明客户端要使用的一些附加信息

              第二行和第三行为消息报头,
              Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8

第三部分:空行,消息报头后面的空行是必须的
第四部分:响应正文,服务器返回给客户端的文本信息。

响应状态码:

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
1xx:指示信息--表示请求已接收,继续处理
2xx:成功--表示请求已被成功接收、理解、接受
3xx:重定向--要完成请求必须进行更进一步的操作
4xx:客户端错误--请求有语法错误或请求无法实现
5xx:服务器端错误--服务器未能实现合法的请求
常见状态码:
200 OK                        //客户端请求成功
400 Bad Request               //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized              //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 
403 Forbidden                 //服务器收到请求,但是拒绝提供服务
404 Not Found                 //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error     //服务器发生不可预期的错误
503 Server Unavailable        //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

准备好了吗?基础知识很重要!!

 编写爬虫程序,性能的消耗主要在 IO 请求中,当单进程单线程模式下请求URL时必然会引起等待,效率极低!

 1 import requests
 2 
 3 def task(url):
 4     response = requests.get(url)
 5     return response
 6 
 7 
 8 url_list = ['http://www.github.com', 'http://www.bing.com']
 9 
10 for url in url_list:
11     task(url)
同步执行
 1 from concurrent.futures import ThreadPoolExecutor
 2 import requests
 3 
 4 def task(url):
 5     response = requests.get(url)
 6     print(response)
 7     # print(response.text)  # 获取 页面 源码 字符串数据
 8     print("\033[1;30;47murl==%-70s----------response==%-s\033[0m"%(url,response.status_code))
 9     return response
10 
11 url_list = [
12     "http://tuijian.hao123.com/",
13     "http://www.163.com/",
14     "http://www.eastmoney.com/",
15 ]
16 
17 pool = ThreadPoolExecutor(5)
18 
19 for url in url_list:
20     v = pool.submit(task,url)
21 
22 pool.shutdown(wait=True)
多线程执行
from concurrent.futures import ThreadPoolExecutor
import requests

def task(url):
    """  返回页面 源码数据 """
    response = requests.get(url)
    print("\033[1;30;47murl==%-70s----------response==%-s\033[0m"%(url,response.status_code))
    return response

def done(future,*args, **kwargs):
    """  解析页面 源码  """
    print(future.result())

url_list = [
    "http://tuijian.hao123.com/",
    "http://www.163.com/",
    "http://www.eastmoney.com/",
]

pool = ThreadPoolExecutor(5)

for url in url_list:
    v = pool.submit(task,url)
    v.add_done_callback(done)

pool.shutdown(wait=True)
多线程 + 回调函数 
from concurrent.futures import ProcessPoolExecutor
import requests

pool = ProcessPoolExecutor(5)
def task(url):
    response = requests.get(url)
    print(response)
    print("\033[1;30;47murl==%-70s----------response==%-s\033[0m"%(url,response.status_code))
    return response

url_list = [
    "http://tuijian.hao123.com/",
    "http://www.163.com/",
    "http://www.eastmoney.com/",
]

if __name__ == "__main__":  # 必须要加 此 判断语句
    for url in url_list:
        v = pool.submit(task, url)

pool.shutdown(wait=True)
多进程执行
from concurrent.futures import ProcessPoolExecutor
import requests
pool = ProcessPoolExecutor(5)

def task(url):
    response = requests.get(url)
    # print("\033[1;30;47murl==%-70s----------response==%-s\033[0m"%(url,response.status_code))
    return response

def done(*args, **kwargs):
    print(args[0].result())
    print("\033[1;31;40m %s-----%s \033[0m"%(args, kwargs))
url_list = [
    "http://tuijian.hao123.com/",
    "http://www.163.com/",
    "http://www.eastmoney.com/",
]

if __name__ == "__main__":  # 必须要加 此 判断语句
    for url in url_list:
        v = pool.submit(task, url)
        v.add_done_callback(done)

pool.shutdown(wait=True)
多进程 + 回调函数

 以上代码 均 可提升请求性能, 但多线程 和 多进程 的缺点是 在 IO 阻塞时 依然会造成线程 和 进程的 浪费,

故 异步 IO   是 首选:  

                  遇到 IO , 则 立即转而操作其他, IO 完成时,则自动回调运行处理 IO 的 函数。

import asyncio
                                      # 不支持 HTTP 请求, 但支持 tcp 请求,socket
@asyncio.coroutine                    # 不可缺少
def task():
    print('before ......Task')
    yield from asyncio.sleep(5)      # 不可写为 time.sleep(5)
    print('after ...... Task')


tasks = [task(), task(), task()]      # 任务列表
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
asyncio 示例

     asyncio + 模拟 http :

import asyncio
@asyncio.coroutine
def task(host,url='/'):
    #  创建 链接
    reader, writer = yield from asyncio.open_connection(host, 80)

    request_header_content = """GET %s HTTP/1.0\r\nHost: %s\r\n\r\n"""%(url,host,)
    request_header_content = bytes(request_header_content, encoding='utf-8')

    writer.write(request_header_content)
    yield from writer.drain()
    text = yield from reader.read()
    print('ending ',host, url, text)
    writer.close()

tasks = [
    task('www.cnblogs.com', '/wupeiqi/'),
    task('www.cnblogs.com', '/shiwei1930/'),
    task('www.cnblogs.com', '/alex3714/'),
]

loop = asyncio.get_event_loop()
results = loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

    asyncio  + aiohttp :

import aiohttp
import asyncio

@asyncio.coroutine
def task(url):
    print(url)
    resp = aiohttp.ClientSession().get(url)
    response = yield from resp
    print(url, response)
    response.close()

tasks = [task('http://www.baidu.com/'), task('http://www.chouti.com/')]

loop = asyncio.get_event_loop()
results = loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

     asyncio + requests

import  requests
import asyncio
@asyncio.coroutine
def task(func,*args):
    loop = asyncio.get_event_loop()
    future = loop.run_in_executor(None,func,*args)
    response = yield from future
    print('\033[1;31;40m url=%-30s\n\033[0mcontents=%-30s'%(response.url,response.content))

tasks = [
    task(requests.get, 'http://www.baidu.com/'),
    task(requests.get, 'http://www.cnblogs.com/'),
    task(requests.get, 'http://autohome.com.cn'),
]

loop = asyncio.get_event_loop()
results = loop.run_until_complete(asyncio.gather(*tasks))
print(results)
loop.close()

     gevent + requests

import requests
import gevent
from gevent import monkey
monkey.patch_all()                 # 不可缺少

def task(method,url,req_kwargs):
    print('method=%s,url=%s,kwargs=%s'%(method,url,req_kwargs))
    response = requests.request(method=method,url=url, **req_kwargs)
    print('url=%s\n\t\tcontent=%s'%(response.url,response.content))

gevent.joinall([
    gevent.spawn(task,method='get', url='https://www.github.com/', req_kwargs={}),
    gevent.spawn(task, method='get', url='https://www.yahoo.com/', req_kwargs={}),
    gevent.spawn(task, method='get', url='https://www.python.org/', req_kwargs={}),
])

      grequests  =    gevent + requests

import grequests

request_list = [
    grequests.get('http://www.cnblogs.com/'),
    grequests.get('https://www.yahoo.com/'),
    grequests.get('https://www.github.com/'),
]

response_list = grequests.map(request_list)
print('result=',response_list)
for item  in response_list:
    print('content=',item.content)

   Twisted

from twisted.web.client import getPage,defer
from twisted.internet import reactor

def callback(contents):
    print(contents)

def all_done(arg):
    reactor.stop()  # 全部完成之后 stop

@defer.inlineCallbacks
def task(url):
    defered = getPage(bytes(url, encoding='utf8'))
    defered.addCallback(callback)
    yield defered
defered_list = []

url_list = ['http://www.cnblogs.com/', 'https://github.com/', 'http://baidu.com/']
for url in url_list:
    defered = task(url)
    defered_list.append(defered)

dlist = defer.DeferredList(defered_list)
dlist.addBoth(all_done)

# 事件循环
reactor.run()  # 去 defered_list 中 检测 谁回来了, 是个死 循环

    Tornado

from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPRequest
from tornado import ioloop

COUNT = 0
def handler_response(response):
    global COUNT
    COUNT -= 1
    if response.error:
        print('error:',response.error)
    else:
        print('body:',response.body)
    if COUNT == 0:
        ioloop.IOLoop.current().stop()

def task():
    url_list = [
        'http://www.baidu.com/',
        'http://www.cnblogs.com/',
        'https://www.github.com',
    ]
    global COUNT
    COUNT = len(url_list)
    for url in url_list:
        print("url===",url)
        http_client = AsyncHTTPClient()
        http_client.fetch(HTTPRequest(url),handler_response)
if __name__ == '__main__':
    ioloop.IOLoop.current().add_callback(task)
    ioloop.IOLoop.current().start()   #     # 事件循环, 是一个 死循环

自定义 异步 IO 框架

 最基本的 网络编程的 本质:客户端

import socket
sk = socket.socket()
# 链接
sk.connect(('www.baidu.com',80))  # IO 阻塞

# 设置 非阻塞
# sk.setblocking(False)  # 请求是 发过去了

print('链接成功!')

sk.send(b'GET / HTTP/1.0\r\nHost: baidu.com\r\n\r\n')
# sk.send(b'POST / HTTP/1.0\r\nHost: baidu.com\r\n\r\n fdkfjdkjfkdfj')

buffer_size = 1024
# 等待 着 服务端响应
data  = sk.recv(buffer_size)  # IO 阻塞

print(data)
# 关闭链接
sk.close()

自定义 异步 IO 框架-----》客户端

import select
import socket

# IO  多路复用: 监听多个 Socket 对象 ,死循环, 事件循环
#         - 利用此特性 可以开发 异步 IO 模块 等等

# 异步 IO : 遇到 IO , 则 立即转而操作其他, IO 返回时,则自动回调处理 IO 的 函数。
# 回答: 非阻塞的 socket(setblocking(False)) + IO 多路复用

#                    此时 为客户端
class HttpRequest:
    def __init__(self, sk, host, callback):
        self.socket = sk
        self.host = host
        self.callback = callback
    def fileno(self):
        return self.socket.fileno()

Buffer_size = 4096

class HttpResponse:
    def __init__(self, recv_data):
        self.recv_data = recv_data
        self.header_dict = {}
        self.body = ""
        self.initialize()

    def initialize(self):
        headers, body = self.recv_data.split(b'\r\n\r\n', 1)
        header_list = headers.split(b'\r\n')
        for item in header_list:
            item_str = str(item, encoding='utf-8')
            sp = item_str.split(':', 1)
            if len(sp) == 2:
                self.header_dict[sp[0]] = sp[1]

class AsyncRequest:
    def __init__(self):
        self.conn = []
        self.connection = []  # 用于检测 是否链接成功

    def add_request(self, host,callback):
        try:
            sk = socket.socket()
            sk.setblocking(False)
            sk.connect((host, 80))
        except BlockingIOError as e:
            pass
        request = HttpRequest(sk, host, callback)
        self.conn.append(request)
        self.connection.append(request)

    def run(self):
        # 事件 循环
        while True:
            rlist, wlist, elist = select.select(self.conn, self.connection, self.conn, 0.05)
            for w in wlist:
                print(w.host,'链接成功')
                # 表示 socket  和服务端链接 成功
                txt = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n"%(w.host)
                w.socket.send(bytes(txt, encoding='utf8'))
                self.connection.remove(w)
            print('write over')
            for r in rlist:
                print(r.host, '有数据返回 ')
                # r = HttpRequest object
                recv_data = bytes()
                while True:
                    try:
                        chunck = r.socket.recv(Buffer_size)
                        recv_data += chunck
                    except Exception as e:
                        break

                response = HttpResponse(recv_data)

                r.callback(response)
                print('host=%s,返回数据为: recv_data=%s'%(r.host,recv_data))
                self.conn.remove(r)

            if not self.conn:
                break

            self.connection
            self.conn

def f1(response):
    print('保存到文件,', response.header_dict)
def f2(response):
    print('保存到数据库,', response.header_dict)
url_list = [
    # 'cn.bing.com',
    {"host": 'www.baidu.com', 'callback': f1},
    {'host': 'www.cnblogs.com', 'callback': f2},
]

req = AsyncRequest()
for item in url_list:
    req.add_request(item['host'], item['callback'])

req.run()

# 等待 服务端的 链接成功 信号

 

posted @ 2019-07-06 14:43  梭梭666  阅读(318)  评论(0编辑  收藏  举报
返回顶部