正则表达式 测试工具开发文档
作者: 锦沐Python | PASSLINK (各平台账号同名)
版本:Python 3.10.x
运行效果
需求分析
本项目使用python Tk 开发正则表达式测试程序,该程序可实时高亮匹配文本结果,显示详细位置信息,以及提供匹配结果替换功能。用户可选择正则表达式匹配标志位,实现不同的匹配需求。其他功能还包含保存匹配式,py代码片段生成,匹配项位置信息导出,常用匹配式手册。
界面设计
顶部为正则表达式输入框,左侧为菜单按钮,中间上半部分为测试文本框,下半部分为用户手册,替换功能文本框,右侧顶部为提示信息框,底部为详细匹配位置情况。
项目结构
D:.
│ main.py #主函数入口
│ main.spec #打包exe配置文件
│ requirement.txt #依赖包文件
│ __init__.py
├─build #打包exe生成文件,不需要管
├─export_data # 导出文件夹
├─data
│ │ data.csv #示例表达式数据
│ │ file_tool.py #文件处理脚本
│ │ learn_data.csv #学习模式数据
│ │ __init__.py
│ │
│ ├─create_data # 数据处理,不关联项目,仅处理数据
├─dist # 打包结果
│ └─main
│ │ 正则表达式测试器.exe
│ └─_internal
├─doc # 文档
│
├─src # 源码
│ │ config.py # 配置
│ │ main_window_ui.py # UI
│ │ window_app.py # 交互
│ │ __init__.py
│ │
│ ├─assest # 其他素材
│ │ icon.ico
│ │ icon.png
编码规范
模块名(文件名)使用小写字母与下划线组合,语义清晰,例如 main_app.py。
常量名,使用大写、数字、下划线组合, 例如 DATA_PATH。
类名:使用双驼峰命名法,单词开头大写,例如 MainWindowUi。
素材文件命名:小写字母、下划线、数字组合,相关资源前缀一致。
变量名:小写字母、下划线、数字组合,相关变量前缀一致,例如 expression_enty。
编码
配置信息
项目里经常用到许多常量,比如文件路径,常量配置信息等,因此为了便于修改和管理配置,创建一个配置模块。
"""
@FileName:config.py\n
@Description:\n
@Author:锦沐Python\n
@Time:2024/8/19 11:57\n
"""
import re
from pathlib import Path
WIN_NAME = "锦沐Python-正则表达式测试程序"
THEME_STYLE = "cyborg" # "darkly"
WIN_WIDTH = 1000
WIN_HEIGHT = 700
# 打包使用
_current_path = Path(".").resolve() / "_internal"
# _current_path = Path(".").resolve()
print(_current_path)
# 图标
ICON_PATH = _current_path / "src" / "assest" / "icon.ico"
# 模式
ENCODING_MODE = [re.UNICODE, re.ASCII]
ENCODING_MODE_NAME = ["UNICODE", "ASCII"]
MODE = [re.IGNORECASE, re.LOCALE, re.MULTILINE, re.DOTALL, re.VERBOSE]
MODE_NAME = ["IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "VERBOSE"]
# 菜单按钮
MEAU_ITEMS = ["保存匹配式", "匹配式测试", "替换匹配项", "代码生成", "导出匹配项", "常用匹配式", "学习模式"]
# 数据
CSV_DATA_PATH = _current_path / "data" / "data.csv"
CSV_SAVE_PATH = Path(".").resolve() / "export_data"
if not CSV_SAVE_PATH.exists():
CSV_SAVE_PATH.mkdir(parents=True)
CSV_LEARN_PATH = _current_path / "data" / "learn_data.csv"
NOTE_BOOK_PAGE = ["替换", "常用表达式", "学习模式"]
# 表达式标题
ExpressionsDetailTitle = "选项"
ExpressionsDetailCloums = ["简介", "内容"]
# 测试匹配项详情
TestExpressDetailCloum = ["序号", "行列位置", "匹配内容"]
Tk界面UI编写
初始化窗口参数
class MainWindowUi():
"""
主窗口
"""
def __init__(self):
# 创建一个窗口,配置他的基础信息
self.root = tk.Tk()
self.root.geometry(f"{WIN_WIDTH}x{WIN_HEIGHT}+5+5")
self.root.minsize(WIN_WIDTH, WIN_HEIGHT)
self.root.title(WIN_NAME)
# 设置基础参数
self.root.iconbitmap(ICON_PATH)
# 主题样式
Style(THEME_STYLE)
# 变量
# 正则表达式
self.expression_entry_data = tk.StringVar(value="PASSLINK团队,小红书:锦沐python")
# 模式选项
self.select_modes_state = [tk.IntVar(value=0) for _ in range(len(MODE))]
# 替换值
self.replace_entry_data = tk.StringVar(value="")
# 菜单按钮组
self.menu_buttons = []
启动窗口函数
def run_app(self):
"""
启动主循环
"""
self.root.mainloop()
创建容器框架
def create_container(self):
"""
创建容器
"""
# 根容器
self.root_container_frame = ttk.Frame(self.root)
self.root_container_frame.pack(expand=True, fill=tk.BOTH, pady=5, padx=5)
# 顶部输入框
self.regular_expression_frame = ttk.Frame(self.root_container_frame)
self.regular_expression_frame.pack(side=tk.TOP, expand=True, fill=tk.X)
# 文本框容器
self.text_container_frame = ttk.Frame(self.root_container_frame)
self.text_container_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
# 菜单栏
self.menu_frame = ttk.Frame(self.text_container_frame)
self.menu_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 中心文本框
self.center_text_frame = ttk.Frame(self.text_container_frame)
self.center_text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 右侧提示
self.tips_frame = ttk.Frame(self.text_container_frame)
self.tips_frame.pack(side=tk.RIGHT, expand=False, fill=tk.Y)
填充容器
顶部表达式输入框,我们的表达式从这里输入,绑定 self.expression_entry_data (StringVar)变量,该变量的值变化就会重新渲染文本内容,当输入内容变化也会改变 self.expression_entry_data 的值。一种数据双向绑定效果,之后我们可以监听 self.expression_entry_data 变化就可触发一些业务函数改变状态。
def fill_container(self): """ 填充容器 """ # 表达式输入框 self.expression_entry = ttk.Entry(self.regular_expression_frame, justify=tk.CENTER, textvariable=self.expression_entry_data) self.expression_entry.pack(expand=True, fill=tk.X, pady=5, padx=5)
模式选择,先选择 UNICODE 或者 ASCII,然后再选择其他标志位,用
|
运算组合起来。需要关注self.au_mode,self.select_modes_state 变量,每次选择都会触发业务函数。self.au_mode = tk.IntVar(value=0) self.UNICODE_mode_redio = ttk.Radiobutton(self.regular_expression_frame, text=ENCODING_MODE_NAME[0], variable=self.au_mode, value=0) self.ASCII_mode_redio = ttk.Radiobutton(self.regular_expression_frame, text=ENCODING_MODE_NAME[1], variable=self.au_mode, value=1) self.UNICODE_mode_redio.pack(side=tk.RIGHT, padx=5) self.ASCII_mode_redio.pack(side=tk.RIGHT, padx=5) # 匹配选项多选 option_vars = list(zip(self.select_modes_state, MODE_NAME)) for value, option in option_vars: # 创建并放置Checkbutton选项 ttk.Checkbutton(self.regular_expression_frame, text=option, variable=value, onvalue=1, offvalue=0, padding=3).pack(side=tk.LEFT, padx=5)
菜单按钮,左侧菜单栏,通过绑定点击事件获取当前按钮文本判断执行内容。
# 菜单按钮 for item in MEAU_ITEMS: button = ttk.Button(self.menu_frame, text=item) button.pack(pady=3, padx=3, expand=False, fill=tk.X) self.menu_buttons.append(button)
测试文本框, self.data_text 对象内会经常写入或读取文本内容
# 中间测试文本框 self.data_text = tk.Text(self.center_text_frame) self.data_text.pack(pady=5, padx=3, expand=True, fill=tk.BOTH)
中间底部工具切换栏, self.notebook 管理三个界面,三个界面都包含了 Treeview ,可以绑定点击事件获取被点击目标内容。
# 创建底部切换窗口 self.notebook = ttk.Notebook(self.center_text_frame) # 字符串替换界面 self.notebook_page1 = ttk.Frame(self.notebook) # 替换字符串内容 self.replace_entry = ttk.Entry(self.notebook_page1, textvariable=self.replace_entry_data) self.replace_entry.pack(pady=5, padx=3, expand=True, fill=tk.X) # 替换结果 self.replace_result_txt = tk.Text(self.notebook_page1) self.replace_result_txt.pack(pady=5, padx=3, expand=True, fill=tk.BOTH) # 表达式参考列表界面 self.notebook_page2 = ttk.Frame(self.notebook) self.expression_title_list = ttk.Treeview(self.notebook_page2, columns=[ExpressionsDetailTitle]) # 绑定 Treeview 点击事件 # 为操作列配置可选值 self.expression_title_list.column("#0", width=0, anchor=tk.W, stretch=tk.NO) self.expression_title_list.column(ExpressionsDetailTitle, width=100, anchor=tk.CENTER) self.expression_title_list.heading(ExpressionsDetailTitle, text=ExpressionsDetailTitle) self.expression_title_list.pack(expand=False, fill=tk.BOTH, side=tk.LEFT) # 表达式项详情 self.document_list = ttk.Treeview(self.notebook_page2, columns=ExpressionsDetailCloums) self.document_list.column("#0", width=0, stretch=tk.NO) # 隐藏默认的第一列索引列 self.document_list.column(ExpressionsDetailCloums[0], width=20, anchor=tk.W) self.document_list.column(ExpressionsDetailCloums[1], width=40, anchor=tk.CENTER) # 设置列标题 self.document_list.heading(ExpressionsDetailCloums[0], text=ExpressionsDetailCloums[0]) self.document_list.heading(ExpressionsDetailCloums[1], text=ExpressionsDetailCloums[1]) self.document_list.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH) # 学习模式 self.notebook_page3 = ttk.Frame(self.notebook) # 表达式项详情 self.rex_learn_list = ttk.Treeview(self.notebook_page3, columns=ExpressionsDetailCloums) self.rex_learn_list.column("#0", width=0, stretch=tk.NO) # 隐藏默认的第一列索引列 self.rex_learn_list.column(ExpressionsDetailCloums[0], width=20, anchor=tk.W) self.rex_learn_list.column(ExpressionsDetailCloums[1], width=40, anchor=tk.CENTER) # 设置列标题 self.rex_learn_list.heading(ExpressionsDetailCloums[0], text=ExpressionsDetailCloums[0]) self.rex_learn_list.heading(ExpressionsDetailCloums[1], text=ExpressionsDetailCloums[1]) self.rex_learn_list.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH) # 将页面添加到 Notebook self.notebook.add(self.notebook_page1, text=NOTE_BOOK_PAGE[0]) self.notebook.add(self.notebook_page2, text=NOTE_BOOK_PAGE[1]) self.notebook.add(self.notebook_page3, text=NOTE_BOOK_PAGE[2]) self.notebook.pack(padx=3, side=tk.BOTTOM, fill=tk.BOTH, expand=True)
提示框,该部件会显示所以提示信息,通过不同的颜色标注
# 右边提示文本框 self.tips_text = tk.Text(self.tips_frame) self.tips_text.pack(side=tk.TOP, pady=5, padx=3, expand=True, fill=tk.BOTH)
匹配详情表,此表会显示匹配序号,行列位置,匹配内容
# 右边匹配项详情 self.match_list = ttk.Treeview(self.tips_frame, columns=TestExpressDetailCloum) # 配置列属性 self.match_list.column("#0", width=0, stretch=tk.NO) self.match_list.column(TestExpressDetailCloum[0], width=40, anchor=tk.CENTER) self.match_list.column(TestExpressDetailCloum[1], width=70, anchor=tk.CENTER) self.match_list.column(TestExpressDetailCloum[2], width=150, anchor=tk.CENTER) # 设置列标题 self.match_list.heading(TestExpressDetailCloum[0], text=TestExpressDetailCloum[0]) self.match_list.heading(TestExpressDetailCloum[1], text=TestExpressDetailCloum[1]) self.match_list.heading(TestExpressDetailCloum[2], text=TestExpressDetailCloum[2]) # 布局 Treeview self.match_list.pack(side=tk.BOTTOM, pady=5, padx=3, expand=True, fill=tk.BOTH)
底部 notebook 部件的显示与隐藏
def show_notebook(self): """ 显示 notebook """ self.notebook.pack(padx=3, side=tk.BOTTOM, fill=tk.BOTH, expand=True) def hide_notebook(self): """ 隐藏 notebook """ self.notebook.pack_forget()
业务处理
文件处理
文件内容读取,我们使用csv文件作为数据库
import csv
import os
from ReLearning.src.config import CSV_DATA_PATH, CSV_LEARN_PATH
print(CSV_DATA_PATH)
def write_example_expression_data(data):
"""
写入示例表达式
:param data:
"""
file_exists = os.path.isfile(CSV_DATA_PATH) and os.path.getsize(CSV_DATA_PATH) == 0
with open(CSV_DATA_PATH, 'a', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames= ["type", 'description', 'expression'])
# 写入标题行
if file_exists:
writer.writeheader()
# 逐行写入数据
for row in data:
writer.writerow(row)
def read_example_expression_data():
"""
读取示例表达式
"""
with open(CSV_DATA_PATH, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
return list(reader)
def read_learn_data():
"""
读取示例表达式
"""
with open(CSV_LEARN_PATH, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
return list(reader)
初始化
继承 MainWindowUi 获取所有部件信息和函数,定义常用变量,绑定点击事件。
class WindowApp(MainWindowUi):
def __init__(self):
super().__init__()
# 选择的模式
self.all_modes = ENCODING_MODE[0]
# 匹配模式追踪
self.au_mode.trace("w", self.get_mode_select)
for mode in self.select_modes_state:
mode.trace("w", self.get_mode_select)
# 常用匹配式列表
self.common_expression_list = []
self.pattern = None
# 匹配详情列表
self.match_detail_list = []
# 表达输入追踪
self.expression_entry_data.trace("w", self.expression_entry_data_change)
self.replace_entry_data.trace("w", self.replace_match_data)
# 菜单栏点击事件
for button in self.menu_buttons:
button.bind("<Button-1>", self.menu_button_clicked)
# 常用表达式
self.expression_title_list.bind("<ButtonRelease-1>", self.expression_title_click)
self.document_list.bind("<ButtonRelease-1>", self.expression_document_click)
self.rex_learn_list.bind("<ButtonRelease-1>", self.rex_learn_click)
# 插入示例数据
example_data = read_example_expression_data()
self.example_types = set()
for item in example_data:
if item["type"] not in self.example_types:
self.expression_title_list.insert("", "end", values=(item["type"],))
self.example_types.add(item["type"])
# 插入学习数据
learn_data = read_learn_data()
for item in learn_data:
self.rex_learn_list.insert("", "end", values=(item["title"], item["description"], item["text"]))
计算匹配标志位
def get_mode_select(self, *args):
"""
匹配模式配置
"""
# 编码二选一
self.all_modes = ENCODING_MODE[self.au_mode.get()]
# 其他标志组合
for index, state in enumerate(self.select_modes_state):
if state.get() == 1:
self.all_modes |= MODE[index]
# 重新匹配高亮
self.expression_entry_data_change()
菜单按钮
def menu_button_clicked(self, event):
"""
菜单点击事件处理
:param event:
"""
# 获取触发事件的按钮的文本
button_text = event.widget.cget('text')
# 保存表达式
if button_text == MEAU_ITEMS[0]:
self.save_expression()
# 匹配式测试
elif button_text == MEAU_ITEMS[1]:
self.hide_notebook()
# 替换匹配项
elif button_text == MEAU_ITEMS[2]:
self.show_notebook()
self.notebook.select(0)
# 代码生成
elif button_text == MEAU_ITEMS[3]:
self.create_py_code()
# 导出匹配项
elif button_text == MEAU_ITEMS[4]:
self.export_match_data()
# 常用匹配式
elif button_text == MEAU_ITEMS[5]:
self.show_notebook()
self.notebook.select(1)
# 学习模式
elif button_text == MEAU_ITEMS[6]:
self.show_notebook()
self.notebook.select(2)
获取表达式
获取用户输入的表达式,并实时反应
def expression_entry_data_change(self, *args):
"""
正则表达式改变时,重新匹配测试
:param args:
"""
# 清空匹配信息列表
self.match_detail_list.clear()
# 获取表达式
expression = self.expression_entry_data.get()
# 获取测试文本
all_content = self.data_text.get("1.0", tk.END)
# 根据换行符将测试文本切割
self.lines = all_content.split('\n')
# 不开 多行模式 则只处理一行文本
if re.MULTILINE not in self.all_modes:
self.lines = [self.lines[0]]
# 创建正则表达式对象
try:
self.pattern = re.compile(rf'{expression}', self.all_modes)
except Exception as e:
self.set_tips(msg="表达式不合法", m_type="info")
return
# 处理每一行
for line_id, line in enumerate(self.lines):
matches = self.pattern.finditer(line)
for index, match in enumerate(matches):
start = match.start()
end = match.end()
matched_text = match.group() # 获取匹配到的字符串内容
match_detail = {
"ID": index,
"interval": (start, end),
"matched_text": matched_text,
"line_id": line_id + 1
}
# 存入列表
self.match_detail_list.append(match_detail)
self.set_match_detail()
self.data_text_highlight(color="red")
高亮文本(打标签法)
def data_text_highlight(self, color):
"""
高亮文本
:param color: red | blue
"""
# 去除之前的标签
self.data_text.tag_remove(f"{color}_tag", "1.0", tk.END)
# 打标签
for item in self.match_detail_list:
index_list = range(item["interval"][0], item["interval"][1])
# 匹配位置区间逐个添加标签
for index in index_list:
self.data_text.tag_add(f"{color}_tag", f"{item['line_id']}.{index}")
# 标注的文字显示颜色
self.data_text.tag_config(f"{color}_tag", foreground=color)
表格内容点击
def expression_title_click(self, event):
"""
获取点击事件
:param event:
"""
# 获取被点击的列表项
item = self.expression_title_list.identify("item", event.x, event.y)
if item:
# 清空列表
self.document_list.delete(*self.document_list.get_children())
# 获取被点击项的文本
values = self.expression_title_list.item(item, "values")
# print(values)
m_type = values[0]
example_data = read_example_expression_data()
self.filtered_data = list(filter(lambda x: x["type"] == m_type, example_data))
for item in self.filtered_data:
self.document_list.insert("", "end", values=(item["description"], item["expression"],))
def expression_document_click(self, event):
"""
查看表达式
:param event:
"""
# 获取被点击的列表项
item = self.document_list.identify("item", event.x, event.y)
if item:
# 获取被点击项的文本
values = self.document_list.item(item, "values")
# print(values)
tips = values[0]
expression = values[1]
self.set_tips(msg=tips, m_type="info")
self.expression_entry_data.set(expression)
def rex_learn_click(self, event):
"""
学习表达式
:param event:
"""
# 获取被点击的列表项
item = self.rex_learn_list.identify("item", event.x, event.y)
if item:
# 获取被点击项的文本
values = self.rex_learn_list.item(item, "values")
# print(values)
title = values[0]
description = values[1]
test_text = values[2]
self.data_text.delete("1.0", "end")
self.tips_text.delete("1.0", "end")
self.set_tips(msg=f"{title}:\n{description}", m_type="info")
self.data_text.insert("end", test_text)
提示
def set_tips(self, msg, m_type):
"""
给提示框填充内容
"""
color = COLORS["info"]
if m_type in COLORS.keys():
color = COLORS[m_type]
self.tips_text.insert("end", f"提示:{msg}\n\n", f"{color}_tag")
self.tips_text.tag_config(f"{color}_tag", foreground=color)
self.tips_text.see(tk.END)
替换
def replace_match_data(self, *args):
"""
将匹配的文本替换为用户输入的内容
"""
replace_text = self.replace_entry_data.get()
if not replace_text or self.pattern is None:
self.set_tips(msg="替换文本为空请填写替换文本", m_type="error")
return
replace_counts = 0
result_txt = ""
for line in self.lines:
result = self.pattern.subn(replace_text, line)
replace_counts += result[1]
result_txt += f"{result[0]}\n"
self.set_tips(msg=f"总共替换次数:{replace_counts}", m_type="info")
self.replace_result_txt.delete("1.0", "end")
self.replace_result_txt.insert("end", result_txt)
其他功能很简单,不再赘述。
主函数
from ReLearning.src.window_app import WindowApp
if __name__ == "__main__":
main_window = WindowApp()
main_window.run_app()
打包exe
安装pyinstaller
pip install pyinstaller
修改路径
# 打包使用 # _current_path = Path(".").resolve() / "_internal"
配置文件
# -*- mode: python ; coding: utf-8 -*- a = Analysis( ['main.py'], pathex=[ ], binaries=[], datas=[ (r"src\assest", r"src\assest"), (r"data", r"data"), (r"data\create_data", r"data\create_data"), ], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], noarchive=False, optimize=0, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name='正则表达式测试器', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=[r'src\assest\icon.ico'], onefile=True ) coll = COLLECT( exe, a.binaries, a.datas, strip=False, upx=True, upx_exclude=["python3.dll"], name='main', )
打包
pyinstaller .\main.spec
项目部署
- 创建项目文件夹,创建虚拟环境 venv
- 将源码包解压,把整个文件夹移入与虚拟环境文件夹同级的位置
- 依赖安装
- 启动 main.py