又有一段时间没有写新文章了,在这段时间里,我悄悄把网站升级成https了,这从一定程度上保证了网站访问的安全性,除此之外我还写了一个爬虫。这个爬虫用来爬取某个不能说的网站上的视频。后来这个爬虫能稳定运行后,我又想采集另一个网站的视频,所以我就尝试把程序作为一个爬虫调度平台,将爬虫作为插件的方式来开发。以后有想采集新的网站、app的数据,就可以以插件的方式进行开发及添加。这样才符合所谓的“开闭原则”。

这篇文章不会主要介绍爬虫部分,也不会主要介绍多线程爬虫的调度和管理,只介绍Python的插件化设计。如果有需要了解爬虫或是这个系统的其他部分的,后面单独开文章介绍。
本文部分内容(有可能)会涉及到爬虫开发,在进行爬虫开发时,请注意以下法律问题:

1. 网站的使用条款:在爬取网站的信息之前,应该阅读并遵守网站的使用条款。
2. 个人隐私保护:爬取的信息中可能包含个人隐私信息,在使用这些信息时应该遵守相关法律规定。
3. 知识产权:爬取的信息中可能包含知识产权信息,应该遵守版权法。
4. 访问限制:某些网站可能会对爬虫进行限制,在爬取信息时应该遵守相关限制。

其他具体内容请查询你所在国家/地区的相关法律法规。

什么是插件化,为什么要插件化?

插件化的意思就是将程序的某些功能以插件的方式加入到程序中,或者通过插件对程序功能进行定制或扩展。将程序插件化可以使得程序动态的增减功能,同时也提高了程序整体的可维护性。

用游戏来打比方,游戏本体就是程序本体,插件就像是游戏的mod,通过安装插件可以增加、删除或修改游戏本体的内容。从而优化游戏或更改游戏内容。

将程序插件化的好处有很多,不过我没有去仔细地收集这些资料,就凭我自己的印象来说:

  1. 降低程序耦合度:从开发角度来说,最大的好处就是可以降低程序的耦合度,将程序中的各个模块分离开来,每个模块间单独运行互不影响,在对某个功能进行更新或修改的时候,也能尽量减少对程序的影响。
  2. 提高可扩展性:程序在开发时一般不太容易一次性完成所有功能的开发,所以系统在使用的时候,可能会有新的需求进入。如果把所有功能都集成到程序本体中,编写这些新的需求就很可能会对程序本体进行修改,这样就违反了开闭原则(对于开闭原则,我后面应该会再开一个“程序设计模式”的系列,又开一个新坑...),使用插件化设计就可以很好的解决这个问题,有新的需求直接开发一个新的插件,不会对原程序有任何影响。
  3. 易于维护:插件如果出现问题,结合日志系统就能很方便的对出现问题的地方进行定位。
  4. 其他好处: ...想不到了,但是应该不止这些好处

简单的开发思路

要开发一个插件系统,首先应该开发一套用于插件开发的sdk。插件开发者基于这套sdk进行插件开发,最后的插件成品为软件包的形式,最后在本体系统中对检查通过的插件使用importlib.import_module进行载入。

详细一点的说,按照以下步骤来:

  1. 设定一个通用的插件目录,用于存放插件。同时也可以自定义插件分类,因为插件管理器是面向对象开发,所以可以对不同目录中的插件分类管理。
  2. 创建一个插件管理类,PluginsManager,这个类用于插件的加载,检查,卸载,初始化等等。使用类来对插件管理,基于面向对象的方法,这样的好处是每个插件管理类都是一个单独的对象,每个对象可以管理不同的插件,有不同的配置。
  3. 为插件设计一套存储元数据的方法,并设计一个标准初始化入口。在将插件载入完成后,插件管理器会分别执行每个插件的初始化函数,以此来对插件进行初始化。
  4. 创建一个全局的插件管理对象用于管理插件,同时也可以按需创建多个不同的插件管理对象。
  5. 设计一套sdk,供插件功能开发使用。
  6. 插件管理器的扩展设计,比如依赖检查、安全性检查、权限分类、高危操作监控等等。

有了这一系列开发思路,我们就可以按照这个思路来开发插件程序了。

着手实现插件系统

这个插件系统是我的爬虫平台中的一个服务的一部分,我会把这个插件系统的部分代码放上来供参考,不过因为一些不太好细说的原因,我可能会删掉一部分东西,或者对代码进行一些修改。但是按照我描述的步骤来一步一步操作,(应该)还是可以正常运行的。

首先是插件管理器类PluginsManager,我们先为这个类设计几个方法:

首先是初始化方法,在插件管理类初始化时,需要一个变量指定对应的插件目录,同时需要一个字典来保存载入的插件名称及对应包对象,然后需要为插件管理器实例化一个日志器(日志器Logger是这个插件平台的一个模块,负责日志管理,也是我自己开发的,不过这个日志器不是今天的重点,就不过多介绍了)用于管理插件管理器的日志。

代码如下:

1
2
3
4
5
6
class PluginsManager:
    def __init__(self, plugins_floder):
        self.plugins = {}
        self.plugins_floder = plugins_floder
        self._logger = Logger("PluginsManager")
        sys.path.append(self.plugins_floder)

在上述代码中,最后一行sys.path.append(self.plugins_floder)是为了把插件目录添加到python查找路径中,否则在导入包时python会提示找不到对应的包。

在完成插件管理器的实例化后,我们需要对插件进行导入。我为导入插件设计了两个方法,load_plugin和load_all_plugins,从名字就可以看出,这两个方法分别对应的功能就是载入某个插件和载入所有插件。载入所有插件的实现方法很简单,就是对插件目录下的所有包执行load_plugin就行,这里就不细说了,直接上代码:

1
2
3
4
5
6
7
# 查找和加载所有插件
def load_all_plugins(self):
    self._logger.info(f'开始从 {self.plugins_floder} 加载插件...')
    # 获取plugins文件夹下所有插件
    for plugin in os.listdir(self.plugins_floder):
        self.load_plugin(plugin)
    self._logger.info(f'插件加载完成, 共加载 {len(self.plugins)} 个插件: {list(self.plugins.keys())}')

加载插件(load_plugin)的过程就比较复杂了,在介绍插件加载方法之前,先说说插件的格式。

我设计的插件其实就是一个python的包,所以其最基本应该满足python包的格式,也就是说,在插件的根目录下应该有一个__init__.py文件。

示例插件的文件结构

在导入插件前,我们需要找个地方来保存插件的元数据,以此来保存插件的名称、版本号、依赖插件、版权信息、程序入口等。既然我们python包里面包含__init__.py文件,那么把这些元数据保存在这里面即可。

示例插件的meta信息,其中logout_test是初始化入口