文章目录
- 并发编程
- 1、进程、线程与协程
- 【1】进程概念
- 【2】线程的概念
- 线程的生命周期
- 进程与线程的区别
- 【3】协程(Coroutines)
- 2、多线程实现
- 【1】threading模块
- 【2】线程应用案例
- 【3】线程池
- 【4】互斥锁(同步锁)
- 【5】线程队列
- (1)队列的基本语法
- (2)生产者-消费者模型
- 3、多进程实现
- 4、协程并发
- 【1】Greenlet库
- 【2】asyncio模块
- (1)基本使用
- (2)任务对象
- (3)新版本语法支持
- 【3】基于协程的异步爬虫应用
- (1)同步请求爬虫
- (2)基于asyncio库的异步爬虫
并发编程
并发编程是一种编程模式,旨在使程序能够同时执行多个任务或操作。它涉及到同时处理多个独立任务的能力,这些任务可以在同一时间段内或者在不同的时间段内并行执行。
在传统的单线程编程模型中,程序按照顺序依次执行指令,每个操作都必须在上一个操作完成后才能开始。这种模型的缺点是,在处理耗时的操作时,程序可能会出现停顿或阻塞,导致执行效率低下。
并发编程通过引入多个执行线程或进程,使得程序能够并行执行多个任务,从而提高系统的吞吐量和响应性能。每个线程或进程可以独立地执行任务,它们之间可以交替执行,或者同时执行不同的任务。这种并行执行的方式可以充分利用多核处理器的优势,同时还可以提升对输入/输出等耗时操作的处理效率。
总之,并发编程是一种利用多线程或多进程实现并行执行的编程模式,它可以提高程序的性能和响应性,并充分利用多核处理器的潜力。然而,并发编程也带来了一些挑战,如资源竞争和数据同步,需要合理地设计和管理以确保程序的正确性和可靠性。
1、进程、线程与协程
【1】进程概念
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
多道技术:空间复用+时间复用,于是有了多进程!
进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成。
例子:我和我的女朋友们的故事我就是CPU,我跟三个女朋友玩就是三个任务1. 我教第一个女朋友做菜,菜谱就是程序,食材就是数据,我做饭的过程就是一个进程(切换,状态保存)2. 我给第二个女朋友治疗脚伤,医疗手册就是程序,医药箱就是数据,治疗脚伤的过程就是第二个进程。。。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
【2】线程的概念
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
线程的生命周期
在单个处理器运行多个线程时,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统。
我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。
线程的生命周期# 创建:一个新的线程被创建,等待该线程被调用执行;
# 就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
# 运行:此线程正在执行,正在占用时间片;
# 阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
# 退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
进程与线程的区别
前面讲了进程与线程,但可能你还觉得迷糊,感觉他们很类似。的确,进程与线程有着千丝万缕的关系,下面就让我们一起来理一理:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
【3】协程(Coroutines)
协程又叫用户空间线程,是为了解决线程上下文切换慢以及内存开销的问题。在一个线程中开很多协程,此时协程的调度是由用户级别的调度器执行的,而不是系统级别的切换。此时通过用户进程的调度器把每次拿到的协程交给线程,而不是直接交给整个任务。
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber
),或者绿色线程(GreenThread
)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程解决的是线程的切换开销和内存开销的问题
将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。此模式中,用户级线程对操作系统不可见(即透明)。
优点: 这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。
2、多线程实现
【1】threading模块
threading创建通过主线程创建子线程,之后多个线程一起执行,主线程结束程序不会结束,只有当所有线程都结束程序才会结束。可以通过join来阻塞线程。
通过对每一次socket传入的coon对象都创建一个对应的线程,可以实现了可以一对多的CS架构
Python提供两个模块进行多线程的操作,分别是thread
和threading
,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。
import timedef spider01():print("spider01 start")time.sleep(3)print("spider01 end")def spider02():print("spider02 start")time.sleep(5)print("spider02 end")spider01()
spider02()
多线程并发版本
import threading
import timedef spider01(timer):print("spider01 start")time.sleep(timer) # 模拟IOprint("spider01 end")def spider02(timer):print("spider02 start")time.sleep(timer) # 模拟IOprint("spider02 end")start = time.time()# 创建线程对象t1 = threading.Thread(target=spider01, args=(3,))
t1.start()
t2 = threading.Thread(target=spider02, args=(5,))
t2.start()t1.join()
t2.join()
end = time.time()
print("时间花销:", end - start)
【2】线程应用案例
通过并发对每一次爬取一张图片创建对应的线程,实现了多线程爬取
import threadingimport requests
import redef get_one_img(path, n):domain = "https://pic.netbian.com/"url = domain + pathres = requests.get(url)f = open(f"./imgs/{n}.jpg", "wb")f.write(res.content)f.close()print(f"{n}下载成功")n = 1
for page in range(2, 11):res = requests.get(f"https://pic.netbian.com/4kmeinv/index_{page}.html")# print(res.text)ret = re.findall('<img src="(/uploads/allimg/.*?)"', res.text)print(ret)for path in ret:t = threading.Thread(target=get_one_img, args=(path, n))t.start()n += 1
【3】线程池
线程池的基本语法:
from concurrent.futures import ThreadPoolExecutor先导入线程池
pool = ThreadPoolExecutor(3)创建指定大小的线程池
for i in range(1,10):future=pool.submit(task,i),submit函数要传入函数和参数。
如果有返回值,接收后可以通过future.result()拿到返回的结果。但是future.result()也有阻塞的效果。
pool.shutdown()是阻塞。线程池内的全部线程执行完成才会放行
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。在这种情形下,使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
import time
from concurrent.futures import ThreadPoolExecutordef task(i):print(f'任务{i}开始!')time.sleep(i)print(f'任务{i}结束!')return istart = time.time()
pool = ThreadPoolExecutor(3)future01 = pool.submit(task, 1)
# print("future01是否结束", future01.done())
# print("future01的结果", future01.result()) # 同步等待
future02 = pool.submit(task, 2)
future03 = pool.submit(task, 3)
pool.shutdown() # 阻塞等待
print(f"程序耗时{time.time() - start}秒钟")print("future01的结果", future01.result())
print("future02的结果", future02.result())
print("future03的结果", future03.result())
使用线程池来执行线程任务的步骤如下:
- 调用 ThreadPoolExecutor 类的构造器创建一个线程池。
- 定义一个普通函数作为线程任务。
- 调用 ThreadPoolExecutor 对象的 submit() 方法来提交线程任务。
- 当不想提交任何任务时,调用 ThreadPoolExecutor 对象的 shutdown() 方法来关闭线程池。
【4】互斥锁(同步锁)
经典错误:正常情况下,在Local中找不到x,会找Gloab的x。但是这个例子x在Local中已经存在,这个变量已经创建,只不过在这个变量赋值时就要使用这个变量,但此时这个变量又没有明确的值,所以报错。
互斥锁是为了解决在并发时的资源竞争
和数据同步
问题
lock = Thread.Lock()
lock.acqiure()
lock.release()
并发编程中需要解决一些常见的问题,例如资源竞争和数据同步。由于多个线程或进程可以同时访问共享的资源,因此可能会导致数据不一致或错误的结果。为了避免这种情况,需要采用合适的同步机制,如互斥锁、信号量或条件变量,来确保对共享资源的访问是同步和有序的。
import time
import threadingLock = threading.Lock()def addNum():global num # 在每个线程中都获取这个全局变量# 上锁Lock.acquire()t = num - 1time.sleep(0.0001)num = tLock.release()# 放锁num = 100 # 设定一个共享变量thread_list = []for i in range(100):t = threading.Thread(target=addNum)t.start()thread_list.append(t)for t in thread_list: # 等待所有线程执行完毕t.join()print('Result: ', num)
【5】线程队列
多线程队列,队列实际上还是为了解决多线程下资源抢占的问题,队列默认在每一个块都有一把锁,在有线程获取的情况下不允许其他的再获取
(1)队列的基本语法
线程队列是一种线程安全的数据结构,用于在线程之间传递和共享数据。它提供了一种解耦的方式,使生产者线程能够将数据放入队列,而消费者线程可以从队列中获取数据进行处理,从而实现线程之间的通信和协调。
线程队列的主要目的是解决多线程环境下的数据共享和同步问题。在多线程编程中,如果多个线程同时访问共享资源,可能会导致数据的不一致性和竞争条件。通过使用线程队列,可以避免直接访问共享资源,而是通过队列来传递数据,从而保证线程安全。
import queue# 创建一个空的队列
# q = queue.Queue()# 创建具有固定大小的队列
q = queue.Queue(3)q.put(100) # 将元素item放入队列
q.put(200)
q.put(300)
# q.put(400)print(q.get())
print(q.get())
print(q.get())
# print(q.empty()) # 如果队列为空,返回True;否则返回False
# print(q.qsize()) # 返回队列中的元素个数
# print(q.get())
线程队列还提供了一些特性和机制,如阻塞和超时等待。当队列为空时,消费者线程可以选择阻塞等待新的数据被放入队列,并且可以设置超时时间。这样可以避免消费者线程空转浪费资源,只有在有新的数据可用时才会继续执行。
(2)生产者-消费者模型
当某线程执行q.get()时,会让其处于一种监控模式,就是如果此时队列没有数据但是运行了q.get()不会报错,而是监控该队列,一旦有值插入立刻get出去
生产者消费者模型中的队列主要起到一个数据缓冲的作用,将生产者和消费者进行了解耦。
常见的线程队列模型是生产者-消费者模型。生产者线程负责生成数据并将其放入队列,而消费者线程则从队列中获取数据并进行处理。通过使用队列作为缓冲区,生产者和消费者之间解耦,可以实现高效的线程间通信。
案例1
import queue
import time
import threadingq = queue.Queue()def producer():for i in range(1, 11):time.sleep(1)q.put(i)print(f"生产者生产数据{i}")print("生产者结束")def consumer():while 1:val = q.get()print("消费者消费数据:", val)if val == 10:print("消费者结束")breakp = threading.Thread(target=producer)
p.start()
time.sleep(1)
c = threading.Thread(target=consumer)
c.start()
案例2:
import queue
import time
import threadingq = queue.Queue()def producer():for i in range(1, 11):time.sleep(3)q.put(i)print(f"生产者生产数据{i}")print("生产者结束")def consumer(name):while 1:val = q.get()print(f"消费者{name}消费数据:{val}")time.sleep(6)if val == 10:print("消费者结束")breakp = threading.Thread(target=producer)
p.start()
time.sleep(1)
c1 = threading.Thread(target=consumer, args=("消费线程1",))
c1.start()
c2 = threading.Thread(target=consumer, args=("消费线程2",))
c2.start()
总而言之,线程队列是一种重要的多线程编程工具,用于实现线程安全的数据传递和同步。它提供了一种简单而高效的方式,让多个线程能够安全地共享和处理数据,从而提高程序的并发性和可靠性。
3、多进程实现
并发:在某一时间段,有多个任务被执行
并行:在某一时刻,有多个任务被同时执行,所以只有在多核CPU的情况下才会有并行
GIL:它确保在任何时刻只有一个线程执行Python字节码
python没有真正意义上的并行线程
python无法想c,java那要,同时将多个线程交给不同的CPU处理。由于GIL的限制,python只能在同一时刻执行一个线程,无论有几个CPU。
GIL是一个互斥锁,它允许多个线程存在,但一次只允许一个线程执行Python字节码。这意味着即使在多核处理器上,CPython的线程也无法实现真正的并行执行
由于GIL对于进程没有影响,所以可以多进程解决GIL的影响。
但是进程并不能多开,且空间开辟,时间消耗都是最大的。
所以最好是先通过多进程将多核利用起来,实现并行的效果。再通过每个进程下协程的方式发挥最大优势。
对于多线程来说,当它执行IO密集型任务是,遇到IO阻塞就切换,所以节约的是IO等待的时间。
但是当多线程执行计算密集型任务时:由于大部分时候会在时间片走完时才切换,而且切换回来后依然继续执行,一来一回还增加了切换的时间。所以多线程不适合执行计算密集型任务。
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
python的进程调用:
import multiprocessing
import timedef foo():print("foo start...")time.sleep(5)print("foo end...")def bar():print("bar start...")time.sleep(3)print("bar end...")if __name__ == '__main__':start = time.time()t1 = multiprocessing.Process(target=foo, args=())t1.start()t2 = multiprocessing.Process(target=bar, args=())t2.start()# 等待所有子线程结束t1.join() # 等待子线程t1t2.join() # 等待子线程t2end = time.time()print(end - start)
案例:
import multiprocessing
import threading
import timedef foo(x):ret = 1for i in range(x):ret += iprint(ret)start = time.time()
# (1) 串行版本
# foo(120000000)
# foo(120000000)
# foo(120000000)# (2) 多线程版本
# t1 = threading.Thread(target=foo, args=(120000000,))
# t1.start()
# t2 = threading.Thread(target=foo, args=(120000000,))
# t2.start()
# t3 = threading.Thread(target=foo, args=(120000000,))
# t3.start()
#
# t1.join()
# t2.join()
# t3.join()# end = time.time()
# print(end - start)# (3) 多进程版本
if __name__ == '__main__':p1 = multiprocessing.Process(target=foo, args=(120000000,))p1.start()p2 = multiprocessing.Process(target=foo, args=(120000000,))p2.start()p3 = multiprocessing.Process(target=foo, args=(120000000,))p3.start()p1.join()p2.join()p3.join()end = time.time()print(end - start)
这个程序展示了三种不同的执行方式:串行版本、多线程版本和多进程版本,并统计了它们的执行时间。
- 串行版本:
- 在串行版本中,
foo(120000000)
被连续调用了三次,以便计算累加和。 - 这种方式是单线程执行的,每个调用都会阻塞其他调用的执行,直到计算完成并打印结果。
- 执行时间是三次调用的总和。
- 在串行版本中,
- 多线程版本:
- 在多线程版本中,使用了三个线程并发执行三次调用:
t1 = threading.Thread(target=foo, args=(120000000,))
。 - 每个线程独立执行一次计算,并打印结果。
- 由于全局解释器锁(GIL)的存在,多线程并不能真正实现并行计算,因此在CPU密集型任务上可能无法获得明显的性能提升。
- 执行时间是最长的单个线程的执行时间。
- 在多线程版本中,使用了三个线程并发执行三次调用:
- 多进程版本:
- 在多进程版本中,使用了三个进程并发执行三次调用:
p1 = multiprocessing.Process(target=foo, args=(120000000,))
。 - 每个进程独立执行一次计算,并打印结果。
- 多进程可以实现真正的并行计算,每个进程都在独立的Python解释器中运行,不受GIL的限制。
- 执行时间是最长的单个进程的执行时间。
- 在多进程版本中,使用了三个进程并发执行三次调用:
4、协程并发
进程和线程都是由操作系统进行调度的,协程是由用户控制导入的Greenlet或者asyncio模块进行调度的。
协程能保留上一次调用时的状态
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
【1】Greenlet库
Greenlet是对Python标准库中的yield关键字进行封装的库。它允许我们在协程中使用yield语句来暂停和恢复执行,从而实现协程的功能。
在Greenlet中,协程被称为greenlet对象。我们可以创建一个greenlet对象,并使用它的switch方法来切换协程的执行。当一个协程暂停时,它的状态会被保存下来,可以在需要时恢复执行。自己通过switch方法转换就体现了协程时用户态的轻量级线程,是由用户自己通过模块控制调度的
from greenlet import greenletdef foo():print("foo step1") # 第1步:输出 foo step1gr_bar.switch() # 第3步:切换到 bar 函数print("foo step2") # 第6步:输出 foo step2gr_bar.switch() # 第7步:切换到 bar 函数,从上一次执行的位置继续向后执行def bar():print("bar step1") # 第4步:输出 bar step1gr_foo.switch() # 第5步:切换到 foo 函数,从上一次执行的位置继续向后执行print("bar step2") # 第8步:输出 bar step2if __name__ == '__main__':gr_foo = greenlet(foo)gr_bar = greenlet(bar)gr_foo.switch() # 第1步:去执行 foo 函数# 注意:switch中也可以传递参数用于在切换执行时相互传递值。
Python Greenlet库提供了一种轻量级的协程实现方式,适合处理高并发和I/O密集型任务。其简单易用的API和良好的兼容性使其成为Python开发者的理想选择。
【2】asyncio模块
asyncio即Asynchronous I/O是python一个用来处理并发(concurrent)事件的包,是很多python异步架构的基础,多用于处理高并发网络请求方面的问题。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。
asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。
(1)基本使用
import asyncio
import timeasync def task(i):print(f"task {i} start")await asyncio.sleep(1)print(f"task {i} end")start = time.time()
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 直接将协程对象加入时间循环中
tasks = [task(1), task(2)]
# asyncio.wait:将协程任务进行收集,功能类似后面的asyncio.gather
# run_until_complete阻塞调用,直到协程全部运行结束才返回
loop.run_until_complete(asyncio.wait(tasks))
loop.close()end = time.time()print("cost timer:", end - start)
(2)任务对象
task
: 任务,对协程对象的进一步封装,包含任务的各个状态;asyncio.Task是Future的一个子类,用于实现协作式多任务的库,且Task对象不能用户手动实例化,通过下面2个函数loop.create_task() 或 asyncio.ensure_future()
创建。
import asyncio, timeasync def work(i, n): # 使用async关键字定义异步函数print('任务{}等待: {}秒'.format(i, n))await asyncio.sleep(n) # 休眠一段时间print('任务{}在{}秒后返回结束运行'.format(i, n))return i + nstart_time = time.time() # 开始时间tasks = [asyncio.ensure_future(work(1, 1)),asyncio.ensure_future(work(2, 2)),asyncio.ensure_future(work(3, 3))]
# tasks[1].add_done_callback()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()print('运行时间: ', time.time() - start_time)
for task in tasks:print('任务执行结果: ', task.result())
(3)新版本语法支持
async是一个标识,标识着某个函数一定是协程任务。在协程任务中必须有一个await异步等待,会自动切换协程
async.create_task()
创建task
async.gather()
获取返回值
async.run()
运行协程
# 用gather()收集返回值import asyncio, timeasync def work(i, n): # 使用async关键字定义异步函数print('任务{}等待: {}秒'.format(i, n))await asyncio.sleep(n) # 休眠一段时间print('任务{}在{}秒后返回结束运行'.format(i, n))return i + nasync def main():tasks = [asyncio.create_task(work(1, 1)),asyncio.create_task(work(2, 2)),asyncio.create_task(work(3, 3))]# 将task作为参数传入gather,等异步任务都结束后返回结果列表response = await asyncio.gather(tasks[0], tasks[1], tasks[2])print("异步任务结果:", response)start_time = time.time() # 开始时间asyncio.run(main())print('运行时间: ', time.time() - start_time)
【3】基于协程的异步爬虫应用
(1)同步请求爬虫
import os.path
import time
import requests
import redef get_page_img_urls(page):# 获取页面内容res = requests.get(f"https://pic.netbian.com/4kmeinv/index_{page}.html")# 使用正则表达式提取图片URLret = re.findall('<img src="(/uploads/allimg/.*?)"', res.text)print(ret)return retdef download_one_img(url, n):# 下载单张图片res = requests.get(url)f = open(f"./imgs/{n}", "wb")f.write(res.content)f.close()print(f"{n}下载成功")def download_page_imgs(img_urls):domain = "https://pic.netbian.com/"for path in img_urls:title = os.path.basename(path)url = domain + pathdownload_one_img(url, title)def main():start = time.time()for i in range(2, 6):page_img_urls = get_page_img_urls(i)# 获取页面中的图片URL列表download_page_imgs(page_img_urls)# 下载页面中的所有图片end = time.time()print("cost timer:", end - start)main()
(2)基于asyncio库的异步爬虫
request模块是同步请求模块,当使用request爬虫遇到IO操作是不能进行转换。aiohttp是异步爬虫模块
当同步请求时,并没有遇到大块时间的IO阻塞,网络情况特别好的时候。利用异步的意义就不大。
只有在需要大块时间等待的时候,异步操作转而执行其他的操作,才会体现出节约等待时间的价值。
import time
import requests
import re
import asyncio
import aiohttp
import osasync def get_page_img_urls(page):# 获取页面内容# res = requests.get(f"https://pic.netbian.com/4kmeinv/index_{2}.html")async with aiohttp.ClientSession() as session:async with session.get(f"https://pic.netbian.com/4kmeinv/index_{page}.html", verify_ssl=False) as res:data = await res.content.read()# 使用正则表达式提取图片URLret = re.findall('<img src="(/uploads/allimg/.*?)"', data.decode("GBK"))print(ret)return retasync def download_one_img(url, n):# 下载单张图片async with aiohttp.ClientSession() as session:async with session.get(url, verify_ssl=False) as res:f = open(f"./imgs/{n}", "wb")data = await res.content.read()f.write(data)f.close()print(f"{n}下载成功")async def download_page_imgs(img_urls):domain = "https://pic.netbian.com/"for path in img_urls:title = os.path.basename(path)url = domain + pathawait download_one_img(url, title)async def main():start = time.time()for i in range(2, 6):# 获取页面中的图片URL列表page_img_urls = await get_page_img_urls(i)# 下载页面中的所有图片await download_page_imgs(page_img_urls)end = time.time()print("cost timer:", end - start)asyncio.run(main())
在这段代码中,async with
是用于创建一个异步上下文管理器,而不是用于等待异步操作的完成。异步上下文管理器可以在异步代码块中管理资源的获取和释放。
在这个例子中,aiohttp.ClientSession()
返回一个异步上下文管理器对象 session
,它负责管理与服务器的连接和会话。使用 async with
可以确保在代码块执行完毕后,自动关闭和释放与服务器的连接。
在 async with session.get(...)
中,session.get(...)
返回一个异步上下文管理器对象 response
,它负责发送 HTTP 请求并获取响应。在 async with
代码块内部,我们可以使用 response
对象进行响应的处理,例如读取响应的内容。
若有错误与不足请指出,关注DPT一起进步吧!!!