python 协程爬虫_Python爬虫进阶教程(二):线程、协程
简介
线程
线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。 线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。
多线程类似同时执行多个不同程序,python的标准库提供了两个模块thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装,大多情况下,我们只使用threading模块即可。
threading模块
此模块在较低级别的thread模块之上构建的更高级别的线程接口,一般通过两种方式实现多线程,第一种方式是把一个函数传入并创建实例,然后调用start方法执行;第二种方式是直接从threading.Thread继承并创建线程类,然后重写init方法和run方法。
第一种方式代码示例:
import time
import random
import threading
'''更多Python学习资料以及源码教程资料,可以在群821460695 免费获取'''
def t_run(urls):
"""线程执行代码"""
# threading.current_thread()返回当前的Thread对象,对应于调用者控制的线程。
# 如果调用者控制的线程不是通过threading模块创建的,则返回一个只有有限功能的虚假线程对象。
print('Current%sis running...' % threading.current_thread().name)
for url in urls:
print(' threading%s----->%s' % (threading.current_thread().name, url))
time.sleep(random.random())
print('%sended.' % threading.current_thread().name)
if __name__ == '__main__':
# 创建两个线程实例
t1 = threading.Thread(target=t_run, name='Thread_1', args=(['url1', 'url2'],))
t2 = threading.Thread(target=t_run, name='Thread_2', args=(['url3', 'url4'],))
# 启动线程
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print('%sended.' % threading.current_thread().name)
运行结果如下:
Current Thread_1 is running...
threading Thread_1 -----> url1
Current Thread_2 is running...
threading Thread_2 -----> url3
threading Thread_1 -----> url2
threading Thread_2 -----> url4
Thread_2 ended.
Thread_1 ended.
MainThread ended.
第二种方式用threading.Thread继承创建线程类
import time
import random
import threading
class MyThread(threading.Thread):
"""定义线程类"""
def __init__(self, name, urls):
"""初始化,重写线程"""
threading.Thread.__init__(self, name=name)
self.urls = urls
def run(self):
"""执行函数"""
# 打印当前线程名
print('Current%sis running...' % threading.current_thread().name)
for url in self.urls:
print('Thread%s------>%s' % (threading.current_thread().name, url))
time.sleep(random.random())
print('%sended.' % threading.current_thread().name)
if __name__ == '__main__':
print('%sis running...' % threading.current_thread().name)
t1 = MyThread(name='Thread_1', urls=['url1', 'url2'])
t2 = MyThread(name='Thread_2', urls=['url3', 'url4'])
t1.start()
t2.start()
t1.join()
t2.join()
print('%sended.' % threading.current_thread().name)
结果如下:
MainThread is running...
Current Thread_1 is running...
Thread Thread_1 ------> url1
Current Thread_2 is running...
Thread Thread_2 ------> url3
Thread Thread_1 ------> url2
Thread Thread_2 ------> url4
Thread_1 ended.
Thread_2 ended.
MainThread ended.
线程同步
如果多个线程共同对某个数据进行修改,就有可能会造成不可预料的结果,为了防止这种情况发生,需要对线程进行同步,使用Lock和Rlock可以实现简单线程同步。
Lock 对象
一个可重入锁处于“locked”或者“unlocked”状态中的一种。它创建时处于unlocked状态。它有两个基本方法,acquire()和release()。当状态是unlocked时,acquire()改变该状态为locked并立即返回。当状态被锁定时,acquire()阻塞,直到在另一个线程中对release()的调用将其改为unlocked,然后acquire()执行,release()方法只应在锁定状态下调用;它将状态更改为已解锁并立即返回。如果尝试释放已解锁的锁,将会引发RuntimeError。
Rlock 对象
一个可重入锁必须由获得它的线程释放。一旦线程获得了可重入锁,同一线程可以再次获取它而不阻塞;在所有的release操作完成后,别的线程才能申请Rlock对象,见下面例子:
import threading
# 创建Rlock实例
lock = threading.RLock()
# 定义变量
num = 0
class MyThread(threading.Thread):
"""定义线程类"""
def __init__(self, name):
"""重新定义name"""
threading.Thread.__init__(self, name=name)
def run(self):
"""执行函数"""
# 全局变量num
global num
while True:
# 加锁
lock.acquire()
print('%slocked, Number:%d' % (threading.current_thread().name, num))
if num >= 4:
# 解锁
lock.release()
print('%sreleased, Number:%d' % (threading.current_thread().name, num))
break
num += 1
print('%sreleased, Number:%d' % (threading.current_thread().name, num))
lock.release()
if __name__ == '__main__':
thread1 = MyThread('Thread_1')
thread2 = MyThread('Thread_2')
thread3 = MyThread('Thread_3')
thread1.start()
thread2.start()
thread3.start()
运行结果如下:
Thread_1 locked, Number: 0
Thread_1 released, Number: 1
Thread_1 locked, Number: 1
Thread_1 released, Number: 2
Thread_1 locked, Number: 2
Thread_1 released, Number: 3
Thread_1 locked, Number: 3
Thread_1 released, Number: 4
Thread_1 locked, Number: 4
Thread_1 released, Number: 4
Thread_2 locked, Number: 4
Thread_2 released, Number: 4
Thread_3 locked, Number: 4
Thread_3 released, Number: 4
可以看出Rlock锁只有线程1的num为4时,调用release方法,全部解锁后,线程2才可以调用,线程2开始时num就是4,所以也直接到if判断结束,调用release后,线程3开始执行。
全局解释器锁(GIL)
首先说的一点是GIL并不是Python的特性,它是Python解析器(CPython)引入的一个概念。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。 GIL全称Global Interpreter Lock,是一个互斥锁,它可以防止多个本地线程同时执行Python的某个值,毫无疑问全局锁的存在会对多线程的效率有不小影响。几乎等于Python是个单线程的程序。(这也是大家吐槽python多线程慢的槽点)
协程
协程又称微线程、纤程,就好比同时开启多个任务,但一次只顺序执行一个。等到所执行的任务遭遇阻塞,就切换到下一个任务继续执行,以期节省下阻塞所占用的时间。 协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。对CPU来说协程就是单线程,不必考虑切换开销。 那么python如何实现协程呢?Python对协程的支持是通过generator实现的。在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。见下面简单生产者消费者示例:
def consumer():
r = ''
while True:
# 这个地方注意,到达这个yield后,就会抛出n的值,暂停等待next或send继续
n = yield r
if not n:
return
print('[CONSUMER] Consuming%s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Porducing%s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return:%s...' % r)
c.close()
c = consumer()
produce(c)
结果如下:
[PRODUCER] Porducing 1 ...
[CONSUMER] Consuming 1 ...
[PRODUCER] Consumer return: 200 OK...
[PRODUCER] Porducing 2 ...
[CONSUMER] Consuming 2 ...
[PRODUCER] Consumer return: 200 OK...
[PRODUCER] Porducing 3 ...
[CONSUMER] Consuming 3 ...
[PRODUCER] Consumer return: 200 OK...
[PRODUCER] Porducing 4 ...
[CONSUMER] Consuming 4 ...
[PRODUCER] Consumer return: 200 OK...
[PRODUCER] Porducing 5 ...
[CONSUMER] Consuming 5 ...
[PRODUCER] Consumer return: 200 OK...
注意到consumer函数是一个generator,把一个consumer传入produce后: 首先调用c.send(None)启动生成器; 然后,一旦生产了东西,通过c.send(n)切换到consumer执行; consumer通过yield拿到消息,处理,又通过yield把结果传回; produce拿到consumer处理的结果,继续生产下一条消息; produce决定不生产了,通过c.close()关闭consumer,整个过程结束。 整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。 最后套用Donald Knuth的一句话总结协程的特点: “子程序就是协程的一种特例。”
asyncio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。用asyncio实现Hello world代码如下:
import asyncio
# 把一个generator标记为协程类型
@asyncio.coroutine
def hello():
"""定义一个生成器"""
print('Hello world!')
# 通过yield from调用另一个标记为协程的生成器
r = yield from asyncio.sleep(2)
print('Hello again!')
# 生成实例
loop = asyncio.get_event_loop()
# 将标记为协程的生成器执行
loop.run_until_complete(hello())
loop.close()
执行结果:
Hello world!
# 此处等待了2秒
Hello again!
从结果可以看出hello()会首先打印出Hello world!,然后,yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。 我们用Task封装两个coroutine试试:
import asyncio
import threading
@asyncio.coroutine
def hello():
print('Hello world!%s' % threading.current_thread())
r = yield from asyncio.sleep(5)
print('Hello again!%s' % threading.current_thread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出结果如下:
Hello world! <_mainthread started>
Hello world! <_mainthread started>
# 此处暂停5秒
Hello again! <_mainthread started>
Hello again! <_mainthread started>
由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。 如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。 我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:
import asyncio
@asyncio.coroutine
def wget(host):
print('wget%s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost:%s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%sheader >%s' % (host, line.decode('utf-8').rstrip()))
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.baidu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出结果如下:
wget www.163.com ...
wget www.sina.com.cn ...
wget www.baidu.com ...
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
www.163.com header > Date: Sat, 06 Jan 2018 14:14:58 GMT
www.163.com header > Content-Length: 0
www.163.com header > Location: http://www.163.com/special/0077jt/error_isp.html
www.163.com header > Connection: close
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Server: nginx
www.sina.com.cn header > Date: Sat, 06 Jan 2018 14:12:15 GMT
www.sina.com.cn header > Content-Type: text/html
www.sina.com.cn header > Content-Length: 605048
www.sina.com.cn header > Connection: close
www.sina.com.cn header > Last-Modified: Sat, 06 Jan 2018 14:09:06 GMT
www.sina.com.cn header > Vary: Accept-Encoding
www.sina.com.cn header > Expires: Sat, 06 Jan 2018 14:13:12 GMT
www.sina.com.cn header > Cache-Control: max-age=60
www.sina.com.cn header > X-Powered-By: shci_v1.03
www.sina.com.cn header > Age: 3
www.sina.com.cn header > Via: http/1.1 cnc.beixian.ha2ts4.205 (ApacheTrafficServer/6.2.1 [cMsSf ]), http/1.1 gwbn.beijing.ha2ts4.23 (ApacheTrafficServer/6.2.1 [cHs f ])
www.sina.com.cn header > X-Via-Edge: 15152479356296c6422730904eedb7d91845d
www.sina.com.cn header > X-Cache: HIT.23
www.sina.com.cn header > X-Via-CDN: f=edge,s=gwbn.beijing.ha2ts4.21.nb.sinaedge.com,c=115.34.100.108;f=Edge,s=gwbn.beijing.ha2ts4.23,c=219.238.4.21
www.baidu.com header > HTTP/1.1 200 OK
www.baidu.com header > Date: Sat, 06 Jan 2018 14:12:15 GMT
www.baidu.com header > Content-Type: text/html
www.baidu.com header > Content-Length: 14613
www.baidu.com header > Last-Modified: Fri, 29 Dec 2017 03:29:00 GMT
www.baidu.com header > Connection: Close
www.baidu.com header > Vary: Accept-Encoding
www.baidu.com header > Set-Cookie: BAIDUID=BEA2CAC2706F8386AAC50DEEC6287BD9:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
www.baidu.com header > Set-Cookie: BIDUPSID=BEA2CAC2706F8386AAC50DEEC6287BD9; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
www.baidu.com header > Set-Cookie: PSTM=1515247935; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
www.baidu.com header > P3P: CP=" OTI DSP COR IVA OUR IND COM "
www.baidu.com header > Server: BWS/1.1
www.baidu.com header > X-UA-Compatible: IE=Edge,chrome=1
www.baidu.com header > Pragma: no-cache
www.baidu.com header > Cache-control: no-cache
www.baidu.com header > Accept-Ranges: bytes
async/await
asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。 为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。 请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:把@asyncio.coroutine替换为async;
把yield from替换为await。
'''更多Python学习资料以及源码教程资料,可以在群821460695 免费获取'''
那么6部分的hello代码就可以这样替换了
# 把一个generator标记为协程类型
@asyncio.coroutine
def hello():
"""定义一个生成器"""
print('Hello world!')
# 通过yield from调用另一个标记为协程的生成器
r = yield from asyncio.sleep(2)
print('Hello again!')
替换为
async def hello():
"""定义一个生成器"""
print('Hello world!')
# 通过await调用另一个标记为协程的生成器
r = await asyncio.sleep(2)
print('Hello again!')
其它位置的代码均不变化。
python 协程爬虫_Python爬虫进阶教程(二):线程、协程相关推荐
- python 保留顺序去重_Python数据分析入门教程(二):数据预处理
作者 | CDA数据分析师 从菜市场买来的菜,总有一些不太好的,所以把菜买回来以后要先做一遍预处理,把那些不太好的部分扔掉.现实中大部分的数据都类似于菜市场的菜品,拿到以后都要先做一次预处理. 常见的 ...
- python 正则表达式提取数据_Python爬虫教程-19-数据提取-正则表达式(re)
本篇主页内容:match的基本使用,search的基本使用,findall,finditer的基本使用,匹配中文,贪婪与非贪婪模式 Python爬虫教程-19-数据提取-正则表达式(re) 正则表达式 ...
- python编程理论篇_Python爬虫入门实战之猫眼电影数据抓取(理论篇)
前言 本文可能篇幅较长,但是绝对干货满满,提供了大量的学习资源和途径.达到让读者独立自主的编写基础网络爬虫的目标,这也是本文的主旨,输出有价值能够真正帮助到读者的知识,即授人以鱼不如授人以渔,让我们直 ...
- python线程池并发爬虫_python 并发专题(二):python线程以及线程池相关以及实现...
一 多线程实现 线程模块 - 多线程主要的内容:直接进行多线程操作,线程同步,带队列的多线程: Python3 通过两个标准库 _thread 和 threading 提供对线程的支持. _threa ...
- origin和python有什么不同_python爬虫之git的使用(origin说明)
1.首先我们回忆两个命令 #git remote add origin 远程仓库链接 #git push -u origin master 我们一起看看这个命令,git是git的一级命令,push就是 ...
- python从入门到爬虫_python爬虫从入门到放弃(一)之初识爬虫
什么是爬虫? 网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本.另外一些不常使用的名字还有蚂蚁.自动索引. ...
- python高级教程_Python高级进阶教程
这个系列的教程是在刘金玉编程的<零基础python教程>基础上的高级进阶应用. 教程列表: Python高级进阶教程001期 pycharm+anaconda3+pyqt5可视化界面开发环 ...
- python教程自带数据库_Python入门进阶教程-数据库操作
Python数据库操作Python版本3.8.0,开发工具:Pycharm 建议本节在掌握了数据库相关操作后再进行学习 MySQL 是最流行的关系型数据库管理系统.本小节通过Python对MySQL数 ...
- python二手交易平台代码_PYTHON爬虫实战_垃圾佬闲鱼爬虫转转爬虫数据整合自用二手急速响应捡垃圾平台_3(附源码持续更新)...
说明 文章首发于HURUWO的博客小站,本平台做同步备份发布. 如有浏览或访问异常图片加载失败或者相关疑问可前往原博客下评论浏览. 原文链接 PYTHON爬虫实战_垃圾佬闲鱼爬虫转转爬虫数据整合自用二 ...
- 51自学网python爬虫_Python爬虫基本流程
爬虫定义 爬虫是请求网站并提取自己所需要数据的过程.通过我们的程序,可以代替我们向服务器发送请求,然后进行批量的数据下载. 爬虫基本流程 发起请求 通过url向服务器发送requests请求,请求可以 ...
最新文章
- Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析
- 【.NET架构】BIM软件架构02:Web管控平台后台架构
- linux环境下安装gcc
- linux 下source命令
- centos 6.5 安装redis
- Connection reset原因分析和解决方案
- thinkphp 每个页面自定义加载对应指定的js、css
- python的pyaudio教程入门_Python音频操作工具PyAudio上手教程详解
- 电脑上如何进行录屏?
- 实验3matlab的符号运算,实验3 Matlab 符号运算及求函数极值
- easyui的combobox根据拼音搜索选项
- 黄素单核苷酸小麦麦清白蛋白纳米粒|石杉碱甲乳清白蛋白纳米粒Huperzine-whey protein|化学试剂
- 计算机域 用户怎么设置,电脑怎么加域设置
- H3CNE《第1章 计算机网络概述》
- WARN: Establishing SSL connection without server‘s identity verification is not recommended 的解决方法
- 记一次Oracle 11g xe 导出导入使用过程
- odoo中关于打印word格式的文件,利用docxtemplate方法
- Hadoop快速入门——入门考试(伪分布式60+编码25+执行jar15)
- linux常用命令 cp命令的使用和介绍
- Sensitivity and specificity 敏感性与特异性
热门文章
- postman导出请求url_postman接口测试之复制多个接口或collections到某个子文件夹或collections下...
- 欢乐大作战服务器维护,欢乐大作战
- HTML5实现输入密码(六个格子)
- 自定义RecyclerView.ItemDecoration,实现RecyclerView的分割线效果
- php传引用和全局变量
- 不要让别人左右自己的心情
- rhel6中dhcp服务器配置文件,如何在CentOS/RHEL 7/6/5配置DHCP服务器
- java插件安装步骤_eclipse插件安装的四种方法
- Emacs快速切换行号开关
- DECLARE_META_INTERFACE(INTERFACE)和IMPLEMENT_META_INTERFACE(INTERFACE, NAME)宏定义分析