目录

一、服务端

1. XML配置文件

2. 服务端代码设计

二、客户端

1. XML配置文件

2. 客户端代码设计

三、运行效果

1. 程序目录结构

2. 服务端运行效果

3. 客户端运行效果

四、改进思路

五、文件下载

软件客户端在发布新版本的时候,有时候只修改了几个文件,没必要让用户重新下载整个客户端再重新安装,同时也不应要求用户每次去手动下载更新的文件,再手动覆盖本地文件。这个时候需要设计一个自动升级机制,在某些条件触发时(比如软件启动的时候)自动查看是否有更新,如果有就将改变的内容下载下来,更新本地旧文件,再根据情况判断是否重启客户端。这个功能现在是桌面程序必备的功能,基本所有的客户端都有这个检查更新的功能。我曾经用Python实现过一个基于http下载的简易自动升级系统,可以独立运行、复用在不同的情景下。

设计思路很简单:当有新版本需要发布时,将文件放在服务端,生成一个记录每个文件变化的配置文件。客户端本地也有一个记录文件信息的配置文件,客户端检查更新时,将服务端的配置文件下载下来,与本地配置文件进行比较,然后下载有变化的文件,覆盖本地文件(如果文件正在使用中,可能无法覆盖,这时候更新前应该先关闭正在运行的客户端),中间有Tkinter做的界面提示更新进度。更新结束后根据策略决定是否重启客户端。

一、服务端

服务端要做的事,首先是选择一个端口号,开启用于响应客户端下载的http服务。然后把指定的目录下的所有文件都扫描一遍,给每个文件记录一个版本号和最后修改日期,再生成一个总版本号,写在XML配置文件里。

比如版本号从0开始,第一次发布程序时,每个文件的版本号都是0,总版本号也是0,第二次发布时,扫描每个文件的最后修改日期,如果日期大于XML文件中记录的日期,将这个文件的记录日期更新,版本号加1。扫描完毕,只要有任意文件的版本号发生变化,总版本号也加1。这样客户端在检查更新时,只需要先比较服务端的总版本号和自己本地的总版本号是否一致。如果不一致,再下载XML文件比较每一个文件版本号变化,如果一致就不用下载XML文件比较了(可以在服务端增加一个接口,客户端请求这个接口时返回一个总版本号字段)。

1. XML配置文件

1.1 XML配置文件结构

ServerInfo节点:记录服务端IP和端口号,可以让客户端知道去哪里下载,当下载地址或端口号变化时,通过更新这个节点,客户端下次更新时就会到新的地址和端口号下载。

ClientVersion节点:要升级的模块的文件信息,包含1个总版本号属性,子节点包括该模块下每个文件的相对路径、文件大小、最后更新时间和版本号。这个节点可以设计多个,用不同的节点名,区分不同的模块,每个模块都有自己的总版本号。这里以1个模块为例。

1.2 XML配置文件示例:

<?xml version="1.0" encoding="utf-8"?>
<versionInfo><ServerInfo><ServerIp>202.169.100.52</ServerIp><!--服务端ip地址--><ServerPort>8888</ServerPort><!--服务端端口号--><XmlLocalPath>client_path</XmlLocalPath><!--存放文件的路径--></ServerInfo><!--服务端信息--><ClientVersion Version="11"><object><FileRelativePath>ClientVersion/cfg.ini</FileRelativePath><!--文件相对路径--><FileSize>177</FileSize><!--文件大小B--><LastUpdateTime>2019-04-29 16:27:35</LastUpdateTime><!--文件最后修改时间--><Version>10</Version><!--文件版本号--></object><!--文件节点--><object><FileRelativePath>ClientVersion/Scripts/config.py</FileRelativePath><!--文件相对路径--><FileSize>6567</FileSize><!--文件大小B--><LastUpdateTime>2019-04-02 14:37:57</LastUpdateTime><!--文件最后修改时间--><Version>1</Version><!--文件版本号--></object><!--文件节点--></ClientVersion><!--总版本号-->
</versionInfo>

1.3 XML处理代码:

新建一个处理XML文件的类,服务端和客户端通用,主要是一些XML的增删改查功能。

# 处理xml的类
class VersionInfoXml():def __init__(self, xml_path, server_info=None, module_list=None):self.xml_path = xml_pathif server_info is not None:if module_list is None:module_list = ["ClientVersion"]self.create_new_xml(server_info, module_list)self.tree = ET.parse(self.xml_path)self.root = self.tree.getroot()def create_new_xml(self, server_info, module_info):root = ET.Element("versionInfo")ServerInfo = ET.SubElement(root, "ServerInfo")ET.SubElement(ServerInfo, "ServerIp").text = server_info[0]ET.SubElement(ServerInfo, "ServerPort").text = server_info[1]ET.SubElement(ServerInfo, "XmlLocalPath").text = server_info[2]for each_module in module_info:ET.SubElement(root, each_module).set("Version", "0")self.save_change(root)print("I created a new temp xml!")def save_change(self, root=None):if root is None:root = self.rootrough_bytes = ET.tostring(root, "utf-8")rough_string = str(rough_bytes, encoding="utf-8").replace("\n", "").replace("\t", "").replace("    ", "")content = minidom.parseString(rough_string)with open(self.xml_path, 'w+') as fs:content.writexml(fs, indent="", addindent="\t", newl="\n", encoding="utf-8")return Truedef changeServerInfo(self, name, value):if type(value) is int:value = str(value)Xpath = "ServerInfo/%s" % nameelement = self.root.find(Xpath)if element is not None:element.text = value# self.save_change()else:print("I can't find \"ServerInfo/%s\" in xml!" % name)def addObject(self, module_name, file_path, file_size, last_update_time, version):moduleVersion = self.root.find(module_name)object = ET.SubElement(moduleVersion, "object")ET.SubElement(object, "FileRelativePath").text = str(file_path)ET.SubElement(object, "FileSize").text = str(file_size)ET.SubElement(object, "LastUpdateTime").text = str(last_update_time)ET.SubElement(object, "Version").text = str(version)# self.save_change()def deleteObject(self, module_name, file_name):Xpath = "%s/object" % module_nameobjects = self.root.findall(Xpath)moudleVersion = self.root.find(module_name)for element in objects:if element.find('FileRelativePath').text == file_name:moudleVersion.remove(element)# self.save_change()print("Delete object: %s" % file_name)breakelse:print("I can't find \"%s\" in xml!" % file_name)def updateObject(self, module_name, file_name, version):if type(version) is int:version = str(version)Xpath = "%s/object" % module_nameobjects = self.root.findall(Xpath)for element in objects:if element.find('FileRelativePath').text == file_name:element.find('Version').text = version# self.save_change()# print("Update \"%s\" version: %s" % (file_name, version))breakelse:print("I can't find \"%s\" in xml!" % file_name)def updateAttribute(self, module_name, version):if type(version) is int:version = str(version)moduleVersion = self.root.find(module_name)moduleVersion.set("Version", version)# self.save_change()def getObjects(self, module_name):list_element = []Xpath = "%s/object" % module_nameobjects = self.root.findall(Xpath)for element in objects:dict_element = {}for key, value in enumerate(element):dict_element[value.tag] = value.textlist_element.append(dict_element)return list_elementdef addModule(self, module):self.root.append(module)# self.save_change()def deleteModule(self, module_name):module = self.root.find(module_name)if module is not None:self.root.remove(module)# self.save_change()def getModules(self):dict_element = {}objects = self.root.getchildren()for key, value in enumerate(objects):dict_element[value.tag] = value.attrib.get("Version")del dict_element["ServerInfo"]return dict_elementdef getAttribute(self, module_name):moduleVersion = self.root.find(module_name)return moduleVersion.get("Version")def get_node_value(self, path):'''查找某个路径匹配的第一个节点tree: xml树path: 节点路径'''node = self.tree.find(path)if node == None:return Nonereturn node.text

2. 服务端代码设计

源码文件太长,这里只贴出主要的两个方法,具体实现源码文件放在文末下载。

首先是根扫描所有文件,生成一个最新xml配置文件,然后再比较两个xml,分析出增删改。

# -*- coding: utf-8 -*-
# @Time    : 2019/4/25 20:16
# @Author  : yushuaige
# @File    : AutoCheckVersion.py
# @Software: PyCharm
# @Function: 实现客户端自动更新(服务端)# 处理xml的类
class VersionInfoXml():pass # 同上面xml类def AutoCheckVersion(old_xml_path, new_xml_path):'''比较两个xml的objects节点,分析出增加,更改,和删除的文件列表,并在新xml里更新版本号:param old_xml: 旧xml的完整路径:param new_xml: 新xml的完整路径:return: len(add_list), len(delete_list), len(change_list),:return: add_list: [filname1, filname2], delete_list: [filname1, filname2] change_list: [filname1, filname2]'''print("Analyze the xml files and update the version number ...")old_xml = VersionInfoXml(old_xml_path)new_xml = VersionInfoXml(new_xml_path)# 先分析模块的增、删、改old_modules = list(old_xml.getModules().keys())new_modules = list(new_xml.getModules().keys())add_modules_list = list(set(new_modules).difference(set(old_modules)))for module_name in add_modules_list:ET.SubElement(old_xml.root, module_name).set("Version", 0)common_modules_list = [item for item in old_modules if item in new_modules]# 分析每个的模块中的每个文件的增、删、改total_add_list = []total_delete_list = []total_change_list = []common_modules_list.extend(add_modules_list)for module_name in common_modules_list:old_xml_objects = old_xml.getObjects(module_name)new_xml_objects = new_xml.getObjects(module_name)old_xml_objects_dict = {file_info["FileRelativePath"]: file_info for file_info in old_xml_objects}new_xml_objects_dict = {file_info["FileRelativePath"]: file_info for file_info in new_xml_objects}old_data_list = set(old_xml_objects_dict.keys())new_data_list = set(new_xml_objects_dict.keys())add_list = list(new_data_list.difference(old_data_list))delete_list = list(old_data_list.difference(new_data_list))common_list = list(old_data_list.intersection(new_data_list))change_list = []# 更新每个文件的版本号信息for file_name in common_list:new_version = int(old_xml_objects_dict[file_name]["Version"])update = TimeFormatComp(new_xml_objects_dict[file_name]["LastUpdateTime"],old_xml_objects_dict[file_name]["LastUpdateTime"])if update is True:change_list.append(file_name)new_version += 1new_xml.updateObject(module_name, file_name, new_version)# 更新模块版本信息new_module_version = int(old_xml.getAttribute(module_name))if len(add_list) or len(delete_list) or len(change_list):new_module_version = new_module_version + 1new_xml.updateAttribute(module_name, new_module_version)total_add_list.extend(add_list)total_delete_list.extend(delete_list)total_change_list.extend(change_list)# 保存到文件new_xml.save_change()print("Analysis update info done. Save the new xml ...")# 结果提示if len(total_add_list) or len(total_delete_list) or len(total_change_list):# 替换旧的xml文件os.remove(old_xml_path)os.rename(new_xml_path, old_xml_path)print("Done. add: %d, delete: %d, update: %d. The new client version: %s." % (len(total_add_list), len(total_delete_list), len(total_change_list), str(new_xml.getModules())))else:os.remove(new_xml_path)print("No file changed! The current client version: %s." % (str(new_xml.getModules())))return len(total_add_list), len(total_delete_list), len(total_change_list)def CreateNewXmlFromFiles(client_dir):'''遍历文件夹所有文件,生成标准xml:param client_dir: 要遍历的文件夹路径:return: 生成的xml的完整路径'''print("Scan the folder and create the temp xml file ...")config_parser = configparser.ConfigParser()config_parser.read(os.path.dirname(sys.path[0]) + '\\cfg.ini')UPDATE_HOST = config_parser.get("mqtt", 'serv')server_info = [UPDATE_HOST, "8888", "dev_manage_win"]module_list = os.listdir(client_dir)new_xml = VersionInfoXml("VersionInfoTemp.xml", server_info, module_list)for module_name in module_list:module_dir = os.path.join(client_dir, module_name)for (dirpath, dirnames, filenames) in os.walk(module_dir):for file in filenames:file_dir = os.path.join(dirpath, file)file_path = file_dir.replace(client_dir, "").strip("\\").replace("\\", "/")file_size = os.path.getsize(file_dir)last_update_time = TimeStampFormat(os.path.getmtime(file_dir))version = 1new_xml.addObject(module_name, file_path, file_size, last_update_time, version)new_xml.save_change()new_xml_path = os.path.join(sys.path[0], "VersionInfoTemp.xml")return new_xml_path

二、客户端

1. XML配置文件

为了简便,客户端和服务端处理xml文件的类用同一个。

2. 客户端代码设计

源码文件太长,这里只贴出主要的两个方法,具体实现源码文件放在文末下载。

下载最新xml配置文件和本地配置文件进行比较,然后分析出增删改,进行下载和删除。

# -*- coding: utf-8 -*-
# @Time    : 2019/4/25 20:16
# @Author  : yushuaige
# @File    : AutoUpdate.py
# @Software: PyCharm
# @Function: 实现客户端自动更新(客户端)# 处理xml的类
class VersionInfoXml:pass # 同上面xml类# 手动更新时,检查更新
def CheckUpdate(server_ip, server_port, module_name, order):pass# 主要函数
def AutoUpdate(server_ip, server_port, module_name, order):time_start = time.perf_counter()try:download_url = "http://{0}:{1}/{2}".format(server_ip, server_port, "VersionInfo.xml")local_path = os.path.join(sys.path[0], "VersionInfoTemp.xml")print("download_url: " + download_url)if not download_file_by_http(download_url, local_path):raise Exception()except Exception as e:# tkinter.messagebox.showerror("更新无法继续", "获取最新版本列表文件出现异常!")print("Update error: Can't get the latest VersionInfo xml!")# root.destroy()return Falseroot.update()root.deiconify()# 比较文件变化add_dict, delete_list = analyze_update_info(local_xml_path, update_xml_path, module_name)if add_dict == {} and delete_list == []:os.remove(update_xml_path)# tkinter.messagebox.showinfo("更新无法继续", "当前客户端已经是最新版本!")print("No file changed!")return False# 下载需要更新的文件download_progress(add_dict)# 文件覆盖到主目录prompt_info11.set("正在解压...")prompt_info13.set("总体进度:99.9%")prompt_info21.set("")root.update()source_dir = os.path.join(sys.path[0], "TempFolder")dest_dir = os.path.dirname(sys.path[0])# dest_dir = os.path.join(sys.path[0], "test_main")override_dir(source_dir, dest_dir)# 删除要删除的文件for file in delete_list:delete_dir(os.path.join(dest_dir, file))# 更新xml文件if module_name == "all_module":os.remove(local_xml_path)os.rename(update_xml_path, local_xml_path)else:update_xml(local_xml_path, update_xml_path, module_name)# 客户端更新结束time_end = time.perf_counter()print("更新耗时:%ds" % (time_end - time_start))prompt_info11.set("更新完毕。")prompt_info13.set("总体进度:100.0%")root.update()# tkinter.messagebox.showinfo("更新完成", "更新完毕,耗时:%ds" % (time_end - time_start))return True# 分析两个xml文件
def analyze_update_info(local_xml, update_xml, module_name):'''分析本地xml文件和最新xml文件获得增加的文件和要删除的文件:param local_xml: 本地xml文件路径:param update_xml: 下载的最新xml文件路径:return: download_info: {filename1: fizesize1, filename2: fizesize2}, delete_list: [filname1, filname2]'''print("Analyze the xml files and check the version number ...")old_xml = VersionInfoXml(local_xml)new_xml = VersionInfoXml(update_xml)module_names = []if module_name == "all_module":module_names = new_xml.getModules()else:module_names.append(module_name)download_info_total = {}delete_list_total = []for module_name in module_names:if old_xml.getAttribute(module_name) is None:ET.SubElement(old_xml.root, module_name).set("Version", "0")if new_xml.getAttribute(module_name) <= old_xml.getAttribute(module_name):continueold_xml_objects = old_xml.getObjects(module_name)new_xml_objects = new_xml.getObjects(module_name)old_xml_objects_dict = {file_info["FileRelativePath"]: file_info for file_info in old_xml_objects}new_xml_objects_dict = {file_info["FileRelativePath"]: file_info for file_info in new_xml_objects}old_data_list = set(old_xml_objects_dict.keys())new_data_list = set(new_xml_objects_dict.keys())add_list = list(new_data_list.difference(old_data_list))delete_list = list(old_data_list.difference(new_data_list))common_list = list(old_data_list.intersection(new_data_list))download_info = {file_name: new_xml_objects_dict[file_name]["FileSize"] for file_name in add_list}# 根据每个文件的版本号,确定是否需要更新for file_name in common_list:if int(new_xml_objects_dict[file_name]["Version"]) > int(old_xml_objects_dict[file_name]["Version"]):download_info.update({file_name: new_xml_objects_dict[file_name]["FileSize"]})download_info_total.update(download_info)delete_list_total.extend(delete_list)# return download_info, delete_listreturn download_info_total, delete_list_total

三、运行效果

1. 程序目录结构

1.1 服务端

ClientFolder目录用来存放要更新的文件夹,

venv是python目录,

cfg.ini文件用来配置ip、端口等信息,

server.py是主程序,

start.bat用来双击启动server.py,

VersionInfo.xml是存放文件信息的xml

1.2 客户端

TempFolder目录用来存放下载下来的文件,

venv是python目录,

client.py是主程序,

start.bat用来双击启动server.py,

VersionInfo.xml是存放文件信息的xml,

VersionInfoTemp.xml是更新时自动生成的,是下载的最新配置文件

2. 服务端运行效果

默认使用本地测试ip 127.0.0.1,默认端口8888

3. 客户端运行效果

上面窗口是控制台窗口,显示运行过程的日志,下面是更新界面。

如果不想显示控制台界面,只需要把start.bat里前三行的注释打开即可。

文件太小可能会一闪而过,因为程序默认更新完立即退出。

四、改进思路

1.多线程提高效率

因为没有测试过文件数量和大小非常大的情况,现在程序的所有步骤都是单线程执行,可以将文件扫描和下载等耗时间的步骤,改进成多线程或者协程同时运行,提高程序的运行效率。

2.文件扫描方式

当前只根据文件相对路径加文件全名的方式,进行文件区分,然后根据最后修改时间来判断是否需要更新,可以增加MD5校验来保证文件的唯一性。

3.界面完善

当前只有在下载文件时有界面提示,可以改进界面,使整个更新过程可视化。

4.启动方式

当前使用bat脚本调命令行的方式启动程序,会有一个黑色窗口,可以将程序打包成exe文件发布。

五、文件下载

零积分下载整个程序源码:用Python实现一个软件自动升级系统

用Python实现一个软件自动升级系统相关推荐

  1. 软件自动升级系统:支持exe程序覆盖更新、目录结构、更新更新程序自身、更新sql、执行bat批处理...

    软件自动升级系统说明 目录 一.         自动升级需要需要实现些什么?... 1 二.         服务器安装... 2 三.         客户端配置... 4 四.         ...

  2. 利用Web Services实现软件自动升级

    摘 要:软件维护升级工作是软件生命周期最重要的环节.为了解决以往C/S(Client/Server)模式下的客户端软件升级效率低的问题,设计了C/S应用系统自动升级处理程序.该程序利用Web Serv ...

  3. C# 实现在线软件自动升级自动下载更新文件

    1 前言 长期以来,广大程序员为到底是使用Client/Server,还是使用Browser/Server结构争论不休,在这些争论当中,C /S结构的程序可维护性差,布置困难,升级不方便,维护成本高就 ...

  4. [转载]利用Web Services实现软件自动升级

    [---  资料是从免费网站上获取的,上载在这里,只为交流学习目的,文章原作者保留所有权力, 如本博客的内容侵犯了你的权益,请与以下地址联系,本人获知后,马上删除.同时本人深表歉意,并致以崇高的谢意! ...

  5. HTML5进阶(二)HBuilder实现软件自动升级

    HBuilder实现软件自动升级 前言 移动APP开发好后需要实现软件自动升级功能,经过一番搜索,发现HBuilder具有"App资源在线升级更新"的功能,遂研究之. 经过一番测试 ...

  6. WPF开发一款软件自动升级组件

    前几天介绍了WPF进行自定义窗口的开发,关注的朋友还挺多,超出本人意料,呵呵,那么我就再接再励,在上篇的基础上,讲述一下软件自动升级组件的开发原理,大家时间宝贵,不想搞太长的篇幅,所以尽可能拣重要的说 ...

  7. 可操作性强!Python实现一个电影订票系统!

    来源丨Python小二 一.效果展示 通过Python实现一个电影订票系统,效果如下所示: 二.整体结构图 三.代码分解 3.1 infos.py 一部电影的详细信息适合用 字典 结构来存储,我们可以 ...

  8. python写一个系统-使用Python写一个量化股票提醒系统

    大家在没有阅读本文之前先看下python的基本概念, Python是一种解释型.面向对象.动态数据类型的高级程序设计语言. Python由Guido van Rossum于1989年底发明,第一个公开 ...

  9. 用python读取股票价格_使用Python写一个量化股票提醒系统

    大家在没有阅读本文之前先看下python的基本概念, Python是一种解释型.面向对象.动态数据类型的高级程序设计语言. Python由Guido van Rossum于1989年底发明,第一个公开 ...

  10. python量化股票源码_使用Python写一个量化股票提醒系统

    大家在没有阅读本文之前先看下python的基本概念, Python是一种解释型.面向对象.动态数据类型的高级程序设计语言. Python由Guido van Rossum于1989年底发明,第一个公开 ...

最新文章

  1. AXI DMA DRIVER 阶段性 kernel driver 构建并测试(三 )
  2. 零基础自学python的建议-如何从零基础自学Python?
  3. ECMAScript5 Array新增方法
  4. 图论--LCA--Tarjan(离线)
  5. 表达能力VS只会敲代码(2018届毕业生web前端)
  6. 操作系统(李治军) L9多进程图像-操作系统最重要的图像
  7. 关于“想哭”病毒,我也来两句。--转载
  8. 20201221:力扣220场周赛题解
  9. ajax在php中使用方法,在项目中如何使用ajax请求
  10. 设计模式(三)结构型模式
  11. 数据结构——课程设计
  12. 数据结构——队列(银行叫号系统)
  13. 谢晶:webpower中国区正在向“多渠道智能化营销”全面转型
  14. 计算机网络谢希仁第七版 第五章 答案
  15. 利器 | REST Assured 实践(二):断言实现
  16. java.io.IOException: Server returned HTTP response code: 503 for UR
  17. Word2vec And Doc2vec - 文本向量化
  18. 文件上传流程和协议规范,简,全
  19. 企业ERP管理系统原型
  20. Dell服务器启动显示Entering System Servvice To cancel.

热门文章

  1. 修改VNR源码接入新版有道中文翻译API
  2. 行业案例 | 数据分析在银行业应用之欺诈检测
  3. opencv4nodejs安装
  4. ENVI学习总结(十)——遥感图像监督分类
  5. RHEL7CentOS7 精简操作指令
  6. NLP之文本特征提取详解
  7. “Matter时代”行业再显梯队化,多强竞逐第一序列
  8. Linux 优秀软件资源大全中文版
  9. base循环解码工具
  10. matlab 波形仿真,MATLAB仿真波形的处理