记录最近的针对网关报文的Excel处理工具设计路线。

##记录最近的针对网关报文的Excel处理工具设计路线。

需求:

首先说下开发的需求:最近有个产品是汽车网关。汽车网关主要功能和路由器差不多,也就是查询某条报文是否需要被转发。虽然没有家用路由器那么多的协议和额外功能,但报文比较多,一般汽车上也会有400-600条不同的报文。有些报文需要以高频的速度转发到不同的车身节点上。也就是说网关需要了解这几百条报文从哪来,到哪去。网关要快速响应并转发。

这几百条报文在代码中实现不算复杂,即使只用Switch和if语句也可以实现。但考虑到后续的维护,这些判断语句一定会给我们照成网关查询响应速度慢和报文变更麻烦等一系列问题。因此我希望可以将设计和维护聚焦在报文和后续变更上。而不需要考虑报文转发的代码逻辑。

假设我们有10条新增的报文需要被转发,那我们只需要在数据结构(或数据库)中添加这10条报文,而不需要修改网关的底层转发代码。如果我们使用Switch和if语句,我们不需要数据结构(或数据库),但得不断的往转发代码中添加条件,这会导致代码臃肿,可读性也会直线下降。

所以我需要设计一个简单的工具,来统一管理报文表和转发代码的关系。这个工具可以让我导入路由表后生成一份报文转发代码。这份代码我可以直接合成进我的网关工程中。网关每次收到报文后都会执行这份代码,看是否需要被转发之后进行后续的动作。

这样一来对变更的管理就从对代码管理变成了对表格的管理。如果有报文变更,我只需要维护报文表就行了。

目标:

假设需求上有600条不同的报文,如果使用巨大的switch和if语句是不合理也的。因为网关作快速响应的硬件,如果查询时间太长是会影响性能的。而且网关路由的变更是非常频繁的,经常需要添加、删除或对转发条件做出修改。

我有几个设计的目标和对应的方法:

  • 网关的查询快捷——需要使用二分查询算法来查询报文。
  • 报文变更方便——将客户需求整理成Excel。维护Excel就相当于维护程序。
  • 报文变更不影响网关程序结构——使用DAO方式生成C文件。将C文件合成进网关工程。实现模块化。

设计:

报文有几个主要属性,分别是:源地址、报文ID、报文长度、目的地址、周期。

还有有些次要属性,属于标定项,意思是当达成特定条件时特定的报文才需要被转发(或者说不应该被转发)。

我选择用Python来写。主要有几个原因:

  • 比起其他语言我Python会的多点。
  • Python有一些现成的Excel处理库,可以很方便的使用。
  • 我对界面的设计要求不高,即使后续添加新的功能也可以使用简单的界面添加。

下面是整个设计思路:

根据客户提供的需求把600条报文整理后放进Excel中,软件从Excel文件中提取数据,做出排序去重和判断后生成一份我们需要的C语言文件。

  • 我需要软件在Windows7上运行。所以选用3.8版本的Python。
  • 界面使用Python原生的tkinter库。配合外部的TKinterDesigner做界面设计。
  • Excel文件的数据提取使用第三方的xlwings库。
  • 数据提取后使进行数据验证,排序和去重。这部分使用一些算法就可以实现。
  • DAO功能使用Python中string库的Template模板替换。
  • 软件的打包可以使用Python原生的PyInstaller库,也可以使用TKinterDesigner直接发布。

其中还需要用到一些小东西。

  • tkinter是单线程界面,配合外部Excel提取需要使用多线程。
  • tkinter可以添加进度条让用的人看起来舒服一些。
  • 界面提醒可以使用日志打印也可以使用输出重定向,把程序运行情况打印到界面上或写入日志文件。
  • DAO的实现可以配合Json的解析。让模板替换的自定义程度更高一些。

Python中函数装饰器@

##Python中函数装饰器@

Python中的@是函数修饰器,它其实是种语法糖。本身并不会语言的功能产生影响,但可以更方便程序员的使用。让代码看起来更清晰和简洁。

def funA(fn):
    ...


def funB():
    ...


funB = funA(funB)

在上面这段代码中我们需要注意几个地方:

  • 这里定义了两个函数,分别是funA和funB。而funA是有参数的。
  • 在这段代码的最后还执行了一行语句。函数funB被当作参数传入了funA。并且,把funB重新赋值成了funA(funB)的返回值。

在上面的代码中有一件事悄悄发生了,那就是函数funB的地址已经找不到了。

代码中的函数funB已经成为了funA(funB)的返回值。如果返回值为None,那funB则为None。

如果使用了函数装饰器@,那上面的代码与下面的代码是等价的。

#funA 作为装饰器函数
def funA(fn):
    ...


#funB 作为被装饰函数,funB 被 funA 装饰
@funA
def funB():
    ...

这段代码同样有值得注意的地方:

  • 函数funA是必须有参数的,因为它需要把函数funB作为参数传入。
  • 语句funB = funA(funB)似乎“消失”了。但这条语句依然会被执行,只是被隐藏了。

结合上面两段代码来看,函数装饰器@的作用就是把被装饰函数传给装饰函数,以此来进行功能上的扩展。但需要强调,被装饰函数会被重新赋值为装饰器的返回值!


##单个修饰器,修饰不带参数的函数

函数装饰器@经常可以被用来,插入日志、性能测试、事务处理、记录时间戳等等。

假设现在我们新写了个功能,需要用日志进行测试。但我们不应该直接把日志功能加入到新的函数中。因为这会导致功能耦合,代码的可读性非常的差。在这种情况下,我们可以来看个使用函数装饰器@的例子:

def log(function):
    def wrapper():
        print("log start...")
        function()
        print("log end...")
    return wrapper
 
@log
def test():
    print("test...")
 
test()

代码执行结果如下:

log start...
test...
log end...

上面这段代码等价于:

def log(function):
    def wrapper():
        print("log start...")
        function()
        print("log end...")
    return wrapper
 
def test():
    print("test...")

test = log(test)
test()

因为函数log返回了一个函数给test,实际上也就是返回了函数的地址给test。这也test才能被调用。如果函数log返回None,那test的地址内容也就是None,会抛出错误:

TypeError: ‘NoneType’ object is not callable

因为标识符test为None,而我们却想把空值None当作函数来调用,这当然是不可行的。


##单个修饰器,修饰带参数的函数

上面被修饰的函数是没有传入参数的,如果我们新写的函数是有传入参数的怎么办呢?我们可以这样写:

def log(function):
    def wrapper(*arg, **kwargs):
        print("log start...")
        function(*arg, **kwargs)
        print("log end...")

    return wrapper


@log
def test(arg):
    print(arg)


test("test_num")

代码的执行结果如下:

log start...
test_num
log end...

使用“*arg, **kwargs”的话被装饰函数test的所有参数都会被传入到装饰器中。


##多个修饰器

一个被函数可以被多个修饰器修饰。假设,我某个函数我既要观察日志,也需要记录时间戳。我们就可以使用函数修饰器来进行多个事务上的处理。

def log(function):
    def wrapper(*arg, **kwargs):
        print("log start...")
        function(*arg, **kwargs)
        print("log end...")

    return wrapper


def time_stamp(function):
    def wrapper(*arg, **kwargs):
        print("time start...")
        function(*arg, **kwargs)
        print("time end...")

    return wrapper


@time_stamp
@log
def test(arg):
    print(arg)


test("test_num")

代码的执行结果如下:

time start...
log start...
test_num
log end...
time end...

在上面的修饰器会先被执行。


还需要考虑一个问题,修饰器可以嵌套么?

def log(function):
    def wrapper(*arg, **kwargs):
        print("log start...")
        function(*arg, **kwargs)
        print("log end...")



    return wrapper


@log
def time_stamp(function):
    def wrapper(*arg, **kwargs):
        print("time start...")
        function(*arg, **kwargs)
        print("time end...")


    return wrapper

@time_stamp
def test(arg):
    print(arg)

对于上面这段代码,并没有按照预期的那样正常运行,会抛出错误。即使在内置函数wrapper加上返回值return function,也不会按照预期的那样执行函数test的内容。

尽量还是不要使用嵌套的函数修饰符。

Python的应用程序打包方式——PyInstaller

##Python的应用程序打包方式——PyInstaller

PyInstaller并不是一个Python的原生模块,因此需要自己下载。可以使用pip的方式在线下载或自行下载.whl文件离线安装PyInstaller模块。

但无论是那种方式,在准备好打包环境后才正式开始阅读这篇文章。


当你拥有一个Python项目,你想把你的项目打包成一个.exe的运行程序。这样你就可以直接其他人的电脑上运行,无需安装Python解释器,也无需进行编译。

打包的过程结束后,可能会出现几个东西,分别是:__pycache__文件夹、build文件夹、dist文件夹和一个单独的.spec文件。

  • __pycache__文件夹,名称很直白,Python Cache,也就是Python缓存的意思。这个文件会出现在你项目自身的目录里。Python解释器会将* .py 脚本文件进行编译,并将结果保存到__pycache__目录中。下次运行工程时,若解释器发现这个.py 脚本没有修改过,就会跳过编译这文件,直接运行以前生成的保存在__pycache__的.pyc 文件。这对于大型工程是有好处的,可以大大缩短项目运行前的准备时间。如果不想暴露源码也可以使用.pyc 文件文件。但这个文件夹本身不属于PyInstaller产生的,而是Python解释器产生的。
  • build文件夹,是PyInstaller在打包程序时产生的临时文件夹。
  • dist文件夹,是PyInstaller打包出来的可执行文件目录。打包完成后的程序会放在该目录中。一般有两种方式,一种方式是生成目录,另一种方式是生成单一的.exe应用程序。
  • .spec文件,是PyInstaller为打包而准备的配置文件。就像一份清单一样,你想指定你的项目如何打包,打包成什么样。例如是否使用图标、项目使用的资源文件在哪、如何指定项目依赖等等。这些都可以先写入.spec文件中,之后使用PyInstaller的命令,根据.spec里的内容将你的项目打包成你想要的应用程序。似于cmake的.makefile文件,都是用于控制编译构建过程的配置文件。

-h,–help查看PyInstaller的帮助信息。
-F,-onefile生成单个的可执行文件。
-D,–onedir生成一个目录(包含多个文件)作为可执行程序,这也是默认方式。
-o DIR,–out=DIR指定.spec文件生成的目录。如果没有指定,则默认在当前目录生成.spec 文件
-n NAME,–name=NAME指定项目(产生的.spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为.spec的名字。
-w,–windowed,–noconsolc指定程序运行时不显示命令行窗口(仅对 Windows 有效)。

将项目打包成单一可执行文件和文件夹有什么区别么?

  • 单一可执行文件比文件夹的启动时间要长。因为当程序运行时,单一的可执行文件需要解压程序的第三方依赖文件到临时文件夹中。
  • 单一可执行文件的文件结构和工程目录是一样的。但是生成文件夹就不一样了,若程序中包含相对路径,这个相对路径是文件夹目录的,这点需要注意。

有更多问题可以在PyInstaller的wiki中查看:https://github.com/pyinstaller/pyinstaller

PyInstaller打包不同的Python版本程序。

##PyInstaller打包不同的Python版本程序。

因为Python 3.9以后的版本不再支持Windows 7系统,当我们打包的.exe程序需要在Windows 7系统上运行时,必须使用较低版本的Python解释器,如Python 3.8。

我一开始使用Python 3.10打包程序,当生成的.exe程序到Windows7系统上运行时候会报错误:

无法启动此程序,因为计算机中丢失api-ms-win-core-path-l1-1-0.dll

因此,我需要用Python 3.8来打包我程序。但我们在打包时使用的命令:PyInstaller -F。是使用当前系统环境变量中默认的Python解释器版本。

也就是说,当系统环境变量中默认为Python 3.10(已安装了PyInstaller包),那PyInstaller命令打包的程序会默认使用Python 3.10的环境。

我并没有更好的办法,只能新下载一个Python 3.8的解释器,并且将环境变量也设置成Python 3.8。还需要在Python 3.8中安装PyInstaller包。否则PyInstaller -F命令可能会无效或继续使用Python 3.10的解释器。

环境变量中Python 3.8的路径在Python 3.10上方。在终端中执行的Python命令会优先使用Python 3.8的解释器。如果找不到才会去找Python 3.10的解释器。

在系统变量中我使用了Python 3.8的版本,并且安装了PyInstaller包。接下来使用PyInstaller的打包命令:PyInstaller -F main.py。打包后的.exe程序使用的就是系统变量中的Python 3.8版本解释器。

总结:如果你希望你打包的程序使用某个版本的Python解释器,例如Python 3.8版本。那你需要做以下几件事:

  • 1、下载你需要的版本的Python解释器——下载Python 3.8版本的解释器。
  • 2、将系统变量配置成你需要的Python解释器——在命令行(终端)中输入Python后显示的是Python 3.8版本。
  • 3、安装PyInstaller包——PyInstaller必须在Python 3.8的环境中。
  • 4、在你的程序中使用PyInstaller的打包命令——在打包时候显示Python: 3.8。

在打包时显示使用的Python版本就是你程序所使用的Python解释器。

这里我多说两句,通常我们的开发环境和我们使用的操作系统环境是不一样的。我开发的程序使用的是Python 3.10。因为我的IDE指向的是某个固定的解释器,也就是Python 3.10。但我的操作系统环境变量是指向另一个解释器。而终端是依附操作系统的,使用终端打包就容易出现,打包环境和开发环境不一致的情况。