当前位置: 首页 > news >正文

Gradle 核心之 Task

一、前言


只有 Task 才可以在 Gradle 的执行阶段去执行(其实质是执行的 Task 中的一系列 Action),所以 Task 的重要性不言而喻。

二、Task


2.1 Task 定义与配置

Task 的定义方式有如下两种:

Task 的配置方式也有如下两种:

配置了 group 后可以在 Android Studio 的 Gradle 面板看到对应的 Task Group 及其分组下的 Tasks,如下图所示:

一般来说都推荐为我们的 task 配置 group,便于我们查找 task。另外,group 和 descreption 只是最基本的配置,我们看下 Task 的源码:

可以看到这些属性都是可以进行配置的,后面会一一讲解。

选型描述默认值
nametask 名字无,必须指定
type需要创建的 task ClassDefaultTask
action当 task 执行的时候,需要执行的闭包 closure 或 行为 Actionnull
overwrite替换一个已存在的 taskfalse
dependsOn该 task 所依赖的 task 集合[]
group该 task 所属组null
descriptiontask 的描述信息null
constructorArgs传递到 task Class 构造器中的参数null

2.2 Task 执行

上一节我们定义了两个 task,我们来执行其中一个:

看到上述输出我们会有个疑问,为什么我们执行 helloTask2,但是 helloTask 也被执行输出了呢?其实很简单,因为这两个 task 都是在 gradle 配置阶段执行的,所以我们任何 task 的执行,我们 project 的整个配置代码都是会执行的,所以这两个输出语句都会被执行到。

我们可以通过添加 doFirst 与 doLast 执行动作(Action)为我们的 task 指定执行阶段要执行的代码,这样它就只会在 gradle 执行阶段去执行。需要注意的是,doFirst 和 doLast 是可以被执行多次的。对于 doFirst 与 doLast 这两个 Action,它们的作用分别如下所示:

  • doFirst:表示 task 执行最开始的时候被调用的 Action。
  • doLast:表示 task 将执行完的时候被调用的 Action。

我们来验证下:

接下来,我们就使用 doFirst 与 doLast 来进行一下实战,来实现计算 build 执行期间的耗时,其完整代码如下所示:

2.3 Task 执行顺序

指定 Task 的执行顺序有三种方式,如下图所示:

2.3.1 dependsOn 强依赖方式

dependsOn 强依赖的方式可以细分为静态依赖和动态依赖,首先看看静态依赖,如下所示:

taskZ 依赖 taskX 和 taskY:

执行 taskZ 看看:

可以看到被依赖的 task 先执行,这和我们 java 的继承关系是很相似的。需要注意的是,这里 taskX 和 taskY 的执行顺序是随机的。

下面我们再来看看动态依赖:

2.3.2 通过Task输入输出指定

我们也可以通过 Task 来指定输入输出,Task 的输入输出对应 TaskInput 和 TaskOutput。下面我们来看一个示例,使用这种方式实现一个自动维护版本发布文档的 gradle 脚本,其中输入输出相关的代码如下所示:

首先,我们定义了一个 WirteTask,然后,在注释1处,指定了输出文件为 destFile, 并写入版本信息到 XML 文件。接着,定义了一个 readTask,并在注释2处,指定输入文件为上一个 task(即 writeTask) 的输出文件。最后,在注释3处,使用 dependsOn 将这两个 task 关联起来,此时输入与输出的顺序是会先执行写入,再执行读取。这样,一个输入输出的实际案例就实现了。

2.3.3 通过API指定执行顺序

除了 dependsOn 的方式,我们还可以在 task 闭包中通过 mustRunAfter 方法指定 task 的依赖顺序,mustRunAfter 可以指定一个或多个 task,其示例代码如下所示:

下面我们在命令行中将 taskX、taskY、taskZ 打乱执行:

可以看到最终的执行顺序始终是 taskX、taskY、taskZ。

mustRunAfter 是强制指定顺序,另外还有一个 shouldRunAfter 不强制性指定,实际应用中一般不会使用 shouldRunAfter,了解一下即可。

2.4 挂接自定义 task 到构建生命周期

我们可以使用 gradle 提供的一系列生命周期 API 去挂接我们自己的 task 到构建生命周期之中,比如使用 afterEvaluate 方法将我们第三小节定义的 writeTask 挂接到 gradle 配置完所有的 task 之后的时刻,示例代码如下所示:

2.5 Task 类型

除了定义一个新的 task 之外,我们也可以使用 task 的 type 属性来直接使用一个已有的 task 类型,比如 Gradle 自带的 Copy、Delete、Sync task 等等。示例代码如下所示:

更多的 Task 类型我们可以查阅官方文档。


Android 对 Gradle 的扩展:Variants、Transform

一、前言


本篇我们来学习下 Android 对 Gradle 的扩展:Variants(变体)以及 Transform。通过扩展可以让我们在自定义 Gradle 插件时做更多的事情。

二、Variants(变体)


2.1 Variants 是什么

要理解 Variants 的作用,就必须先了解 buildType、flavor、dimension 与 variant 之间的关系。在 android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension,可以理解为 flavor 的分组,然后不同 dimension 里的 flavor 会组合成一个 variant。示例代码如下所示:

    android {...defaultConfig {...}//gradle 默认就有 debug 和 release 两个 buildTypebuildTypes {debug{...}release{...}}flavorDimensions "version"productFlavors {demo {dimension "version"}full {dimension "version"}}}

根据上述配置 Gradle 会创建以下构建变体:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 Variants 共有 三种类型,如下所示:

  • applicationVariants:只适用于 app plugin。
  • libraryVariants:只适用于 library plugin。
  • testVariants:在 app plugin 与 libarary plugin 中都适用。

2.2 Variants 的使用

我们来看看 applicationVariants 的使用,首先我们在 app.gradle 中配置 buildTypes、flavorDimensions、productFlavors 同上。然后,我们可以 使用 applicationVariants.all 在配置阶段之后去获取所有 variant 的 name 与 baseName。代码如下所示:

最后我们来执行下 gradle clean 任务:

可以看到,name 与 baseName 的区别:demoDebug 与 demo-debug 。

接下来我们来看看使用 applicationVariants.all 在配置阶段之后去修改输出的 APK 名称:

可以看到,我们上面用到了一个 releaseTime() 方法获取当前时间:

最后我们来执行以下 gradle clean:

可以看到正常修改了 apk 的名称。

最后我们来看一下如何对 applicationVariants 中的 Task 进行 Hook,我们可以在 android.applicationVariants.all 的闭包中通过 variant.task 来获取相应的 Task。代码如下所示:

然后,执行 gradle clean,其输出信息如下所示:既然可以获取到变体中的 Task,我们就可以根据不同的 Task 类型来做特殊处理。例如,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,其核心代码如下所示:

至于 update_plugin 的实现,主要就是一些插件安全校验与下载的逻辑,这部分其实跟 Gradle 没有什么联系。

variant 中能获取到哪些 task 我们可以去 ApplicationVariant 的父类 BaseVariant 中去查看,比如:

2.3 Gradle 构建流程

在执行 Android 项目的构建流程,可以发现没有任何修改的情况下就已经有 30 多个Task需要执行:

 其中关键的 task 如下:

三、Transform


Google 官方在 Android Gradle V1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是实现 Transform 来对 .class 文件遍历以拿到所有方法,修改完成后再对原文件进行替换即可。总的来说,Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件。我们可以通过 Gradle Plugin 来注册我们编写的 Transform。注册后的 Transform 会被 Gradle 包装成一个 Gradle Task,这个 TransForm Task 会在 java compile Task 执行完毕后运行。

我们来看看 Transform 的执行流程图:

3.1 Transform 的使用

下面我们来看看如何使用 Transform,首先如果是在 buildSrc 中,由于 buildSrc 的执行时机要早于任何一个 project,因此需要添加仓库:

然后,创建一个 Transform 的子类继承自 com.android.build.api.transform.Transform:

可以看到其创建步骤可以细分为五步,如下所示:

3.1.1、getName()

指定自定义 Transform 的名称。返回对应的 Task 名称。

3.1.2、getInputTypes()

可以看到这个方法返回的是一个 Set<QualifiedContent.ContentType> 集合,指明你自定义的这个 Transform 处理的输入类型集合。QualifiedContent.ContentType 是一个接口,它的实现类有 DefaultContentType 和 ExtendedContentType。为了方便 TransformManager 为我们封装了以下几种输入类型集合:

分别代表的是:

  • CONTENT_CLASS:表示需要处理 java 的 class 文件。
  • CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
  • CONTENT_RESOURCES:表示需要处理 java 的资源文件。
  • CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
  • CONTENT_DEX:表示需要处理 DEX 文件。
  • CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
3.1.3、getScopes()

可以看到这个方法返回的是一个 Set<QualifiedContent.Scope> 集合,用来指明自定义的 Transform 的输入文件所属的范围,这是因为 gradle 是支持多工程编译的。Scope 是一个枚举类:

可以看到目前有 5 种基本类型,分别代表的是:

  • PROJECT:只有项目内容。
  • SUB_PROJECTS:只有子项目。
  • EXTERNAL_LIBRARIES:只有外部库,
  • TESTED_CODE:由当前变体(包括依赖项)所测试的代码。
  • PROVIDED_ONLY:只提供本地或远程依赖项。

同样,为了方便,TransformManager 为我们封装了 getScope 的返回:

如果一个 Transform 不想处理任何输入,只是想查看输入的内容,那么只需在 getScopes() 返回一个空集合,然后在getReferencedScopes() 返回想要接收的范围。

 
  1. public Set<? super Scope> getReferencedScopes() {

  2. return ImmutableSet.of();

  3. }

3.1.4、isIncremental()

isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。

3.1.5、transform(TransformInvocation transformInvocation)

它是 Transform 的关键方法,在 transform() 方法中,就是用来给我们进行具体的输入输出转换过程的。它是一个空实现,input 的内容将会打包成一个 TransformInvocation 对象,因为我们要想使用 input,我们需要详细了解一下 TransformInvocation 参数。

public interface TransformInvocation {// 输入作为 TransformInput 返回Collection<TransformInput> getInputs(); //TransformOutputProvider 可以用来创建输出内容TransformOutputProvider getOutputProvider(); boolean isIncremental();
...
}

TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分,如下所示:

public interface TransformInput {Collection<JarInput> getJarInputs();Collection<DirectoryInput> getDirectoryInputs();
}public interface JarInput extends QualifiedContent {File getFile(); //jar文件Set<ContentType> getContentTypes(); // 是class还是resourceSet<? super Scope> getScopes();  //属于Scope:
}

其中:

  • DirectoryInput 集合:表示以源码方式参与项目编译的所有目录结构与其目录下的源码文件。
  • JarInput 集合:表示以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。需要注意的是,这个 jar 所指也包括 aar。

TransformOutputProvider 表示 Transform 的输出,利用它我们可以获取输出路径等信息。

public interface TransformOutputProvider {//根据 name、ContentType、QualifiedContent.Scope返回对应的文件( jar / directory)File getContentLocation(String name, Set<QualifiedContent.ContentType> types, Set<? super QualifiedContent.Scope> scopes, Format format);
}

即我们可以通过 TransformInvocation 来获取输入,同时也获得了输出的功能。举个例子:

    @Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)println '--------------- MyCustomTransform visit start --------------- 'def startTime = System.currentTimeMillis()def inputs = transformInvocation.inputsdef outputProvider = transformInvocation.outputProvider// 1、删除之前的输出    if (outputProvider != null) {outputProvider.deleteAll()}// Transform 的 inputs 有两种类型,一种是目录,一种是 jar包,要分开遍历    inputs.each { TransformInput input ->// 2、遍历 directoryInputs(本地 project 编译成的多个 class⽂件存放的目录)        input.directoryInputs.each { DirectoryInput directoryInput ->handleDirectory(directoryInput, outputProvider)}// 3、遍历 jarInputs(各个依赖所编译成的 jar 文件)        input.jarInputs.each { JarInput jarInput ->handleJar(jarInput, outputProvider)}}def cost = (System.currentTimeMillis() - startTime) / 1000println '--------------- MyCustomTransform visit end --------------- ' println "MyCustomTransform cost : $cost s"}

这里我们主要是做了三步处理,如下所示:

  • 删除之前的输出。
  • 遍历 directoryInputs(本地 project 编译成的多个 class ⽂件存放的目录)。
  • 遍历 jarInputs(各个依赖所编译成的 jar 文件)。

在 handleDirectory 与 handleJar 方法中则是进行了相应的 文件处理 && ASM 字节码修改。

编写完 Transform 的代码之后,我们就可以在 Plugin 的 apply 方法中加入下面代码去注册 TransformTest 的实例,代码如下所示:

相关文章:

Gradle 核心之 Task

一、前言 只有 Task 才可以在 Gradle 的执行阶段去执行&#xff08;其实质是执行的 Task 中的一系列 Action&#xff09;&#xff0c;所以 Task 的重要性不言而喻。 二、Task 2.1 Task 定义与配置 Task 的定义方式有如下两种&#xff1a; Task 的配置方式也有如下两种&#xf…...

【React 】折叠面板,点击展开时再请求数据

需求背景&#xff1a;使用折叠面板的形式展示数据&#xff0c;面板内部数据需要在打开时请求接口获取。 遇到问题&#xff1a;最开始使用Antd 的折叠面板组件&#xff0c;它对于数据直接渲染是没问题的&#xff0c;但是不好满足打开面板时再动态加载数据的需求&#xff0c;于是…...

c++学习 文件操作,模板

文件操作 #include<iostream> #include<string> #include<fstream> using namespace std; //文本操作 //程序运行时产生的数据都属于临时数据&#xff0c;程序一旦运行结束都会被释放 //通过文件可以数据持久化 //c中对文件操作包含头文件<fstream> /…...

开源与在线 M3U8 Downloader 项目介绍及使用指南

M3U8 是一种用于播放列表格式的文件类型&#xff0c;广泛应用于流媒体服务中&#xff0c;特别是 HLS&#xff08;HTTP Live Streaming&#xff09;协议。它包含了一系列的 TS&#xff08;Transport Stream&#xff09;视频片段地址&#xff0c;使得视频能够分段加载&#xff0c…...

正则表达式与文本处理器

正则表达式 基础正大表达式 查看特定字符 grep grep-n the test.txt grep-in the test.txt-n 显示行号 -i 不区分大小写 -v 反转查找 [] &#xff1a;中括号里可以写元素&#xff0c;内容符合任意元素&#xff0c;就会过滤出来 ^ :写在中括号里&#xff0c;代表取反。以^开头&…...

RedisTemplate方法一览表

数据类型RedisTemplate 方法Redis命令解释应用场景stringopsForValue().set(key, value)SET设置存储在指定 key 下的值存储简单数据&#xff0c;如用户的设置、配置项opsForValue().get(key)GET获取存储在指定 key 下的值读取存储的数据&#xff0c;如用户信息、配置参数opsFor…...

个人对devops的一点见解

DevOps 是一种将开发&#xff08;Development&#xff09;和运维&#xff08;Operations&#xff09;相结合的理念和实践方法。 它强调打破开发团队和运维团队之间的传统壁垒&#xff0c;促进两个团队之间更紧密的协作和沟通&#xff0c;以实现更高效、更快速、更可靠的软件交付…...

HarmonyOS鸿蒙应用开发基础知识

参考 HarmonyOS鸿蒙应用开发 (二、应用程序包结构理解及Ability的跳转&#xff0c;与Android的对比)_hap(harmonyos ability package)包的开发-CSDN博客 HarmonyOS NEXT下一代编程语言仓颉介绍及入门-CSDN博客...

Halcon 根据霍夫变换在图像中寻找直线

一 霍夫变换 1 定义 霍夫变换是图像处理中从图像中识别几何形状的基本方法之一.几何形状包括圆&#xff0c;椭圆&#xff0c;直线等等. 2 直线方程 直线的方程可以用yk*xb 来表示&#xff0c;其中k和b是参数&#xff0c;分别是斜率和截距; 3 霍夫变换原理&#xff1a; 设…...

基于Openmv的追小球的云台

介绍 在这篇文章&#xff0c;我会先介绍需要用到且需要注意的函数&#xff0c;之后再给出整体代码 在追小球的云台中&#xff0c;比较重要的部分就是云台&#xff08;实质上就是舵机&#xff09;的控制以及对识别的色块位置进行处理得到相应信息后控制云台进行运动 1、舵机模…...

关于scrapy模块中setting.py文件的介绍

作用 在Scrapy框架中&#xff0c;settings.py 文件起着非常重要的作用&#xff0c;它用于配置和控制整个Scrapy爬虫项目的行为、性能和功能。 setting.py文件的介绍 # Scrapy settings for haodaifu project # # For simplicity, this file contains only settings consider…...

laravel Blade 指令的趣味性

首先&#xff0c;我们通过几个要点来解释 Blade 引擎的工作原理。 您选择一个 Blade 模板进行渲染。引擎使用一系列正则表达式来解析和编译模板。该引擎生成一个普通的 PHP 文件并将其写入磁盘&#xff08;以便将其缓存以供将来渲染&#xff09;。包含 PHP 文件并使用输出缓冲…...

【面试题】等保(等级保护)的工作流程

等保&#xff08;等级保护&#xff09;的工作流程主要包括以下几个步骤&#xff0c;以下将详细分点介绍&#xff1a; 系统定级&#xff1a; 确定定级对象&#xff1a;根据《信息系统等级保护管理办法》和《信息系统等级保护定级指南》的要求&#xff0c;确定需要进行等级保护的…...

python调用麦克风和扬声器,并调用阿里云实时语音转文字

import time import queue import sounddevice as sd import numpy as np import nls import sys# 阿里云配置信息 URL "wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1" TOKEN "XXXX" # 参考https://help.aliyun.com/document_detail/450255.html获…...

描述在React中集成第三方库(如Redux或React Router)的常见模式。

在React中集成第三方库&#xff0c;如状态管理库Redux或路由库React Router&#xff0c;通常遵循一些常见的模式和最佳实践。下面是一些集成这些库的步骤和模式&#xff1a; 集成Redux 安装Redux及相关包: 安装Redux及其中间件&#xff08;如redux-thunk或redux-saga&#xf…...

JavaScript语法特性篇-空值合并运算符(??)

1、基本使用 空值合并运算符&#xff08;??&#xff09;英文名称为 Nullish coalescing operator&#xff0c;是一个逻辑运算符。 特性&#xff1a;当左侧的操作数为 null 或者 undefined 时&#xff0c;返回其右侧操作数&#xff0c;否则返回左侧操作数。 const foo nul…...

rancher快照备份至S3

巧用rancher的S3快照备份功能&#xff0c;快速实现集群复制、集群转移、完全崩溃后的极限修复 1.进入集群管理&#xff0c;在对应的集群菜单后&#xff0c;点击编辑配置 2.选择ETCD&#xff0c;启用&#xff0c;Backup Snapshots to S3选项 并填入你的minio 3 配置成功后 手…...

ChatGPT API教程在线对接OpenAI APIKey技术教程

一、OpenAI基本库介绍 您可以通过 HTTP 请求与 API 进行交互&#xff0c;这可以通过任何编程语言实现。我们提供官方的 Python 绑定、官方的 Node.js 库&#xff0c;以及由社区维护的库。 要安装官方的 Python 绑定&#xff0c;请运行以下命令&#xff1a; pip install open…...

随心而遇,跟着感觉走

分数限制下&#xff0c;选好专业还是选好学校&#xff1f; 24年高考结束&#xff0c;很多学生犹豫选择专业还是好学校&#xff0c;我的建议是&#xff0c;选择好学校。 本人体验来说&#xff0c;电子&#xff0c;工地&#xff0c;计科&#xff0c;数学&#xff0c;工科相关的…...

LeetCode题练习与总结:只出现一次的数字--136

一、题目描述 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;且该算法只使用常量额外空间。 示例 1 &#xff1a; …...

常见的中间件都在解决什么问题?

常见的中间件都在解决什么问题 RocketMQ RocketMQ 是一款功能强大的分布式消息系统。 RocketMQ 源码地址&#xff1a;https://github.com/apache/rocketmq(opens new window) RocketMQ 官方网站&#xff1a;https://rocketmq.apache.org 什么场景下用 RocketMQ&#xff1f…...

微信小程序-scroll-view实现上拉加载和下拉刷新

一.scroll-view实现上拉加载 scroll-view组件通过自身一些属性实现上拉加载的功能。 lower-threshold“100"属性表示距离底部多少px就会实现触发下拉加载的事件。 类似于在.json文件里面配置"onReachBottomDistance”: 100 bindscrolltolower"getMore"属…...

TS中interface和type的区别

在 TypeScript 中&#xff0c;interface 和 type 都可以用来定义对象的类型&#xff0c;但它们之间存在一些差异。 以下是 interface 和 type 的主要区别&#xff1a; 扩展&#xff08;Extending&#xff09;: interface 可以通过 extends 关键字来扩展其他 interface。interfa…...

Hightec编译器系列之高级调试技巧精华总结

Hightec编译器系列之高级调试技巧精华总结 小T为了便于大家理解&#xff0c;本文的思维导图大纲如下&#xff1a; 之前可能很多小伙伴没有使用过Hightec编译器&#xff0c;大家可以参考小T之前的文章《Hightec编译器系列之白嫖就是爽》可以下载一年试用版本。 小T使用过适配英…...

【论文笔记】LoRA LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS

题目&#xff1a;LoRA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS 来源: ICLR 2022 模型名称: LoRA 论文链接: https://arxiv.org/abs/2106.09685 项目链接: https://github.com/microsoft/LoRA 文章目录 摘要引言问题定义现有方法的问题方法将 LORA 应用于 Transformer 实…...

【Sa-Token|4】Sa-Token微服务项目应用

若微服务数量多&#xff0c;如果每个服务都改动&#xff0c;工作量大&#xff0c;则可以只在网关和用户中心进行改动&#xff0c;也是可以实现服务之间的跳转。 这种方式可以通过在网关服务中生成和验证 Sa-Token&#xff0c;并将其与现有的 Token关联存储在 Redis 中。用户中心…...

鸿蒙开发系统基础能力:【@ohos.hilog (日志打印)】

日志打印 hilog日志系统&#xff0c;使应用/服务可以按照指定级别、标识和格式字符串输出日志内容&#xff0c;帮助开发者了解应用/服务的运行状态&#xff0c;更好地调试程序。 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用…...

SpringMVC系列十: 中文乱码处理与JSON处理

文章目录 中文乱码处理自定义中文乱码过滤器Spring提供的过滤器处理中文 处理json和HttpMessageConverter<T>处理JSON-ResponseBody处理JSON-RequestBody处理JSON-注意事项和细节HttpMessageConverter<T\>文件下载-ResponseEntity<T\>作业布置 上一讲, 我们学…...

使用MyBatisPlus进行字段的自动填充

使用MyBatisPlus进行字段的自动填充 需求场景 当我们往数据库里面插入一条数据&#xff0c;或者是更新一条数据时&#xff0c;一般都需要标记创建时间create_time和更新时间update_time的值&#xff0c;但是如果我们每张表的每个请求&#xff0c;在执行sql语句的时候我们都手…...

python爬虫之aiohttp多任务异步爬虫

python爬虫之aiohttp多任务异步爬虫 爬取的flash服务如下&#xff1a; from flask import Flask import timeapp Flask(__name__)app.route(/bobo) def index_bobo():time.sleep(2)return Hello boboapp.route(/jay) def index_jay():time.sleep(2)return Hello jayapp.rout…...