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 + 模拟 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() # 等待 服务端的 链接成功 信号