插件开发中如何执行一个异步任务:IDEA Plugin 开发和 Android 开发很类似,一些耗时的任务是不能直接在主线程执行的,需要在特定后台线程执行,否则会阻塞主线程。在 intellij open api 中有个 Task.Backgroundable 抽象类就是处理异步任务的。Backgroundable 继承了 Task 类以及实现了 PerformInBackgroundOption 接口。具体使用很简单传入两个参数一个是 Project 对象和一个执行异步中 hint 提示文本,有四个回调函数分别为 run (progress: ProgressIndicator)、onSuccess、onThrowable、onFinished. 最后通过 queue 方法加入到异步任务队列中。为了方便调用将其封装成一个扩展函数来使用。//创建后台异步任务的Project的扩展函数asyncTaskprivate fun Project.asyncTask( hintText: String, runAction: (ProgressIndicator) -> Unit, successAction: (() -> Unit)? = null, failAction: ((Throwable) -> Unit)? = null, finishAction: (() -> Unit)? = null) { object : Task.Backgroundable(this, hintText) { override fun run(p0: ProgressIndicator) { runAction.invoke(p0) } override fun onSuccess() { successAction?.invoke() } override fun onThrowable(error: Throwable) { failAction?.invoke(error) } override fun onFinished() { finishAction?.invoke() } }.queue()}//asyncTask的使用 project?.asyncTask(hintText = "正在压缩", runAction = { //执行图片压缩操作 outputSameFile.yes { //针对右键选定图片情况,直接压缩当前目录选中图片,输出目录包括文件也是原来的 inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(inputFile.absolutePath) } }.otherwise { inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(getDestFilePath(model, inputFile.name)) } } }, successAction = { successAction?.invoke() }, failAction = { failAction?.invoke("TinyPng key存在异常,请重新输入") })插件开发中如何获取当前选中的文件或目录在插件开发中如何获得当前选中文件,实际上 open api 提供了类似 DataContext 数据上下文环境,我们需要去拿到文件集合对象就需要先找到文件管理的窗口对象,还记得上篇博客中说到的 AnActionEvent 对象是插件与 IDEA 交互通信的一个媒介,通过 AnActionEvent 内部的 dataContext 的 getData 方法,传入对应的 DataKey 对象获得相应的窗口对象。在 CommonDataKey 中有一个 DataKey<VirtualFile []>,通过传入当前 event 中的 dataContext 对象即可获得当前选中的文件对象集合。 private fun DataContext.getSelectedFiles(): Array<VirtualFile>? { return DataKeys.VIRTUAL_FILE_ARRAY.getData(this)//右键获取选中多个文件,扩展函数 }api key 的验证和图片压缩的实现在进行图片压缩前就是需要去验证一下 TingPng ApiKey 的合法性,如果第一次验证合法就需要把该 ApiKey 存储在本地,下次压缩就直接使用本地的 key 进行压缩,一旦本地 key 失效后,需要重新弹出 TinyPng apikey 的验证提示框,进行重新认证。当然需要注意的是验证 api key 的合法性也是进行一次同步的网络请求所以它也要放在异步任务执行。fun checkApiKeyValid( project: Project?, apiKey: String, validAction: (() -> Unit)? = null, invalidAction: ((String) -> Unit)? = null) { if (apiKey.isBlank()) { invalidAction?.invoke("TinyPng key为空,请重新输入") } project?.asyncTask(hintText = "正在检查key是否合法", runAction = { try { Tinify.setKey(apiKey) Tinify.validate() } catch (exception: Exception) { throw exception } }, successAction = { validAction?.invoke() }, failAction = { println("验证Key失败!!${it.message}") invalidAction?.invoke("TinyPng key验证失败,请重新输入") })}然后就是利用异步任务进行图片压缩操作。
常用选项命令选项说明-no-snapshot-load执行冷启动,并在退出时保存模拟器状态。-no-snapshot-save执行快速启动,但在退出时不保存模拟器状态。-no-snapshot彻底停用快速启动功能。-camera-back-camera-front设置后置或前置相机的模拟模式。emulated:模拟器在软件中模拟相机。webcamn:模拟器使用连接到开发计算机的摄像头,由数字指定,例如 webcam0。none:在虚拟设备中停用相机。-webcam-list列出开发计算机上可用于模拟的摄像头。-memory指定物理 RAM 大小,范围为从 128 MB 到 4096 MB。-sdcard指定 SD 卡分区映像文件的文件名和路径。-wipe-data删除用户数据并从初始数据文件中复制数据。-debug启用或停用一个或多个标记的调试消息显示。-logcat启用一个或多个标记的 logcat 消息显示,并将其写入终端窗口。-show-kernel在终端窗口中显示内核调试消息。-verbose将模拟器初始化消息输出到终端窗口。-dns-server使用指定的 DNS 服务器。-http-proxy通过指定的 HTTP/HTTPS 代理进行所有 TCP 连接。-netdelay模拟设置网络延迟-netfast停用网络节流功能。-netspeed设置网络速度模拟。-port设置用于控制台和 adb 的 TCP 端口号。-tcpdump捕获网络数据包并将其存储在文件中。-accel配置模拟器虚拟机加速。-accel-check检查是否已安装模拟器虚拟机加速所需的管理程序(HAXM 或 KVM)。-engine指定模拟器引擎:auto:自动选择引擎(默认值)。classic:使用较旧的 QEMU 1 引擎。qemu2:使用较新的 QEMU 2 引擎。-gpu选择 GPU 模拟模式。-version显示模拟器版本号。-no-boot-anim在模拟器启动期间停用启动动画以加快启动速度。-screen设置模拟触摸屏模式。touch:模拟触摸屏(默认值)。multi-touch:模拟多点触控屏幕。no-touch:停用触摸屏和多点触控屏幕模拟。高级选项命令选项说明-bootchart启用 bootchart,设有超时(以秒为单位)。-cache指定缓存分区映像文件。-cache-size设置缓存分区大小(以 MB 为单位)。-data设置用户数据分区映像文件。-datadir使用绝对路径指定数据目录。-force-32bit在 64 位平台上使用 32 位模拟器。-help-disk-images获取有关磁盘映像的帮助。-help-char-devices获取有关字符 device 规范的帮助。-help-sdk-images获取与应用开发者相关的磁盘映像的帮助。-help-build-images获取与平台开发者相关的磁盘映像的帮助。-initdata指定数据分区的初始版本。-kernel使用特定的模拟内核。-noaudio停用对此虚拟设备的音频支持。-nocache启动没有缓存分区的模拟器。-no-snapshot禁止自动加载和保存操作。-no-snapshot-load阻止模拟器从快照存储加载 AVD 状态。-no-snapshot-save阻止模拟器在退出时将 AVD 状态保存到快照。-no-window停用模拟器上的图形窗口显示。-partition-size指定系统数据分区大小(以 MB 为单位)。-prop在启动时在模拟器中设置 Android 系统属性。-ramdisk指定 ramdisk 启动映像。-shell在当前终端上创建根 shell 控制台。-sysdir使用绝对路径指定系统目录。-system指定初始系统文件。-writable-system使用此选项在模拟会话期间创建可写系统映像。
函数名说明支持平台 onLoad 页面加载时触发,一个页面只会调用一次。可以传递参数,参数说明查看示例 3.2.1 所有 onShow 页面显示时触发,每次打开页面都会调用一次所有 onReady 页面初次渲染完成后触发,一个页面只会调用一次所有 onHide 页面隐藏时触发,每次隐藏页面都会被触发所有 onUnload 页面卸载时触发所有 onResize 页面每次窗口尺寸变化时会被触发,App、微信小程序 onPullDownRefresh 用户下拉页面时触发,一般用于页面下拉刷新,查看实例 3.2.2 所有 onReachBottom 页面上拉滚动触底时触发所有 onTabItemTap 点击底部 tab 栏时触发,参数为 Object,参数说明查看实例 3.2.3 微信小程序、百度小程序、H5、ApponShareAppMessage 点击右上角分享时触发微信小程序、百度小程序、字节跳动小程序、支付宝小程序 onPageScroll 页面滚动时触发,只监听页面垂直滚动所有 onNavigationBarButtonTap 监听原生标题栏按钮点击事件,参数说明查看实例 3.2.4App、H5onBackPress 页面返回时触发,查看实例 3.2.5App、H5onNavigationBarSearchInputChanged 监听原生标题栏搜索输入框输入内容变化事件 App、H5onNavigationBarSearchInputConfirmed 监听原生标题栏搜索输入框搜索事件 App、H5onNavigationBarSearchInputClicked 监听原生标题栏搜索输入框点击事件 App、H53.2.1 onLoad 参数说明页面生命周期函数 onLoad,是页面最先执行的生命周期函数,如果从上个页面跳转到本页面,可以通过参数 option 传递上个页面的数据。我们来看一下实例,比如要从首页(index.vue)跳转到个人中心页面(me.vue)。实例://index.vue//跳转语句,并在跳转链接上面加上要传递的数据uni.reLaunch({ url: 'test?name=我是首页的数据'});//me.vueexport default { //options参数就是上个页面传递过来的数据 onLoad: function (options) { console.log(options.name); }}//打印出来的结果我是首页的数据3.2.2 onPullDownRefresh 函数用于监听该页面用户下拉页面的动作。普通页面下拉不会触发 onPullDownRefresh 函数,要先在 pages.json 里面,找到当前页面的 pages 节点,并在 style 选项中将 enablePullDownRefresh 设置为 true,下拉页面才可以触发 onPullDownRefresh 函数。下面来看一下实例,比如我们想开启首页(index.vue)的下拉动作。实例://pages.json{ "path": "pages/index/index.vue", "style": { "enablePullDownRefresh": true }}调用完成 onPullDownRefresh 函数后,用 stopPullDownRefresh 函数可以停止当前页面的下拉状态。实例:export default{ onPullDownRefresh(){ console.log('用户下拉页面时触发') uni.stopPullDownRefresh() }}3.2.3 onTabItemTap 参数说明属性类型说明 indexString 被点击 tabItem 的序号,从 0 开始 pagePathString 被点击 tabItem 的页面路径 textString 被点击 tabItem 的按钮文字实例:export default { onTabItemTap(options) { console.log('被点击tabItem的序号index:' + options.index) console.log('被点击tabItem的页面路径pagePath:' + options.pagePath) console.log('被点击tabItem的按钮文字text:' + options.text) }}注意以下几点:onTabItemTap 常用于点击当前 tabitem,滚动或刷新当前页面。如果是点击不同的 tabitem,一定会触发页面切换。如果想在 App 端实现点击某个 tabitem 不跳转页面,不能使用 onTabItemTap,可以使用 plus.nativeObj.view 放一个区块盖住原先的 tabitem,并拦截点击事件。在 App 端,从 HBuilderX 1.9 的自定义组件编译模式才开始支持 onTabItemTap。3.2.4 onNavigationBarButtonTap 参数说明属性类型说明 indexNumber 原生标题栏按钮数组的下标实例:export default { onNavigationBarButtonTap(options) { console.log('index:' + options.index) }}3.2.5 onBackPress 参数说明返回结果的格式是这样的:event = {from:backbutton、 navigateBack}。其中 backbutton 表示来源是左上角返回按钮或 android 返回键;navigateBack 表示来源是 uni.navigateBack。属性类型说明 fromString 触发返回行为的来源:‘backbutton’—— 左上角导航栏按钮及安卓返回键;‘navigateBack’——uni.navigateBack () 方法实例:export default { onBackPress(options) { console.log('from:' + options.from) }}
lxml 是 Python 中的一个解析库,支持 HTML 和 XML 的解析,支持 XPath 解析方式,而且解析效率非常高。本节将安装该模块解析 html 文本并提取相应的数据。[store@server2 ~]$ sudo pip3 install lxmlWARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.Collecting lxml Downloading http://mirrors.cloud.aliyuncs.com/pypi/packages/55/6f/c87dffdd88a54dd26a3a9fef1d14b6384a9933c455c54ce3ca7d64a84c88/lxml-4.5.1-cp36-cp36m-manylinux1_x86_64.whl (5.5MB) 100% |████████████████████████████████| 5.5MB 82.9MB/s Installing collected packages: lxmlSuccessfully installed lxml-4.5.1我们先准备好素材,也就是要解析的 HTML 文档。为了更有代入感,我直接使用慕课网 wiki 页面的数据进行操作,获取数据的方式如下图所示:获取慕课网 wiki 页面的 HTML 数据最后保存到一个 test.html 文本,然后我们要准备一段 Python 代码:from lxml import etreetree = etree.parse('test.html', etree.HTMLParser(encoding='utf8'))def print_result(exp, results): print('xpath表达式为:{},其匹配结果为:'.format(exp)) for res in results: print(res.strip()) print('')def test_xpath_expression(exp): results = tree.xpath(exp) print_result(exp, results)将这个 Python 文件命名为 test_xpath.py 和 test.html 放在同一级目录下:[store@server2 ~]$ lsshen test.html test_xpath.py接下来我们就可以进行激动人心的测试了,来完成一个简单的实验:慕课网 wiki 页面数据获取第一个实验的目标就是拿到 javascript 分类下的教程的三个数据:标题、总节数以及访问次数。通过 F12 查看相关的 HTML 结构,我们可以通过如下的 Xpath表达式获取相应的数据:Python 3.6.8 (default, Apr 2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> from test_xpath import test_xpath_expression>>> exp1 = '//h2[@class="language-title"]/text()'>>> test_xpath_expression(exp1)xpath表达式为://h2[@class="language-title"]/text(),其匹配结果为:JavaScriptHTML & CSS服务器开发工具其他后端语言基础应用框架应用基础应用Python Web 开发MySQL接下来看一看元素的结构:javascript 专栏的节点结构可以看到 javascript 专栏标题是 h2 节点,这个节点同级下有一个 div,它下面的四个 div 节点正是那四个专栏。我们首先匹配下这四个专栏元素:>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]'>>> test_xpath_expression(exp1)xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"],其匹配结果为:<Element div at 0x7f7015bf8808><Element div at 0x7f700c656788><Element div at 0x7f700c6567c8><Element div at 0x7f700c656808>那么我们来进一步分析每个 div 内部如何得到教程标题、总节数以及访问次数这些数据:获取教程数据可以看到,在前面找到 div 节点的基础上在往下两层,找到 class 属性值为 text 的 div 节点,所有的数据都在这个节点中:标题:上面找到的 div 节点下的第一个 a 节点的文本值;教程总节数:上面找到的 div 节点下的第一个 p 节点下第一个 span 元素的文本值;总访问次数:上面找到的 div 节点下的第一个 p 节点下第二个 span 元素的文本值;这样我们就能进行写出提取相应数据的 Xpath 路径表达式了,测试如下:>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text()'>>> test_xpath_expression(exp1)xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text(),其匹配结果为:Javascript 入门教程TypeScript 入门教程Vue 入门教程Ajax 入门教程>>> exp2 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text()'>>> test_xpath_expression(exp2)xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text(),其匹配结果为:56小节38小节39小节9小节>>> exp3 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text()'>>> test_xpath_expression(exp3)xpath表达式为://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text(),其匹配结果为:9832354736281800接下来我们整理下 Python 代码,将整个 wiki 页面上的教程都解析出来,并将数据整理成 json 格式。预期最后的结果应该是这样的:{ '前端开发': { 'JavaScript': [ {'title': 'JavaScript入门教程', 'total_chapters': 56, 'total_visited': 9001}, {...}, {...}, {...} ], 'HTML & CSS': [ ... ] } '服务端相关': { }, ...}这样的难度再次增加,其核心的获取数据的过程和上面一致。后面获取其他数据的结果过程不作分析,大家有兴趣仔细研究下代码,然后动手实操。话不多说,上代码:# 代码文件:test_xpath2.pyfrom lxml import etreedef get_direction_data(direction_tree): """ 获取一个方向下的课程数据 :return: """ direction_data = {} cards = direction_tree.xpath('.//div[@class="language-card"]') for card in cards: title = card.xpath('.//h2[@class="language-title"]/text()')[0] course_list = card.xpath('.//div[@class="course-card"]') courses = [] for course in course_list: course_title = course.xpath('.//div[@class="text"]/a[1]/text()')[0] course_total_chaps = course.xpath('.//div[@class="text"]/p/span[1]/text()')[0] course_total_visit_count = course.xpath('.//div[@class="text"]/p/span[2]/text()')[0] courses.append({ 'course_title': course_title.strip(), 'course_total_chaps': course_total_chaps.strip(), 'course_total_visit_count': int(course_total_visit_count.strip()) }) direction_data[title] = courses return direction_datadef get_all_data(): """ 解析慕课网wiki数据 :return: """ result = {} html = etree.parse('test.html', etree.HTMLParser(encoding='utf8')) directions = html.xpath('//div[@class="direction-con"]') for direction in directions: # 提取方向key,注意一定要有点号,表示从当前元素开始提取 direction_name = direction.xpath('./div[@class="title-con"][1]/text()') if direction_name: result[direction_name[0]] = get_direction_data(direction) return result运行的结果如下:[store@server2 ~]$ python3Python 3.6.8 (default, Apr 2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> from test_xpath2 import get_all_dat>>> get_all_data(){'前端开发': {'JavaScript': [{'course_title': 'Javascript 入门教程', 'course_total_chaps': '56小节', 'course_total_visit_count': 9832}, {'course_title': 'TypeScript 入门教程', 'course_total_chaps': '38小节', 'course_total_visit_count': 3547}, {'course_title': 'Vue 入门教程', 'course_total_chaps': '39小节', 'course_total_visit_count': 3628}, {'course_title': 'Ajax 入门教程', 'course_total_chaps': '9小节', 'course_total_visit_count': 1800}], 'HTML & CSS': [{'course_title': 'CSS3 入门教程', 'course_total_chaps': '32小节', 'course_total_visit_count': 1512}, {'course_title': 'Less 入门教程', 'course_total_chaps': '22小节', 'course_total_visit_count': 364}, {'course_title': '雪碧图入门教程', 'course_total_chaps': '24小节', 'course_total_visit_count': 915}]}, '服务端相关': {'服务器': [{'course_title': 'Nginx 入门教程', 'course_total_chaps': '24小节', 'course_total_visit_count': 4500}, {'course_title': 'HTTP 入门教程', 'course_total_chaps': '16小节', 'course_total_visit_count': 456}, {'course_title': 'Docker 入门教程', 'course_total_chaps': '25小节', 'course_total_visit_count': 1067}, {'course_title': 'Shell 入门教程', 'course_total_chaps': '17小节', 'course_total_visit_count': 2060}, {'course_title': 'Linux 入门教程', 'course_total_chaps': '25小节', 'course_total_visit_count': 1430}], '开发工具': [{'course_title': 'Gradle 入门教程', 'course_total_chaps': '12小节', 'course_total_visit_count': 1121}, {'course_title': 'Vim 入门教程', 'course_total_chaps': '14小节', 'course_total_visit_count': 1491}, {'course_title': 'RESTful 规范教程', 'course_total_chaps': '13小节', 'course_total_visit_count': 1316}, {'course_title': 'Markdown 入门教程', 'course_total_chaps': '31小节', 'course_total_visit_count': 733}, {'course_title': 'Maven 入门教程', 'course_total_chaps': '17小节', 'course_total_visit_count': 155}, {'course_title': 'GitHub 入门教程', 'course_total_chaps': '9小节', 'course_total_visit_count': 261}], '其他后端语言': [{'course_title': 'C 语言入门教程', 'course_total_chaps': '45小节', 'course_total_visit_count': 1933}, {'course_title': 'Go 入门教程', 'course_total_chaps': '36小节', 'course_total_visit_count': 691}, {'course_title': 'Ruby 入门教程', 'course_total_chaps': '26小节', 'course_total_visit_count': 410}]}, 'Java': {'基础应用': [{'course_title': 'Java 入门教程', 'course_total_chaps': '39小节', 'course_total_visit_count': 5229}, {'course_title': 'Android 入门教程', 'course_total_chaps': '29小节', 'course_total_visit_count': 553}, {'course_title': '算法入门教程', 'course_total_chaps': '11小节', 'course_total_visit_count': 628}], '框架应用': [{'course_title': 'Spring Boot 入门教程', 'course_total_chaps': '25小节', 'course_total_visit_count': 4861}, {'course_title': 'Spring 入门教程', 'course_total_chaps': '21小节', 'course_total_visit_count': 850}, {'course_title': 'Hibernate 入门教程', 'course_total_chaps': '23小节', 'course_total_visit_count': 619}, {'course_title': 'MyBatis 入门教程', 'course_total_chaps': '23小节', 'course_total_visit_count': 895}]}, 'Python': {'基础应用': [{'course_title': 'Python 入门语法教程', 'course_total_chaps': '24小节', 'course_total_visit_count': 3617}, {'course_title': 'Python 原生爬虫教程', 'course_total_chaps': '19小节', 'course_total_visit_count': 2001}, {'course_title': 'Python 进阶应用教程', 'course_total_chaps': '29小节', 'course_total_visit_count': 726}], 'Python Web 开发': [{'course_title': 'Django 入门教程', 'course_total_chaps': '33小节', 'course_total_visit_count': 668}, {'course_title': 'NumPy 入门教程', 'course_total_chaps': '21小节', 'course_total_visit_count': 152}]}, '数据库': {'MySQL': [{'course_title': 'MySQL 入门教程', 'course_total_chaps': '32小节', 'course_total_visit_count': 3638}, {'course_title': 'SQL 入门教程', 'course_total_chaps': '47小节', 'course_total_visit_count': 2406}]}}是不是实现了预期效果?爬取网页,解析数据的过程和这个类似。掌握好今天的内容,你就已经掌握了爬虫的一个核心步骤。