惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
A
About on SuperTechFans
I
InfoQ
F
Full Disclosure
美团技术团队
Martin Fowler
Martin Fowler
量子位
V
V2EX
小众软件
小众软件
爱范儿
爱范儿
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
T
Tailwind CSS Blog
博客园 - 三生石上(FineUI控件)
N
Netflix TechBlog - Medium
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
IT之家
IT之家
云风的 BLOG
云风的 BLOG
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
T
Threat Research - Cisco Blogs
T
Threatpost
P
Proofpoint News Feed
腾讯CDC
博客园 - 司徒正美
Jina AI
Jina AI
The Hacker News
The Hacker News
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 热门话题
S
Securelist
U
Unit 42
T
The Exploit Database - CXSecurity.com
博客园 - Franky
NISL@THU
NISL@THU
D
Docker
The GitHub Blog
The GitHub Blog
Latest news
Latest news
S
Schneier on Security
J
Java Code Geeks
Blog — PlanetScale
Blog — PlanetScale
P
Palo Alto Networks Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
使用 Python 开发插件化应用程序 - 元视角
飞鸿踏雪 · 2019-10-11 · via 元视角

插件化应用是个老话题啦,在我们的日常生活中更是屡见不鲜。无论是多年来臃肿不堪的 Eclipse,亦或者是扩展丰富著称的 Chrome,乃至近年来最优秀的编辑器 VSCode,插件都是这其中重要的组成部分。插件的意义在于扩展应用程序的功能,这其实有点像 iPhone 手机和 AppStore 的关系,没有应用程序的手机无非就是一部手机,而拥有了应用程序的手机则可以是 Everything。显然,安装或卸载应用程序并不会影响手机的基本功能,而应用程序离开了手机同样无法单独运行。所以,所谓“插件”,实际上是一种按照一定规范开发的应用程序,它只能运行在特定的软件平台/应用程序且无法运行。这里,最重要的一点是应用程序可以不依赖插件单独运行,这是这类“插件式”应用的基本要求。

好了,在了解了插件的概念以后,我们来切入今天的正文。博主曾经在《基于 Python 实现 Windows 下壁纸切换功能》这篇文章中编写了一个小程序,它可以配合 Windows 注册表实现从 Unsplash 上抓取壁纸的功能。最近,博主想为这个小程序增加 必应壁纸WallHaven 两个壁纸来源,考虑到大多数的壁纸抓取流程是一样的,博主决定以“插件”的方式完成这次迭代,换句话说,主程序不需要再做任何调整,当我们希望增加新的数据源的时候,只需要写一个.py 脚本即可,这就是今天这篇文章的写作缘由。同样的功能,如果使用 Java 或者 C#这类编译型语言来做,我们可能会想到为插件定义一个 IPlugin 接口,这样每一个插件实际上都是 IPlugin 接口的实现类,自然而然地,我们会想到通过反射来调用接口里的方法,这是编译型语言的做法。而面对 Python 这样的解释型语言,我们同样有解释型语言的做法。

首先,我们从一个最简单的例子入手。我们知道,Python 中的 import 语法可以用来引入一个模块,这个模块可以是 Python 标准库、第三方库和自定义模块。现在,假设我们有两个模块:foo.pybar.py

#foo.py
import sys

class Chat:

    def send(self,uid,msg):
        print('给{uid}发送消息:{msg}'.format(uid=uid,msg=msg))

    def sendAll(self,msg):
        print('群发消息:{msg}'.format(msg=msg))

#bar.py
import sys

class Echo:

    def say(self):
        print("人生苦短,我用Python")

def cry():
    print("男人哭吧哭吧不是罪")

通常, 为了在当前模块(main.py)中使用这两个模块,我们可以使用以下语句:

import foo
from bar import *

这是一种简单粗暴的做法,因为它会导入模块中的全部内容。一种更好的做法是按需加载,例如下面的语句:

到这里,我们先来思考第一个问题,Python 是怎么样去查找一个模块的呢?这和 Python 中的导入路径有关,通过sys.path我们可以非常容易地找到这些路径,常见的导入路径有当前目录site-package目录PYTHONPATH。熟悉 Python 的朋友应该都知道,site-packagePYTHONPATH各自的含义,前者是通过 pip 安装的模块的导入目录,后者是 Python 标准库的导入目录。当前目录这个从何说起呢?事实上,从我们写下from…import…语句的时候,这个机制就已经在工作了,否则 Python 应该是找不到 foo 和 bar 这两个模块的了。这里还有相对导入和绝对导入的问题,一个点(.)和两个点(..)的问题,这些我们在这里暂且按下不表,因为我们会直接修改sys.path(逃

在 Python 中有一种动态导入模块的方式,我们只需要告诉它模块名称、导入路径就可以了,这就是下面要说的importlib标准库。继续用 foo 和 bar 这两个神奇的单词来举例,假设我们现在不想通过 import 这种偏“静态”的方式导入一个模块,我们应该怎么做呢?一起来看下面代码:

import foo
from foo import Chat
from bar import *
import importlib

#调用foo模块Chat类方法
foo.Chat().send('Dear','I Miss You')
moduleFoo = importlib.import_module('.','foo')
classChat = getattr(moduleFoo,'Chat')
classChat().send('Dear','I Miss You')

#调用bar模块Echo类方法
Echo().say()
moduleBar = importlib.import_module('.','bar')
classEcho = getattr(moduleBar,'Echo')
classEcho().say()

#调用bar模块中的cry()方法
cry()
methodCry = getattr(moduleBar,'cry')
methodCry()

可以注意到,动态导入可以让我们在运行时期间引入一个模块(.py),这恰恰是我们需要的功能。为了让大家对比这两种方式上的差异,我给出了静态引入和动态引入的等价代码。其中,getattr()其实可以理解为 Python 中的反射,我们总是可以按照模块->->方法的顺序来逐层查找,即:通过 dir()方法,然后该怎么调用就怎么调用。所以,到这里整个“插件化”的思路就非常清晰了,即:首先,通过配置来为 Python 增加一个导入路径,这个导入路径本质上就是插件目录。其次,插件目录内的每一个脚本文件(.py)就是一个模块,每个模块都有一个相同的方法签名。最终,通过配置来决定要导入哪一个模块,然后调用模块中类的实例方法即可。顺着这个思路,博主为 WallPaper 项目引入了插件机制,核心代码如下:

if(pluginFile == '' or pluginName == ''):
        spider = UnsplashSpider()
        imageFile = spider.getImage(downloadFolder)
        setWallPaper(imageFile)
    else:
        if(not check(pluginFile,addonPath)):
            print('插件%s不存在或配置不正确' % pluginName)
            return
        module = importlib.import_module('.',pluginFile.replace('.py',''))
        instance = getattr(module,pluginName)
        imageFile = instance().getImage(downloadFolder)
        setWallPaper(imageFile)

接下来,我们可以很容易地扩展出 必应壁纸WallHaven 两个“插件”。按照约定,这两个插件都必须实现 getImage()方法,它接受一个下载目录作为参数,所以,显而易见,我们在这个插件里实现壁纸的下载,然后返回壁纸的路径即可,因为主程序会完成剩余设置壁纸的功能。

# 必应每日壁纸插件
class BingSpider:

    def getImage(self, downloadFolder):
        searchURL = 'https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN'
        response = requests.get(searchURL)
        data = json.loads(response.text)

        resultId = data['images'][0]['hsh']
        resultURL = 'https://cn.bing.com' + data['images'][0]['url']
        print(u'正在为您下载图片:%s...' % resultId)
        if(not path.exists(downloadFolder)):
            os.makedirs(downloadFolder)
        
        jpgFile = resultId + '.jpg'
        jpgFile = os.path.join(downloadFolder, jpgFile)
        response = requests.get(resultURL)
        with open(jpgFile,'wb') as file:
            file.write(response.content)
        return jpgFile      

# WallHaven壁纸插件
class WallHavenSpider:

    def getImage(self,downloadFolder): 
        url = 'https://alpha.wallhaven.cc/wallpaper/' 
        response = requests.get(url) 
        print(response.text)
        soup = BeautifulSoup(response.text,'html.parser')
        imgs = soup.find_all('img')
        length = len(imgs)
        if length > 0:
            match = random.choice(imgs)
            rawUrl = match.get('src')
            rawId = rawUrl.split('/')[-1]
            rawUrl = 'https://w.wallhaven.cc/full/' + rawId[0:2] + '/wallhaven-' + rawId
            raw = requests.get(rawUrl) 
            imgFile = os.path.join(downloadFolder, rawId)
            with open(imgFile,'wb') as f:
                f.write(raw.content)
        return imgFile  

好了,现在功能是实现了,我们来继续深入“插件化”这个话题。考虑到 Python 是一门解释型的语言,我们在编写插件的时候,更希望做到“热插拔”,比如修改了某个插件后,希望它可以立刻生效,这个时候我们就需要重新加载模块,此时 importlib 的 reload 就能满足我们的要求,这正是博主一开始就要使用 importlib,而不是 import 语法对应内建方法__import__()的原因。以 C#的开发经历而言,虽然可以直接更换 DLL 实现更新,可更新的过程中 IIS 会被停掉,所以,这种并不能被称之为“热更新”。基于以上两点考虑,博主最终决定使用 watchdog 配合 importlib 来实现“热插拔”,下面是关键代码:

class LoggingEventHandler(FileSystemEventHandler):

    # 当配置文件修改时重新加载模块
    # 为节省篇幅已对代码进行精简
    def on_modified(self, event):
        super(LoggingEventHandler, self).on_modified(event)
        what = 'directory' if event.is_directory else 'file'
        confPath = os.path.join(sys.path[0],'config.ini')
        if(what =='file' and event.src_path == confPath):
            importlib.reload(module)
        logging.info("Modified %s: %s", what, event.src_path)

好了,现在我们就完成了这次“插件化”的迭代,截止到目前为止,博主共完成了 UnsplashBing 壁纸WallHaven国家地理 四个“源”的接入,这些插件在实现上基本大同小异,本质上来讲它们是一个又一个的爬虫,只要实现了 getImage()这个方法都可以接入进来,这就是我们通常说的“约定大于配置”,关于更多的代码细节,大家可以通过Github来了解。

简单回顾下这篇博客,核心其实是 importlib 模块的使用,它可以让我们在运行时期间动态导入一个模块,这是实现插件化的重要前提。以此为基础,我们设计了基于 Python 脚本的单文件插件,即从指定的目录加载脚本文件,每个脚本就是一个插件。而作为插件化的一个延伸,我们介绍了 watchdog 模块的简单应用,配合 importlib 模块的 reload()方法,就可以实现所谓的“热更新”。好了,以上就是这篇博客的所有内容了,我们下一篇见!