跟我一起从零开始学python(三)多线程/多进程/协程

前言

回顾之前讲了python语法编程 ,关于从零入门python的第一遍,编程语法必修内容,比如python3基础入门,列表与元组,字符串,字典,条件丶循环和其他语句丶函数丶面向对象丶异常和文件处理和网络编程篇

1.跟我一起从零开始学python(一)编程语法必修
2.跟我一起从零开始学python(二)网络编程

本篇讲:python并发编程:多线程/多进程/协程

本系列文根据以下学习路线展开讲述,由于内容较多:

从零开始学python到高级进阶路线图

第一章:多线程

1.线程和进程

线程和进程是操作系统中的两个重要概念,它们都是并发编程的基础。线程是操作系统能够进行运算调度的最小单位,而进程则是操作系统进行资源分配和调度的基本单位。

线程和进程的区别:

  • 线程是进程的一部分,一个进程可以包含多个线程,而一个线程只能属于一个进程。
  • 进程拥有独立的内存空间,而线程共享进程的内存空间。
  • 进程之间的通信需要使用IPC(Inter-Process Communication)机制,而线程之间可以直接共享数据。
  • 进程的创建和销毁比线程慢,因为进程需要分配和释放独立的内存空间,而线程只需要分配和释放一些寄存器和栈空间。

在Python中,可以使用threading模块来创建和管理线程。下面是一个简单的线程示例:

import threading

def worker():
    print('Worker thread started')
    # do some work here
    print('Worker thread finished')

# create a new thread
t = threading.Thread(target=worker)
# start the thread
t.start()
# wait for the thread to finish
t.join()

在这个示例中,我们创建了一个名为worker的函数,它将在一个新的线程中运行。我们使用threading.Thread类创建了一个新的线程对象,并将worker函数作为目标传递给它。然后,我们使用start()方法启动线程,并使用join()方法等待线程完成。

2.使用线程

在Python中,使用线程可以通过threading模块来实现。下面是一个简单的例子,展示了如何使用线程:

import threading

def worker():
    """线程执行的任务"""
    print("Worker thread started")
    # 执行一些任务
    print("Worker thread finished")

# 创建线程
t = threading.Thread(target=worker)
# 启动线程
t.start()

# 主线程继续执行其他任务
print("Main thread finished")

在上面的例子中,我们首先定义了一个worker函数,它将在一个单独的线程中执行。然后,我们使用threading.Thread类创建了一个新的线程,并将worker函数作为参数传递给它。最后,我们调用start方法来启动线程。

注意,线程是异步执行的,因此主线程不会等待线程完成。在上面的例子中,主线程会立即继续执行,输出Main thread finished。如果我们希望等待线程完成后再继续执行主线程,可以使用join方法:

# 等待线程完成
t.join()


# 主线程继续执行其他任务
print("Main thread finished")

在上面的代码中,我们在启动线程后调用了t.join()方法,这将阻塞主线程,直到线程完成。然后,主线程才会继续执行。

3.多线程全局变量

在多线程编程中,多个线程可以共享全局变量。但是需要注意的是,多个线程同时对同一个全局变量进行读写操作时,可能会出现数据竞争(Data Race)的问题,导致程序出现不可预期的结果。

为了避免数据竞争,可以使用线程锁(Thread Lock)来保证同一时刻只有一个线程可以访问共享变量。Python中提供了LockRLockSemaphore等多种锁机制,可以根据实际需求选择合适的锁。

下面是一个使用Lock来保证多线程共享全局变量安全的示例代码:

import threading

# 定义全局变量
count = 0

# 定义线程锁
lock = threading.Lock()

# 定义线程函数
def add():
    global count
    for i in range(100000):
        # 获取锁
        lock.acquire()
        count += 1
        # 释放锁
        lock.release()

# 创建两个线程
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)

# 启动线程
t1.start()
t2.start()

# 等待线程执行完毕
t1.join()
t2.join()

# 输出结果
print(count)

在上面的代码中,我们定义了一个全局变量count,并使用Lock来保证多个线程对count进行读写操作时的安全性。在每个线程中,我们首先获取锁,然后对count进行加1操作,最后释放锁。这样就可以保证同一时刻只有一个线程可以访问count,避免了数据竞争的问题。

需要注意的是,使用锁会带来一定的性能损失,因为每次获取锁和释放锁都需要一定的时间。因此,在实际应用中,需要根据实际情况来选择合适的锁机制,避免过度使用锁导致程序性能下降。

4.共享全局变量所带来的问题

在多线程编程中,多个线程可以共享全局变量。但是,共享全局变量也会带来一些问题:

  • 竞争条件:当多个线程同时访问和修改同一个全局变量时,可能会出现竞争条件,导致程序出现不可预测的结果。

  • 数据不一致:当多个线程同时修改同一个全局变量时,可能会导致数据不一致的问题,即某些线程看到的变量值与其他线程看到的不同。

  • 死锁:当多个线程同时等待对方释放某个资源时,可能会出现死锁的情况,导致程序无法继续执行。

因此,在多线程编程中,需要注意对共享全局变量的访问和修改,避免出现上述问题。可以使用锁、条件变量等机制来保证线程之间的同步和互斥。

5.解决线程同时修改全局变量的方式

在多线程编程中,共享全局变量可能会带来一些问题,例如:

  • 竞争条件:多个线程同时修改同一个全局变量,可能会导致数据不一致或者出现意料之外的结果。

  • 死锁:多个线程同时等待对方释放资源,导致程序无法继续执行。

为了解决这些问题,可以采用以下方式:

  • 使用锁:在访问共享变量时,使用锁来保证同一时刻只有一个线程可以修改变量。Python中提供了threading模块中的Lock类来实现锁。

  • 使用线程安全的数据结构:Python中提供了一些线程安全的数据结构,例如Queue、deque等,可以在多线程环境下安全地访问和修改数据。

  • 使用局部变量:将全局变量作为参数传递给线程函数,让线程函数在局部变量上进行操作,避免多个线程同时修改同一个全局变量。

  • 使用线程本地存储:Python中提供了threading模块中的local类,可以在每个线程中创建一个独立的变量,避免多个线程之间共享变量。

6.互斥锁

在多线程编程中,互斥锁是一种常用的同步机制,用于保护共享资源,防止多个线程同时修改同一个变量导致数据不一致的问题。

互斥锁的基本思想是,在访问共享资源之前,先获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞,直到锁被释放为止。在访问完共享资源之后,释放锁,让其他线程可以获取锁并访问共享资源。

在Python中,可以使用threading模块中的Lock类来实现互斥锁。Lock类有两个基本方法:

  • acquire([blocking]):获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞。如果blockingFalse,则获取锁失败时会立即返回False,而不是阻塞等待。
  • release():释放锁,让其他线程可以获取锁并访问共享资源。
    下面是一个使用互斥锁的例子:
import threading

# 共享变量
count = 0

# 创建互斥锁
lock = threading.Lock()

# 线程函数
def worker():
    global count
    for i in range(100000):
        # 获取锁
        lock.acquire()
        try:
            count += 1
        finally:
            # 释放锁
            lock.release()

# 创建多个线程
threads = []
for i in range(10):
    t = threading.Thread(target=worker)
    threads.append(t)

# 启动线程
for t in threads:
    t.start()

# 等待所有线程执行完毕
for t in threads:
    t.join()

# 输出结果
print(count)

在上面的例子中,我们创建了一个共享变量count,并使用互斥锁来保护它。在每个线程中,我们先获取锁,然后修改count的值,最后释放锁。这样就可以保证多个线程不会同时修改count的值,从而避免了数据不一致的问题。最后输出count的值,可以看到它的值为1000000,符合预期。

7.死锁

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续执行下去。在多线程编程中,死锁是一种常见的问题,需要特别注意。

死锁的产生通常需要满足以下四个条件

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由该线程自己释放。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。

为了避免死锁的产生,可以采用以下几种方式

  • 避免使用多个锁,尽量使用一个锁来控制多个资源的访问。
  • 避免持有锁的时间过长,尽量缩短锁的持有时间。
  • 避免循环等待,尽量按照固定的顺序获取锁。
  • 使用超时机制,当等待时间超过一定时间后,自动释放锁,避免长时间等待造成的死锁。

8.线程池

线程池是一种线程管理技术,它可以在程序启动时创建一定数量的线程,放入一个池中,当需要使用线程时,就从池中取出一个线程执行任务,任务执行完毕后,线程并不会被销毁,而是放回池中等待下一次任务的到来。

使用线程池可以避免频繁创建和销毁线程的开销,提高程序的性能和效率。在Python中,可以使用标准库中的concurrent.futures模块来实现线程池。

下面是一个简单的线程池示例:

import concurrent.futures
import time

def worker(num):
    print(f"Thread-{num} started")
    time.sleep(1)
    print(f"Thread-{num} finished")

if __name__ == '__main__':
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        for i in range(5):
            executor.submit(worker, i)

在这个示例中,我们创建了一个包含3个线程的线程池,然后提交了5个任务给线程池执行。由于线程池中只有3个线程,因此只有3个任务会同时执行,其余的任务会等待空闲线程的出现。

输出结果如下:

Thread-0 started
Thread-1 started
Thread-2 started
Thread-0 finished
Thread-3 started
Thread-1 finished
Thread-4 started
Thread-2 finished
Thread-3 finished
Thread-4 finished

可以看到,线程池中的3个线程依次执行了5个任务,任务的执行顺序与提交的顺序无关。

第二章:多进程

1.进程的状态

在操作系统中,进程有以下几种状态

  • 就绪状态(Ready):进程已经准备好运行,等待分配CPU时间片。

  • 运行状态(Running):进程正在运行,占用CPU时间片。

  • 阻塞状态(Blocked):进程因为某些原因无法继续执行,例如等待I/O操作完成或等待某个资源的释放。

  • 挂起状态(Suspended):进程被暂停,不再占用CPU时间片,但是它的状态信息仍然保存在内存中。

  • 终止状态(Terminated):进程已经完成执行或被强制终止。

进程的状态转换通常是由操作系统内核进行控制的,例如当一个进程等待I/O操作完成时,操作系统会将该进程的状态从就绪状态转换为阻塞状态,当I/O操作完成后,操作系统会将该进程的状态从阻塞状态转换为就绪状态,等待分配CPU时间片。

2.线程的创建-multiprocessing

在 Python 中,可以使用 multiprocessing 模块来创建多进程。multiprocessing 模块提供了一个 Process 类,可以用来创建进程。下面是一个简单的例子:

import multiprocessing

def worker():
    """子进程要执行的任务"""
    print('Worker')

if __name__ == '__main__':
    # 创建子进程
    p = multiprocessing.Process(target=worker)
    # 启动子进程
    p.start()
    # 等待子进程结束
    p.join()

在上面的例子中,我们首先定义了一个 worker 函数,它是子进程要执行的任务。然后,我们使用 multiprocessing.Process 类创建了一个子进程,并将 worker 函数作为参数传递给了 Process 类的构造函数。接着,我们调用 start 方法启动子进程,最后调用 join 方法等待子进程结束。

需要注意的是,在 Windows 系统中,由于 multiprocessing 模块使用了 fork 系统调用,而 Windows 不支持 fork,因此需要在 if __name__ == '__main__': 语句中调用子进程的代码。这是因为在 Windows 中,每个进程都会执行一遍程序的所有代码,而 if __name__ == '__main__': 语句可以保证子进程只会执行指定的代码。

3.进程丶线程对比

进程和线程都是实现并发编程的方式,但是它们有以下不同点:

  • 资源占用:进程拥有独立的内存空间,而线程共享进程的内存空间。因此,创建进程的开销比创建线程大,同时进程间的通信也比线程间的通信复杂。

  • 并发性:由于线程共享进程的内存空间,因此线程间的通信和数据共享比进程间的通信和数据共享更容易。同时,线程的切换比进程的切换更快,因此线程的并发性比进程高。

  • 安全性:由于线程共享进程的内存空间,因此多个线程同时访问同一块内存时,可能会出现竞争条件,导致数据不一致或者程序崩溃。而进程之间的内存空间是独立的,因此进程间的数据不会相互影响。

  • 编程复杂度:由于进程间的通信和数据共享比较复杂,因此编写多进程程序的复杂度比编写多线程程序的复杂度高。

总的来说,进程适合于CPU密集型任务,而线程适合于IO密集型任务。在实际应用中,需要根据具体的场景选择合适的并发编程方式。

4.进程间的通信-Queue

在多进程编程中,不同进程之间的数据是无法直接共享的,因为每个进程都有自己独立的内存空间。因此,为了实现进程间的通信,我们需要使用一些特殊的机制。

其中,最常用的进程间通信方式是使用队列(Queue)。队列是一种先进先出(FIFO)的数据结构,可以用来在多个进程之间传递数据。

在Python中,我们可以使用multiprocessing模块中的Queue类来实现进程间通信。Queue类提供了put()get()方法,用于向队列中添加数据和从队列中取出数据。

下面是一个简单的例子,演示了如何使用Queue实现进程间通信:

from multiprocessing import Process, Queue

def worker(q):
    while True:
        item = q.get()
        if item is None:
            break
        print(item)

if __name__ == '__main__':
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()

    for i in range(10):
        q.put(i)

    q.put(None)
    p.join()

在这个例子中,我们创建了一个进程p,它的工作是从队列q中取出数据并打印出来。主进程向队列中添加了10个数据,然后再添加一个None,表示数据已经全部添加完毕。最后,主进程等待进程p执行完毕。

需要注意的是,当我们向队列中添加数据时,如果队列已满,put()方法会阻塞,直到队列中有空闲位置。同样地,当我们从队列中取出数据时,如果队列为空,get()方法也会阻塞,直到队列中有数据可取。

除了Queue之外,Python中还提供了一些其他的进程间通信方式,比如PipeValueArray等。这些方式各有特点,可以根据具体的需求选择合适的方式

5.进程池的创建-pool

在Python中,我们可以使用multiprocessing模块中的Pool类来创建进程池。进程池是一组可重用的进程,可以在需要时分配给任务。这样可以避免频繁地创建和销毁进程,从而提高程序的效率。

下面是一个使用进程池的例子:

import multiprocessing

def worker(num):
    """进程池中的任务"""
    print('Worker %d is running' % num)

if __name__ == '__main__':
    # 创建进程池,池中有3个进程
    pool = multiprocessing.Pool(processes=3)
    # 向进程池中添加任务
    for i in range(5):
        pool.apply_async(worker, args=(i,))
    # 关闭进程池,不再接受新的任务
    pool.close()
    # 等待所有任务完成
    pool.join()
    print('All workers done.')

在这个例子中,我们首先创建了一个进程池,池中有3个进程。然后向进程池中添加了5个任务,每个任务都是调用worker函数。最后,我们关闭了进程池,并等待所有任务完成。

需要注意的是,进程池中的任务必须是可序列化的,因为进程池会将任务发送给子进程执行。如果任务中包含不可序列化的对象,会导致进程池无法正常工作。

第三章:协程

协程是一种轻量级的线程,也称为微线程或者用户级线程。协程的特点是在一个线程中,可以有多个协程,协程之间可以相互切换,从而实现并发执行。

在Python中,协程是通过生成器实现的。通过yield关键字,可以将一个函数变成一个生成器,从而实现协程的功能。在协程中,可以使用yield关键字来暂停函数的执行,并返回一个值给调用者。当协程再次被调用时,可以从上一次暂停的位置继续执行。

Python中的协程有两种实现方式:使用生成器实现的协程和使用async/await关键字实现的协程。

使用生成器实现的协程

def coroutine():
    while True:
        value = yield
        print('Received value:', value)

c = coroutine()
next(c)  # 启动协程
c.send(10)  # 发送值给协程

使用async/await关键字实现的协程

import asyncio

async def coroutine():
    while True:
        value = await asyncio.sleep(1)
        print('Received value:', value)

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine())

在使用async/await关键字实现的协程中,需要使用asyncio模块提供的事件循环来运行协程。在协程中,可以使用await关键字来暂停函数的执行,并等待一个异步操作完成。当异步操作完成后,协程会从await语句处继续执行。

协程的优点是可以避免线程切换的开销,从而提高程序的性能。同时,协程也可以避免线程之间的竞争条件和死锁问题。但是,协程也有一些缺点,例如不能利用多核CPU的优势,以及不能进行阻塞式IO操作。

1.协程的意义

协程是一种轻量级的线程,可以在单个线程中实现并发。与线程相比,协程的切换开销更小,可以更高效地利用CPU资源。协程的意义在于:

  • 提高程序的并发性能:协程可以在单个线程中实现并发,避免了线程切换的开销,提高了程序的并发性能。

  • 简化编程模型:协程可以使用同步的编程模型,避免了复杂的线程同步问题,使得编程更加简单。

  • 支持高并发:协程可以支持大量的并发任务,可以用于高并发的网络编程、爬虫等场景。

  • 提高代码可读性:协程可以使用同步的编程模型,代码可读性更高,易于维护。

总之,协程是一种高效、简单、可读性强的并发编程模型,可以提高程序的并发性能,支持高并发,简化编程模型。

2.asyncio事件循环

在Python中,协程是一种轻量级的并发编程方式,它可以在单线程中实现并发执行。协程的意义在于可以提高程序的并发性能,减少线程切换的开销,同时也可以简化编程模型,使得代码更加易于理解和维护。

在Python 3.4及以上版本中,标准库中提供了asyncio模块,它是Python中实现协程的主要方式之一。asyncio模块提供了一个事件循环(Event Loop),它可以在单线程中实现多个协程的并发执行。事件循环会不断地从协程队列中取出协程并执行,当协程遇到IO操作时,会自动挂起并切换到其他协程执行,等待IO操作完成后再恢复执行。

asyncio事件循环的使用方式如下:

1.创建一个事件循环对象

import asyncio

loop = asyncio.get_event_loop()

2.将协程对象加入事件循环中

async def coroutine():
    # 协程代码

loop.run_until_complete(coroutine())

3.启动事件循环

loop.run_forever()

在事件循环中,可以使用async/await关键字定义协程函数,使用asyncio模块提供的各种方法实现协程之间的通信和协作。例如,可以使用asyncio.sleep()方法实现协程的延时操作,使用asyncio.wait()方法等待多个协程的完成等。

3.await关键字

在Python中,await是一个关键字,用于等待一个协程完成。当一个协程调用另一个协程时,它可以使用await关键字来等待另一个协程完成并返回结果。在等待期间,当前协程会被挂起,直到被等待的协程完成。

例如,假设有两个协程A和B,A需要等待B完成后才能继续执行。在协程A中,可以使用await关键字来等待协程B完成:

async def coroutine_b():
    # 协程B的代码

async def coroutine_a():
    # 协程A的代码
    result = await coroutine_b()
    # 继续执行协程A的代码

在这个例子中,当协程A调用await coroutine_b()时,它会等待协程B完成并返回结果。在等待期间,协程A会被挂起,直到协程B完成。一旦协程B完成并返回结果,协程A会继续执行。

使用await关键字可以使协程之间的调用更加简洁和直观,同时也可以避免使用回调函数等复杂的异步编程模式。

4.concurrent和future对象

在Python中,asyncio模块提供了一种基于协程的异步编程方式。在协程中,我们可以使用async/await关键字来定义异步函数,使用asyncio模块提供的事件循环来调度协程的执行。

除了协程之外,asyncio还提供了一些其他的并发编程工具,包括concurrentfuture对象。

  • concurrent对象

concurrent对象是asyncio中的一个重要概念,它表示一个协程的执行状态。在asyncio中,我们可以使用asyncio.create_task()函数来创建一个concurrent对象,该函数接受一个协程对象作为参数,并返回一个concurrent对象。

例如,下面的代码创建了一个协程对象,并使用create_task()函数将其转换为concurrent对象:

import asyncio

async def my_coroutine():
    print('Coroutine started')
    await asyncio.sleep(1)
    print('Coroutine ended')

async def main():
    task = asyncio.create_task(my_coroutine())
    await task

asyncio.run(main())

在上面的代码中,我们使用create_task()函数将my_coroutine()函数转换为concurrent对象,并将其赋值给task变量。然后,我们使用await关键字等待task对象的完成。

  • future对象

future对象是asyncio中的另一个重要概念,它表示一个异步操作的结果。在asyncio中,我们可以使用asyncio.Future()函数来创建一个future对象,该函数返回一个未完成的future对象。

例如,下面的代码创建了一个未完成的future对象:

import asyncio

async def my_coroutine():
    print('Coroutine started')
    await asyncio.sleep(1)
    print('Coroutine ended')
    return 'Result'

async def main():
    future = asyncio.Future()
    await asyncio.sleep(1)
    future.set_result(await my_coroutine())
    print(future.result())

asyncio.run(main())

在上面的代码中,我们使用asyncio.Future()函数创建了一个未完成的future对象,并将其赋值给future变量。然后,我们使用await关键字等待1秒钟,然后调用my_coroutine()函数,并将其结果设置为future对象的结果。最后,我们打印future对象的结果。

5.asyncio异步迭代器和上下文管理

除了concurrent和future对象之外,asyncio还提供了一些其他的并发编程工具,包括异步迭代器和上下文管理。

异步迭代器是一种特殊的迭代器,它可以在异步环境中使用。在asyncio中,我们可以使用async for循环来遍历异步迭代器。

例如,下面的代码使用async for循环遍历一个异步迭代器:

import asyncio

async def my_coroutine():
    for i in range(5):
        await asyncio.sleep(1)
        yield i

async def main():
    async for i in my_coroutine():
        print(i)

asyncio.run(main())

在上面的代码中,我们定义了一个异步生成器函数my_coroutine(),它使用yield语句返回一个值,并在每次返回值之间暂停1秒钟。然后,我们使用async for循环遍历my_coroutine()函数返回的异步迭代器,并打印每个返回值。

上下文管理是一种在异步环境中管理资源的方式。在asyncio中,我们可以使用async with语句来管理异步上下文。

例如,下面的代码使用async with语句管理一个异步上下文:

import asyncio

class MyContext:
    async def __aenter__(self):
        print('Entering context')
        await asyncio.sleep(1)
        return self

    async def __aexit__(self, exc_type, exc, tb):
        print('Exiting context')
        await asyncio.sleep(1)

async def main():
    async with MyContext() as context:
        print('Inside context')

asyncio.run(main())

在上面的代码中,我们定义了一个MyContext类,它实现了__aenter__()__aexit__()方法。aenter()方法在进入上下文时被调用,aexit()方法在退出上下文时被调用。在main()函数中,我们使用async with语句管理MyContext对象,并在上下文中打印一条消息。当我们进入和退出上下文时,aenter()__aexit__()方法会被调用,并暂停1秒钟。

6.异步操作MySQL

在Python中,我们可以使用异步IO库asyncio来实现异步操作MySQL数据库。下面是一个简单的示例:

import asyncio
import aiomysql

async def test_mysql():
    # 连接MySQL数据库
    conn = await aiomysql.connect(host='localhost', port=3306,
                                  user='root', password='password',
                                  db='test', charset='utf8mb4')
    # 创建游标
    cur = await conn.cursor()
    # 执行SQL语句
    await cur.execute("SELECT * FROM users")
    # 获取查询结果
    result = await cur.fetchall()
    # 输出查询结果
    print(result)
    # 关闭游标和连接
    await cur.close()
    conn.close()

# 运行异步函数
loop = asyncio.get_event_loop()
loop.run_until_complete(test_mysql())

在上面的示例中,我们使用了aiomysql库来连接MySQL数据库,并使用async/await语法来执行异步操作。首先,我们使用aiomysql.connect()方法来连接MySQL数据库,然后使用await conn.cursor()方法创建游标,使用await cur.execute()方法执行SQL语句,使用await cur.fetchall()方法获取查询结果,最后使用await cur.close()方法关闭游标,使用conn.close()方法关闭连接。

需要注意的是,在使用aiomysql库时,我们需要在连接MySQL数据库时指定charset='utf8mb4',以支持中文字符集。

7.异步爬虫

异步爬虫是指使用协程来实现爬虫程序,通过异步非阻塞的方式来提高爬取效率。在Python中,可以使用asyncio库来实现异步爬虫。

下面是一个简单的异步爬虫示例:

import asyncio
import aiohttp
from bs4 import BeautifulSoup

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def get_links(session, url):
    html = await fetch(session, url)
    soup = BeautifulSoup(html, 'html.parser')
    links = []
    for link in soup.find_all('a'):
        href = link.get('href')
        if href and href.startswith('http'):
            links.append(href)
    return links

async def main():
    async with aiohttp.ClientSession() as session:
        links = await get_links(session, 'https://www.baidu.com')
        for link in links:
            print(link)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

在这个示例中,我们使用了aiohttp库来发送异步HTTP请求,使用BeautifulSoup库来解析HTML页面,使用asyncio库来实现协程。

首先定义了一个fetch函数,用于发送HTTP请求并返回响应内容。然后定义了一个get_links函数,用于获取页面中的所有链接。最后,在main函数中使用aiohttp库创建一个异步HTTP客户端会话,调用get_links函数获取链接,并打印出来。

需要注意的是,在使用aiohttp库时,需要使用async with语句来创建一个异步HTTP客户端会话,以确保会话在使用完毕后能够正确关闭。

下章讲:python数据库编程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容