type
status
date
slug
summary
tags
category
icon
password
标签
是否汉化
📌
当很多人说自己“想变得有钱”的时候,TA们其实是在说:
❶ 我想睡到自然醒。 ❷ 我想有时间陪伴那些我在意的人。 ❸ 我想不再为衣食住行发愁。 ❹ 我想有能力帮助那些需要帮助的人。 ❺ 我想做那些我真正热爱的事情。
在一个更公正的社会里,所有这些都不应该是富有的人的特权。
✍🏻

Python 异步编程入门

notion image
本文解释 Python 的异步模块 asyncio 的概念和基本用法,并且演示如何通过 Python 脚本操作无头浏览器 pyppeteer 。

一、Python 异步编程的由来

历史上,Python 并不支持专门的异步编程语法,因为不需要。
有了多线程(threading)和多进程(multiprocessing,就没必要一定支持异步了。如果一个线程(或进程)阻塞,新建其他线程(或进程)就可以了,程序不会卡死。
notion image
但是,多线程有"线程竞争"的问题,处理起来很复杂,还涉及加锁。对于简单的异步任务来说(比如与网页互动),写起来很麻烦。
notion image
Python 3.4 引入了 asyncio 模块,增加了异步编程,跟 JavaScript 的async/await 极为类似,大大方便了异步任务的处理。它受到了开发者的欢迎,成为从 Python 2 升级到 Python 3 的主要理由之一。

二、asyncio 的设计

asyncio 模块最大特点就是,只存在一个线程,跟 JavaScript 一样。
由于只有一个线程,就不可能多个任务同时运行。asyncio 是"多任务合作"模式(cooperative multitasking),允许异步任务交出执行权给其他任务,等到其他任务完成,再收回执行权继续往下执行,这跟 JavaScript 也是一样的。
由于代码的执行权在多个任务之间交换,所以看上去好像多个任务同时运行,其实底层只有一个线程,多个任务分享运行时间。
表面上,这是一个不合理的设计,明明有多线程多进程的能力,为什么放着多余的 CPU 核心不用,而只用一个线程呢?但是就像前面说的,单线程简化了很多问题,使得代码逻辑变得简单,写法符合直觉。
notion image
asyncio 模块在单线程上启动一个事件循环(event loop),时刻监听新进入循环的事件,加以处理,并不断重复这个过程,直到异步任务结束。事件循环的内部机制,可以参考 JavaScript 的模型,两者是一样的。
notion image

三、asyncio API

下面介绍 asyncio 模块最主要的几个API。注意,必须使用 Python 3.7 或更高版本,早期的语法已经变了。
第一步,import 加载 asyncio 模块。
第二步,函数前面加上 async 关键字,就变成了 async 函数。这种函数最大特点是执行可以暂停,交出执行权。
第三步,在 async 函数内部的异步任务前面,加上await命令。
上面代码中,asyncio.sleep(1) 方法可以生成一个异步任务,休眠1秒钟然后结束。
执行引擎遇到await命令,就会在异步任务开始执行之后,暂停当前 async 函数的执行,把执行权交给其他任务。等到异步任务结束,再把执行权交回 async 函数,继续往下执行。
第四步,async.run() 方法加载 async 函数,启动事件循环。
上面代码中,asyncio.run() 在事件循环上监听 async 函数main的执行。等到 main 执行完了,事件循环才会终止。

四、async 函数的示例

下面是 async 函数的例子,新建一个脚本async.py,代码如下。
上面脚本中,在 async 函数main的里面,asyncio.gather() 方法将多个异步任务(三个 count())包装成一个新的异步任务,必须等到内部的多个异步任务都执行结束,这个新的异步任务才会结束。
脚本的运行结果如下。
上面运行结果的原因是,三个 count() 依次执行,打印完 One,就休眠1秒钟,把执行权交给下一个 count(),所以先连续打印出三个 One。等到1秒钟休眠结束,执行权重新交回第一个 count(),开始执行 await 命令下一行的语句,所以会接着打印出三个Two。脚本总的运行时间是1秒。
作为对比,下面是这个例子的同步版本 sync.py
上面脚本的运行结果如下。
上面运行结果的原因是,三个 count() 都是同步执行,必须等到前一个执行完,才能执行后一个。脚本总的运行时间是3秒。

五、实例:pyppeteer 模块

最后是一个异步编程的真实例子:操作无头浏览器。异步编程对代码的简化,在这个例子体现得淋漓尽致。
我们需要用到 pyppeteer 模块,它是无头浏览器 Puppeteer 的 Python 移植,API 跟 JavaScript 版本基本一致。下面是安装命令。
然后,写一个网页截图脚本screenshot.py
上面代码中,启动浏览器(launch)、打开新 Tab(newPage())、访问网址(page.goto())、截图(page.screenshot())、关闭浏览器(browser.close()),这一系列操作都是异步任务,使用 await 命令写起来非常自然简单。
执行这个脚本,当前目录下就会生成截图文件 example.png
如果脚本执行时报错 No usable sandbox!,可以参考这里。另外,第一次执行这个脚本,会下载安装 Puppeteer,可能需要等待较长时间,但是此后的执行就会很快。
Pyppeteer 的官网还有其他实例,比如向网页注入 JavaScript 代码,大家可以自己试玩。
 

六、两种运行方式的区别

✍🏻
asyncio.run(main())asyncio.get_event_loop().run_until_complete(main())实现的功能相似,都是运行一个异步函数并等待其完成。但它们之间有一些区别。
  1. 语法上的区别:
      • asyncio.run(main())是Python 3.7版本引入的新函数,可以直接使用,它会创建一个新的事件循环,并运行指定的异步函数,直到它完成。在函数内部,会自动创建和销毁事件循环。
      • asyncio.get_event_loop().run_until_complete(main())是在早期版本中使用的方法,它需要手动获取事件循环对象,并调用run_until_complete()方法来运行异步函数。需要注意的是,这种方式需要手动管理事件循环的创建和销毁。
  1. 默认行为的区别:
      • asyncio.run(main())会自动运行事件循环,并在异步函数完成后停止事件循环,而不需要手动调用run_until_complete()stop()方法。
      • asyncio.get_event_loop().run_until_complete(main())需要显式地调用run_until_complete()方法来运行异步函数,然后手动停止事件循环。
  1. 兼容性的区别:
      • asyncio.run(main())是在Python 3.7及更高版本中引入的,如果您的代码需要向下兼容到早期版本的Python,则无法使用此方式。
      • asyncio.get_event_loop().run_until_complete(main())是较早版本的Python中使用的方式,可以在更旧的Python版本中使用。
总的来说,asyncio.run(main())是更简单、更推荐的方式,特别是在使用Python 3.7及更高版本的情况下。它自动管理事件循环的创建和销毁,并提供了更简洁的语法。
 
 
 
 

🎒
离开乏味的皮囊,自由的灵魂在路上
  • Name: Alan Hsu
  • Tag: 随感、技术、经验、旅行、推荐、生活、音乐、电影 etc.
  • Email:xulanzhong521gmail.com
  • WeChat: Alan_Hsu_521
notion image
notion image
 
 
CentOS7安装Chrome以及浏览器驱动进行自动化Notion All in One:搭建高能效率系统
  • Twikoo
  • Cusdis