Python爬虫之异步讲解

1 异步爬虫

1.1 异步了解

使用高性能爬虫可以缩短爬取用时,提供爬取效率
目的:在爬虫中使用异步实现高性能的数据爬取操作
异步爬虫的方式有:

  • 多线程和多进程
    好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行
    坏处:无法无限制的开启多线程或者多进程(如果不限制的开启了,会严重消耗CPU资源,这样会导致响应外界效率变慢)
  • 线程池和进程池
    好处:我们可以降低系统对进场或者线程创建和销毁的一个频率,从而很好的降低系统的开销
    坏处:池中线程或者进程的数量是有上限的,倘若远远超过了上限,爬取效率就会下降

2 多线程

2.1 多线程讲解

多线程类似于同时执行多个不同程序,多线程运行,使用线程可以把占据长时间的程序中的任务放到后台去处理。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
线程可以被抢占(中断)。
在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
线程可以分为:

  • 内核线程:由操作系统内核创建和撤销。
  • 用户线程:不需要内核支持而在用户程序中实现的线程。

2.2 thread模块

thread模块已被废弃。用户可以使用threading模块代替。所以,在 Python3 中不能再使用thread模块。为了兼容性,Python3thread 重命名为 _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 通过两个标准库 _threadthreading 提供对线程的支持
_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 aasync ba中的某一步有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()
posted @ 2021-08-09 16:05  上善若泪  阅读(1512)  评论(0编辑  收藏  举报