Python爬虫之异步讲解
1 异步爬虫
1.1 异步了解
使用高性能爬虫可以缩短爬取用时,提供爬取效率
目的:在爬虫中使用异步实现高性能的数据爬取操作
异步爬虫的方式有:
- 多线程和多进程
好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行
坏处:无法无限制的开启多线程或者多进程(如果不限制的开启了,会严重消耗CPU
资源,这样会导致响应外界效率变慢) - 线程池和进程池
好处:我们可以降低系统对进场或者线程创建和销毁的一个频率,从而很好的降低系统的开销
坏处:池中线程或者进程的数量是有上限的,倘若远远超过了上限,爬取效率就会下降
2 多线程
2.1 多线程讲解
多线程类似于同时执行多个不同程序,多线程运行,使用线程可以把占据长时间的程序中的任务放到后台去处理。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
线程可以被抢占(中断)。
在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) — 这就是线程的退让。
线程可以分为:
- 内核线程:由操作系统内核创建和撤销。
- 用户线程:不需要内核支持而在用户程序中实现的线程。
2.2 thread模块
thread
模块已被废弃。用户可以使用threading
模块代替。所以,在 Python3
中不能再使用thread
模块。为了兼容性,Python3
将 thread
重命名为 _thread
调用 _thread
模块中的start_new_thread()
函数来产生新线程。语法如下:
_thread.start_new_thread ( function, args[, kwargs] )
参数说明:
function
– 线程函数。args
– 传递给线程函数的参数,它必须是个tuple
类型kwargs
– 可选参数
使用例子:
import _thread
import time
# 定义一个函数
def print_time(threadName,delay):
count=0
while count<5:
time.sleep(delay)
count+=1
print ("%s: %s" % ( threadName, time.ctime(time.time()) ))
try:
_thread.start_new_thread(print_time,("test_thread_1",2))
_thread.start_new_thread(print_time,("test_thread_2",4))
except:
print("error:无法启动线程")
# 让脚本不要停下来
while 1:
pass
2.3 threading
Python3
通过两个标准库 _thread
和 threading
提供对线程的支持
_thread
提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading
模块的功能还是比较有限的。
threading
模块除了包含 _thread
模块中的所有方法外,还提供的其他方法:
threading.currentThread():
返回当前的线程变量。threading.enumerate():
返回一个包含正在运行的线程的list
。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。threading.activeCount():
返回正在运行的线程数量,与len(threading.enumerate())
有相同的结果。
除了使用方法外,线程模块同样提供了Thread类
来处理线程,Thread类
提供了以下方法:
run():
用以表示线程活动的方法start():
启动线程活动join([time]):
等待至线程中止
join
:让主线程
等待子线程
结束之后才能继续运行,比如如下程序,看着是thread2
调用了join
方法,其实是当前线程在运行,所以当前main
线程要等待thread2
运行完毕后,才能运行main
线程
thread2 = myThread(2, "Thread-2", 2)
thread2.start()
thread2.join()
isAlive():
返回线程是否活动的getName():
返回线程名setName():
设置线程名
使用例子:
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("开始线程:" + self.name)
print_time(self.name, self.counter, 5)
print ("退出线程:" + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启新线程
thread1.start()
thread2.start()
print("=========================")
thread1.join()
thread2.join()
print ("退出主线程")
3 线程池
3.1 单线程串行
单线程串行就是阻塞连续执行命令,假如有一个耗时时间长,就会一直等待到执行完毕,如下操作大概耗时8秒
import time
def get_page(str):
print('正在下载:',str)
time.sleep(2)
print('下载成功:',str)
name_list=['xiaozi','aa','bb','cc']
start_time=time.time()
for i in range(len(name_list)):
get_page(name_list[i])
end_time=time.time()
print(f'消耗时间secode:{end_time-start_time}')
3.2 使用线程池
导入线程池使用:from multiprocessing.dummy import Pool
如下操作,就是使用线程池后大概2秒
import time
# 导入线程池
from multiprocessing.dummy import Pool
start_time=time.time()
def get_page(str):
print('正在下载:',str)
time.sleep(2)
print('下载成功:',str)
name_list=['xiaozi','aa','bb','cc']
# 实例化一个线程池
pool=Pool(4)
# 第一个参数是要阻塞的函数,第二个参数是可迭代对象
# 如果第一个参数即阻塞函数有返回值,那么就会通过map返回回去
pool.map(get_page,name_list)
end_time=time.time()
print(f'消耗时间secode:{end_time-start_time}')
4 协程操作
最推荐的不是线程池,而是单线程和协程一起操作
4.1 协程基本概念
使用协程中的一般概念:
event_loop:
事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行coroutine:
协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。可以使用async
关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象task:
任务,它是对协程对象的进一步封装,包含了任务的各个状态future:
代表将来执行或还没有执行的任务,实际上和task
没有本质区别async:
定义一个协程,不会立即执行
await:
用来挂起阻塞方法的执行
4.2 协程基本操作
4.2.1 协程对象
使用async
定义一个协程对象,并创建一个事件循环对象
import asyncio
#定义协程对象
async def get_request(url):
print("正在请求的url是:",url)
print('请求成功的url:',url)
return url
#得到协程对象
coroutine_obj=get_request('www.baidu.com')
#创建一个事件循环对象
loop=asyncio.get_event_loop()
#将协程对象注册到loop中,并启动loop
loop.run_until_complete(coroutine_obj)
loop.close()
4.2.2 task对象
task对象需要loop对象基础上建立起来
import asyncio
#定义协程对象
async def get_request(url):
print("正在请求的url是:",url)
print('请求成功的url:',url)
return url
#得到协程对象
coroutine_obj=get_request('www.baidu.com')
#创建一个事件循环对象
loop=asyncio.get_event_loop()
#基于loop创建了一个task对象
task=loop.create_task(coroutine_obj)
print(task)
#基于loop注册任务
loop.run_until_complete(task)
print(task)
loop.close()
4.2.3 future对象
future对象与task对象不同的是创建基于asyncio空间来创建的
import asyncio
#定义协程对象
async def get_request(url):
print("正在请求的url是:",url)
print('请求成功的url:',url)
return url
#得到协程对象
coroutine_obj=get_request('www.baidu.com')
#创建一个事件循环对象
loop=asyncio.get_event_loop()
#基于loop创建了一个task对象
future=asyncio.ensure_future(coroutine_obj)
print(future)
loop.run_until_complete(future)
print(future)
loop.close()
4.2.4 绑定回调
在使用task
或者future
绑定回调时,需要先定义回调函数
4.2.4.1 定义回调函数
回调函数中返回的result
方法就是任务对象
中封装的协程对象
对应的函数返回值
注意:
回调函数必须有返回值,不然result
方法就没有值
def callback_func(task):
print(task.result())
4.2.4.2 绑定回调
在使用task
或者future
绑定回调时,都可以使用方法绑定task.add_done_callback(callback_func)
import asyncio
#定义协程对象
async def get_request(url):
print("正在请求的url是:",url)
print('请求成功的url:',url)
return url
#得到协程对象
coroutine_obj=get_request('www.baidu.com')
loop=asyncio.get_event_loop()
future=asyncio.ensure_future(coroutine_obj)
#把回调函数绑定到任务对象中
future.add_done_callback(callback_func)
loop.run_until_complete(future)
loop.close()
4.2.5 异步多任务
首先说明下async\await
的使用
正常的函数在执行时是不会中断的,所以要写一个能够中断的函数,就需要添加async
关键字
async
用来声明一个函数为异步函数
,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行。
await
用来用来声明程序挂起
,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await
后面只能跟异步程序或有__await__
属性的对象,因为异步程序与一般程序不同。假设有两个异步函数async a
,async b
,a
中的某一步有await
,当程序碰到关键字await b()
后,异步程序挂起后去执行另一个异步b
程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管b
是否执行完,要马上从b
程序中跳出来,回到原程序执行原来的操作。
如果await
后面跟的b
函数不是异步函数,那么操作就只能等b执行
完再返回,无法在b
执行的过程中返回。如果要在b
执行完才返回,也就不需要用await
关键字了,直接调用b函数
就行。所以这就需要await``后面跟的是异步函数了。 在一个异步函数中,可以不止一次挂起,也就是可以用多个``await
另外多任务时,对于run_until_complete
方法需要这样用asyncio.wait()
方法处理:loop.run_until_complete(asyncio.wait(task_list))
代码示例:
import time
import asyncio
async def get_request(url):
print("正在请求的url是:",url)
#在异步协程中如果出现了同步模块相关代码,那么就无法实现异步
# time.sleep(2)
#当在asyncio中遇到阻塞操作就必须进行手动挂起
await asyncio.sleep(2)
print('请求成功的url:',url)
start_time=time.time()
urls=['www.baidu.com','www.sogou.com','www.goubanjia.com']
#任务列表
task_list=[]
for url in urls:
coroutine_obj=get_request(url)
future=asyncio.ensure_future(coroutine_obj)
task_list.append(future)
loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task_list))
loop.close()
print(time.time()-start_time)
4.2.6 aiohttp模块
由于在使用异步多任务时,就不能用request.get()
,因为此方法是同步的,需要使用aiohttp
模块了
在使用aiohttp
模块先安装环境:pip intall aiohttp
,使用该模块中的ClientSession
使用时需要用async
修饰为异步,并用await
修饰耗时操作
async def get_page(url):
async with aiohttp.ClientSession() as session:
async with await session.get(url) as resp:
#此处是和同步获取文本方法不一样地方
#text()获取响应数据,read()获取二进制响应数据,json()返回的是json对象
page_text=await resp.text()
微信赞赏支付宝赞赏