tags:[pyqt, 文件管理器, 文件拖拽, 窗口拉伸]
类文件管理器
--
|--- activity # 活动窗口
|--- frame # 独立控件
|--- config # 配置信息
|--- utils # 工具包
|--- deorator # 装饰器
|--- resource # 静态资源
-main.py
-requirement.txt
以下秩序按照包相干程度排序。
config包中保存在部署中可能发生改变的基本配置,如数据库、保存路径、常用字符串及其他服务的命令参数等。
以下仅是示例文件,并不代表实际生产文件
import os
dir_path = os.path.abspath(os.path.dirname(__file__))
ROOT_PATH = os.path.abspath(os.path.join(dir_path, os.path.pardir))
DOC_PATH = os.path.abspath(os.path.join(ROOT_PATH, "doc"))
# 存放解压后的chm文件
temp_path = "temp"
# 存放chm文件
chm_path = "document"
# 存放高亮的临时文件
html_height_line = "html"
# crate dir
def check(path):
if not os.path.exists(path):
os.makedirs(path)
return path
# document path
def get_chm_path():
return check(os.path.join(ROOT_PATH, "chm"))
此文件保存了项目用到的基本路径。,可以让我在代码中多次使用,如在后期修改时,可以让我在其他代码完整无需改动的基础下快速完成需求。
如果项目中,存在大量的配置文件,可新建多个不同类别的配置文件。
这个包保存常用的装饰器,如lazy,也可对数据库进行log封装
lazy 加载,有关lazy可参考其他博客说明,下例文件使用的是Django2.3中的lazy源码
from functools import total_ordering, wraps
# author by Django
class Promise:
"""
在惰性函数的闭包中创建的代理类的基类。
它用于识别代码中的承诺。
"""
pass
def __cast(self):
pass
def _lazy_proxy_unpickle(func, args, kwargs, *result_classes):
return lazy(func, *result_classes)(*args, **kwargs)
def lazy(func, *result_classes):
"""
将任何可调用项转换为延迟加载的可调用项。结果类或类型
是必需的——至少需要一个,以便自动强制
延迟计算代码被触发。如果结果不存在则初始化
"""
@total_ordering
class Proxy(Promise):
"""
封装函数调用并充当方法的代理
调用该函数的结果。函数没有求值
直到结果上的一个方法被调用。
"""
__prepared = False
def __init__(self, args, kw):
self.__args = args
self.__kw = kw
if not self.__prepared:
self.__prepare_class__()
self.__prepared = True
def __reduce__(self):
return (
_lazy_proxy_unpickle,
(func, self.__args, self.__kw) + result_classes
)
def __repr__(self):
return repr(self.__cast())
@classmethod
def __prepare_class__(cls):
for resultclass in result_classes:
for type_ in resultclass.mro():
for method_name in type_.__dict__:
# 所有 __promise__ 返回相同的包装器方法
if hasattr(cls, method_name):
continue
math = cls.__promise__(method_name)
setattr(cls, method_name, math)
cls._delegate_bytes = bytes in result_classes
cls._delegate_text = str in result_classes
assert not (cls._delegate_bytes and cls._delegate_text), (
"不能同时使用字节和文本返回类型调用lazy()")
if cls._delegate_text:
cls.__str__ = cls.__text_cast
elif cls._delegate_bytes:
cls.__bytes__ = cls.__bytes_cast
@classmethod
def __promise__(cls, method_name):
def __wrapper(self, *args, **kw):
res = func(*self.__args, **self.__kw)
return getattr(res, method_name)(*args, **kw)
return __wrapper
def __text_cast(self):
return func(*self.__args, **self.__kw)
def __bytes_cast(self):
return bytes(func(*self.__args, **self.__kw))
def __bytes_cast_encoded(self):
return func(*self.__args, **self.__kw).encode()
def __cast(self):
if self._delegate_bytes:
return self.__bytes_cast()
elif self._delegate_text:
return self.__text_cast()
else:
return func(*self.__args, **self.__kw)
def __str__(self):
# object defines __str__(), so __prepare_class__() won't overload
# a __str__() method from the proxied class.
return str(self.__cast())
def __eq__(self, other):
if isinstance(other, Promise):
other = other.__cast()
return self.__cast() == other
def __lt__(self, other):
if isinstance(other, Promise):
other = other.__cast()
return self.__cast() < other
def __hash__(self):
return hash(self.__cast())
def __mod__(self, rhs):
if self._delegate_text:
return str(self) % rhs
return self.__cast() % rhs
def __deepcopy__(self, memo):
# 该类的实例实际上是不可变的。它只是一个函数集合。
memo[id(self)] = self
return self
@wraps(func)
def __wrapper__(*args, **kw):
# 创建代理对象
return Proxy(args, kw)
return __wrapper__
class LazyProperty(object):
"""
lazy 加载装饰器
此方法不允许被嵌套使用:
@LazyProperty
def test_function():
do something
"""
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
if instance is None:
return self
value = self.fun(instance)
setattr(instance, self.fun.__name__, value)
return value
工具包中保存常用的工具类如:字符串处理,文件处理,线程操作及日志等。
python logging 日志包并不能直接满足实际需求,所以需要对logger进行重构(示例文件中,包含setting中日志的保存路径setting.get_log_path(), 除此之外可独立运行)
# coding=utf-8
"""
create by pymu on 2020/4/29
package: .logger.py
project: status_document_0.1
"""
import datetime
import logging
import os
import re
from config import setting
description = "日志信息"
LOG_PATH = setting.get_log_path()
class Logger(logging.Logger):
"""
"""
def __init__(self, name='system', level=logging.INFO):
super().__init__(name, level)
self.level = level
self.name = name
self.__set_log_handler()
def __set_log_handler(self):
"""
日志输出格式及输出等级,默认为INFO
:return:
"""
main_handler = MyLoggerHandler(filename=self.name, when='D', backup_count=5,
encoding="utf-8")
warn_handler = MyLoggerHandler(filename='警告日志', when='D',
backup_count=5, encoding="utf-8")
error_handler = MyLoggerHandler(filename='异常日志', when='D',
backup_count=35, encoding="utf-8")
# 设置日志格式
formatter = logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
_formatter = logging.Formatter("\n%(asctime)s - %(levelname)s: %(message)s")
bug_filter = logging.Filter()
bug_filter.filter = lambda record: record.levelno == logging.ERROR # 设置过滤等级
error_handler.addFilter(bug_filter)
error_handler.setFormatter(_formatter)
self.addHandler(error_handler)
bug_filter = logging.Filter()
bug_filter.filter = lambda record: record.levelno == logging.WARNING # 设置过滤等级
warn_handler.addFilter(bug_filter)
warn_handler.setFormatter(_formatter)
self.addHandler(warn_handler)
bug_filter = logging.Filter()
bug_filter.filter = lambda record: record.levelno < logging.WARNING # 设置过滤等级
main_handler.addFilter(bug_filter)
main_handler.addFilter(bug_filter)
main_handler.setFormatter(formatter)
self.main_handler = main_handler
self.addHandler(main_handler)
def reset_name(self, name):
"""
重新设置日志文件名
:param name:
:return:
"""
self.name = name
self.removeHandler(self.main_handler)
self.__set_log_handler()
try:
import codecs
except ImportError:
codecs = None
class MyLoggerHandler(logging.FileHandler):
def __init__(self, filename, when='M', backup_count=15, encoding=None, delay=False):
self.prefix = os.path.join(LOG_PATH, '{name}'.format(name=filename))
self.filename = filename
self.when = when.upper()
# S - Every second a new file
# M - Every minute a new file
# H - Every hour a new file
# D - Every day a new file
if self.when == 'S':
self.suffix = "%Y-%m-%d_%H-%M-%S"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$"
elif self.when == 'M':
self.suffix = "%Y-%m-%d_%H-%M"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}\.log$"
elif self.when == 'H':
self.suffix = "%Y-%m-%d_%H"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}\.log$"
elif self.when == 'D':
self.suffix = "%Y-%m-%d"
self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
else:
raise ValueError("Invalid rollover interval specified: %s" % self.when)
self.filePath = "%s%s.log" % (self.prefix, datetime.datetime.now().strftime(self.suffix))
try:
if os.path.exists(LOG_PATH) is False:
os.makedirs(LOG_PATH)
except Exception as e:
print("can not make dirs")
print("filepath is " + self.filePath)
print(e)
self.backupCount = backup_count
if codecs is None:
encoding = None
logging.FileHandler.__init__(self, self.filePath, 'a', encoding, delay)
def write_log(self):
_filePath = "%s%s.log" % (self.prefix, datetime.datetime.now().strftime(self.suffix))
if _filePath != self.filePath:
self.filePath = _filePath
return 1
return 0
def change_file(self):
self.baseFilename = os.path.abspath(self.filePath)
if self.stream is not None:
self.stream.flush()
self.stream.close()
if not self.delay:
self.stream = self._open()
if self.backupCount > 0:
for s in self.delete_old_log():
os.remove(s)
def delete_old_log(self):
dir_name, base_name = os.path.split(self.baseFilename)
file_names = os.listdir(dir_name)
result = []
p_len = len(self.filename)
for fileName in file_names:
if fileName[:p_len] == self.filename:
suffix = fileName[p_len:]
if re.compile(self.extMatch).match(suffix):
result.append(os.path.join(dir_name, fileName))
result.sort()
if len(result) < self.backupCount:
result = []
else:
result = result[:len(result) - self.backupCount]
return result
def emit(self, record):
"""
Emit a record.
"""
# noinspection PyBroadException
try:
if self.write_log():
self.change_file()
logging.FileHandler.emit(self, record)
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
if __name__ == "__main__":
# to do something
log = Logger('info')
import time
for i in range(12):
time.sleep(1)
log.info("测试" + str(i))
值得一提:日志可以在窗口基类中声明,如此一来便可在子类中调用,至于具体的日志声明由提交日志方法标注,或者由日志方法中的提取方法名。
在qt中常有使用线程来异步更新数据,或者执行操作。为保证不拥塞UI线程的基础下,需要Qthread来运行耗时方法,如果直接使用thread方法更新界面,会警告被没有信号的线程更新UI。
#!/usr/bin/python3
# coding=utf-8
"""---------------------------------------
project :实用法规
包名/文件 :.thread.py
版本 :v1.0
创建 :pymu on 2020/4/22
详情 :
---------------------------------------"""
from PyQt5.QtCore import QThread, pyqtSignal
from utils.logger import Logger
class FuncThread(QThread):
"""
执行返回线程
"""
# 正常通信信号
thread_signal = pyqtSignal(dict)
# 异常通信信号
thread_error_signal = pyqtSignal(str)
def __init__(self):
super().__init__()
self.func = None
self.func_args = []
self.func_kwargs = {}
self.logger = Logger()
def set_func(self, func, *args, **kwargs):
"""
设置执行线程, 新建以一个通信对象之后,需要调用此方法设置执行方法之后才能开启
通信执行线程
:param func: 设置执行方法
:param args: 设置执行参数
:param kwargs: 设置执行参数(键值对)
:return: None
"""
self.func = func
self.func_args = args
self.func_kwargs = kwargs
self.logger.info("设置线程 func={} arg={} kwargs={}".format(func.__name__, args, kwargs))
def run(self):
"""
执行线程:
在 start() 后启动
其中有返回的结果,及执行异常时的通信
"""
if not self.func:
return
self.logger.info("执行 func={} arg={} kwargs={}".format(self.func.__name__, self.func_args, self.func_kwargs))
try:
result = self.func(*self.func_args, **self.func_kwargs)
if not isinstance(result, dict):
result = {}
self.thread_signal.emit(result)
except Exception as e:
self.thread_error_signal.emit(str(e))
self.logger.error("执行失败 func={} arg={} kwargs={}, error={}".format(self.func.__name__,
self.func_args,
self.func_kwargs,
e))
注:方法中可以添加:除执行成功信号、异常信号外,还可以添加多个信号,但是需要在执行方法中声明提交信号(观察者模式),具体使用方法后续会实例说明。
#更新线程
self.update_thread = FuncThread()
self.update_thread.thread_signal.connect(self.set_tree_data)
self.update_thread.set_func(self.get_dir_dict, self.activity_path, _)
self.update_thread.start()
静态资源加载器,在qt中通常使用了大量的图标文件、字体声明。(因为不知道qt是不是会保存在内存中,就自己加了一个)结合lazy加载可以快速访问(对于复杂布局)。
#!/usr/bin/python3
# coding=utf-8
"""---------------------------------------
project :实用法规
包名/文件 :.resourceLoad.py
版本 :v1.0
创建 :pymu on 2020/4/21
详情 : 静态资源加载器
---------------------------------------"""
import os
import threading
import qtawesome
from PyQt5 import QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QBrush, QColor
from config import setting
from utils.lazy_property import LazyProperty
class Resource:
"""
图像
"""
_instance_lock = threading.Lock()
def __init__(self):
self.img_path = setting.get_img_path()
@classmethod
def instance(cls):
with Resource._instance_lock:
if not hasattr(Resource, "_instance"):
Resource._instance = Resource()
return Resource._instance
def render_icon(self, name):
"""
渲染
:return:
"""
path = os.path.join(self.img_path, name)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(path), QtGui.QIcon.Normal, QtGui.QIcon.On)
return icon
@LazyProperty
def QT_img_document(self):
"""
文档 图标
:return:
"""
return self.render_icon("icon_tree_document.png")
@LazyProperty
def QT_img_dir(self):
"""
文件夹 图标
:return:
"""
return self.render_icon("icon_tree_file.png")
@LazyProperty
def QT_img_doc(self):
"""
doc 图标
:return:
"""
return self.render_icon("icon-word.png")
@LazyProperty
def QT_img_excel(self):
"""
Excel 图标
:return:
"""
return self.render_icon("icon-excel.png")
@LazyProperty
def QT_img_pdf(self):
"""
PDF 图标
:return:
"""
return self.render_icon("icon-pdf.png")
@LazyProperty
def QT_img_template_logo(self):
"""
文书模板logo 图标
:return:
"""
return self.render_icon("logo-document.png")
@LazyProperty
def QT_img_new_folder(self):
"""
新建文件夹 图标
:return:
"""
return self.render_icon("icon-add.png")
@LazyProperty
def QT_img_delete(self):
"""
删除 图标
:return:
"""
return self.render_icon("icon-delete.png")
@LazyProperty
def QT_img_upload(self):
"""
上传 图标
:return:
"""
return self.render_icon("icon-up.png")
@LazyProperty
def QT_img_download(self):
"""
下载 图标
:return:
"""
return self.render_icon("icon-down.png")
@LazyProperty
def QT_img_search(self):
"""
搜索 图标
:return:
"""
return self.render_icon("icon_search.png")
@LazyProperty
def QT_img_home_logo(self):
"""
目录 图标
:return:
"""
return self.render_icon("icon-catalogue.png")
@LazyProperty
def QT_img_clear(self):
"""
清除 图标
:return:
"""
return self.render_icon("icon_input_clear.png")
@LazyProperty
def QT_img_unknown(self):
"""
未知文件 图标
:return:
"""
return self.render_icon("unknown.png")
@LazyProperty
def QT_img_none(self):
"""
无图片
:return:
"""
return self.render_icon("??")
@LazyProperty
def QT_img_input_user(self):
"""
输入账号
:return:
"""
# return self.render_icon("icon_login_user.png")
return qtawesome.icon('fa.user', color="#666")
@LazyProperty
def QT_img_input_pass(self):
"""
输入密码
:return:
"""
# return self.render_icon("icon_login_key.png")
return qtawesome.icon('fa.unlock-alt', color="#666")
@LazyProperty
def QT_img_user_more(self):
"""
下拉
:return:
"""
return qtawesome.icon('fa.angle-down', color="white")
@LazyProperty
def QT_font_16px_song(self):
"""
14 px
宋体
:return:
"""
font = QFont()
font.setPixelSize(16)
font.setFamily("宋体")
return font
@LazyProperty
def QT_brush_font_color_333(self):
"""
字体颜色样式 1
:return:
"""
brush = QBrush(QColor(51, 51, 51, 255))
brush.setStyle(Qt.NoBrush)
return brush
这个保存了我自己项目中最烦躁的部分,冗余又拖沓的部分,堪堪可用。
#!/usr/bin/python3
# coding=utf-8
"""---------------------------------------
project :实用法规
包名/文件 :.fileutils.py
版本 :v1.0
创建 :pymu on 2020/4/13
详情 : 文件处理工具
---------------------------------------"""
import base64
import json
import os
import shutil
import time
from lxml import etree
from config import setting
def get_file_name(path):
file_name = os.path.basename(path)
if not file_name:
return
else:
return file_name.split(".")[0]
def unchm(path):
"""
解压chm文件到指定的文件目录
:param path:
:return:
"""
file_name = get_file_name(path)
if not file_name:
return
temp_path = os.path.join(setting.get_temp_path(), file_name)
if not os.path.exists(temp_path):
os.makedirs(temp_path)
command = "HH.EXE -decompile {export_path} {source_path}".format(export_path=temp_path, source_path=path)
os.system(command)
def formatTime(temp_time):
"""
'''格式化时间的函数'''
:param temp_time:
:return:
"""
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(temp_time))
def formatByte(number):
"""
格式化内存大小
:param number:
:return:
"""
if number < 1024:
return "小于1字节"
elif number <= 1024 * 1024:
return "%.2f KB" % (number / 1024.0)
elif number <= (1024 * 1024 * 1024):
return "%.2f MB" % (number / 1024.0 / 1024.0)
elif number <= (1024 * 1024 * 1024 * 1024):
return "%.2f GB" % (number / 1024.0 / 1024.0 / 1024.0)
else:
return "未知文件大小"
def get_doc_dict(path, match_html=True):
"""
输入一个路径,返回一个目录结构
:return:
"""
result = dict()
if not os.path.exists(path):
return result
for i in os.listdir(path):
file = os.path.join(path, i)
if os.path.isdir(file):
s = get_doc_dict(file)
if s:
result.update({i: s})
else:
f_type = i.split(".")
if match_html:
if len(f_type) > 0 and "html" in f_type[-1] or "htm" in f_type[-1]:
result.update({i: file})
else:
result.update({i: file})
return None if len(result.keys()) == 0 else result
def get_doc_dict_new(path):
"""
获取一个路径的目录结构
:param path:
:return:
"""
result = dict()
if not os.path.exists(path):
return result
for i in os.listdir(path):
file = os.path.join(path, i)
if os.path.isdir(file):
s = get_doc_dict_new(file)
result.update({i: s})
else:
result.update({i: file})
return result
def get_dir_dict(path):
"""
新启方法,仅返回目录不返回文件
:param path:
:return:
"""
result = dict()
if not os.path.exists(path):
return result
for i in os.listdir(path):
file = os.path.join(path, i)
if os.path.isdir(file):
s = get_dir_dict(file)
result.update({i: s})
return result
def get_path_list(path, root_path):
"""
输入一个路径获取路径下的文件夹及文件的列表
:param root_path: 最顶级的目录,不允许跨目录操作
:param path: 获取path下的结构
:return:
"""
result_dict = dict()
result_dict.update({"root": path})
result = list()
for root, dirs, files in os.walk(path):
for dir_t in dirs:
item = dict()
path = os.path.join(root, dir_t)
item.update({
"last_change_time": formatTime(os.path.getctime(path)),
"path": path,
"size": "",
"name": dir_t,
"type": "dir"
})
result.append(item)
for file in files:
item = dict()
path = os.path.join(root, file)
file_info = os.stat(path)
item.update({"path": os.path.abspath(path),
"size": formatByte(os.path.getsize(path)),
"last_change_time": formatTime(file_info.st_mtime),
"name": file,
"type": os.path.splitext(path)[1]})
result.append(item)
if not result:
result.insert(0, {"path": os.path.abspath(os.path.join(root, os.path.pardir)),
"size": "",
"last_change_time": "",
"name": "空空如也",
"type": "none"})
if not root == root_path:
result.insert(0, {"path": os.path.abspath(os.path.join(root, os.path.pardir)),
"size": "",
"last_change_time": "",
"name": "<<返回上一级",
"type": "back"})
break
result_dict.update({"data": result})
return result_dict
def clean_temp_file(paths: list):
"""
删除临时的解压文件
:param paths:
:return:
"""
for file in os.listdir(setting.get_temp_path()):
file_path = os.path.join(setting.get_temp_path(), file)
if file_path not in paths:
print(file_path)
# noinspection PyBroadException
try:
shutil.rmtree(file_path)
except Exception as e:
print(e)
def check_chm():
"""
检查目录下的chm文件
更新文档时可调用
:return:
"""
file_dir = []
for file in os.listdir(setting.get_chm_path()):
# 如果是文件
file_path = os.path.join(setting.get_chm_path(), file)
if os.path.isfile(file_path):
s = file.split(".")
if len(s) > 1 and str(s[-1]).lower() == "chm":
file_name = get_file_name(file_path)
if not file_name:
return
temp_path = os.path.join(setting.get_temp_path(), file_name)
file_dir.append(temp_path)
if not os.path.exists(temp_path):
unchm(file_path)
clean_temp_file(file_dir)
def match_key(path, key) -> bool:
"""
根据文件路径查找内容
:param path:
:param key:
:return:
"""
# noinspection PyBroadException
try:
with open(path, "rb") as f:
response = etree.HTML(text=f.read())
if key in response.xpath('string(.)'):
return True
return False
except:
return False
def test():
from lxml import etree
with open("temp/css/index.htm", "rb") as f:
response = etree.HTML(text=f.read())
if "其它经验1" in response.xpath('string(.)'):
return True
return False
def make_dir(path, name):
"""
创建文件夹
:param name:
:param path:
:return:
"""
path = os.path.join(path, name)
os.makedirs(path)
def filter_search(result: dict, data, m):
"""
对目录结构进行循环遍历查找文件名字中包含key 的文件list
:param result:
:param data:
:param m:
:return:
"""
if isinstance(data, dict):
# key 表示文件名, value 表示路径
for key in data:
value = data.get(key)
# 如果是文件夹时递归
if isinstance(value, dict):
filter_search(result, value, m)
else:
if m.lower() in key.lower():
item = {"path": value,
"size": "",
"last_change_time": "",
"name": key,
"type": os.path.splitext(str(value))[1]}
result.get("data").append(item)
def delete_any(path):
"""
删除目录及文件
"""
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
def delete_path(path):
"""
根据地址删除文件及问价夹, 异常不用捕获,直接移交给线程管理器
:param path:
:return:
"""
if isinstance(path, list):
for i in path:
delete_any(i)
return
if isinstance(path, str):
delete_any(path)
return
raise Exception("未知的的数据类型{}".format(path))
def check_exists(source, target):
"""
判断文件是否存在
:param source:
:param target:
:return:
"""
result = []
if source:
for i in source:
file_name = os.path.basename(i)
if os.path.exists(os.path.join(target, file_name)):
result.append(file_name)
return result
def copy_file(source, target):
"""
拷贝文件
:param target:
:param source:
:return:
"""
if source:
target = os.path.abspath(target)
for i in source:
target_ = os.path.join(target, os.path.basename(i))
if os.path.isfile(i):
shutil.copyfile(i, target_)
else:
if os.path.exists(target_):
shutil.rmtree(target_)
shutil.copytree(i, target_)
def get_load_user_info():
"""
获取本地保存的信息
:return:
"""
# noinspection PyBroadException
try:
with open(setting.get_user_ini_path(), "rb") as f:
content = f.read()
return json.loads(base64.b64decode(content).decode())
except Exception as e:
print(e)
return dict()
# noinspection PyBroadException
def replace_html(path, key):
"""
替换成高亮之后的文本
:param key:
:param path:
:return:
"""
if not path:
return
path = os.path.join(setting.get_activity_path(), path)
if key:
resource_path = setting.get_resource_path()
html_path = os.path.join(resource_path, "html")
html_path = os.path.join(html_path, "temp.html")
js_path = os.path.join(resource_path, "js")
# jq文件的路径
jq_path = os.path.join(js_path, "jquery-1.12.4.min.js")
# js替换的路径
js_path = os.path.join(js_path, "height_line._js")
# 读取源文件, 不管编码问题
with open(path, "r", errors='ignore') as file:
html_content = file.read()
# 读取 需要替换的js
with open(js_path, "r", errors='ignore') as file:
js_content = file.read()
# 替换文本
if html_content and js_content:
js_content = js_content.replace("{{key}}", key)
js_content = js_content.replace("{{jq_path}}", "file:///" + jq_path.replace("\\", "/"))
new_content = html_content.replace("