正则表达式 测试工具开发文档

正则表达式 测试工具开发文档

在这里插入图片描述

作者: 锦沐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

编码规范

  1. 模块名(文件名)使用小写字母与下划线组合,语义清晰,例如 main_app.py。

  2. 常量名,使用大写、数字、下划线组合, 例如 DATA_PATH。

  3. 类名:使用双驼峰命名法,单词开头大写,例如 MainWindowUi。

  4. 素材文件命名:小写字母、下划线、数字组合,相关资源前缀一致。

  5. 变量名:小写字母、下划线、数字组合,相关变量前缀一致,例如 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)

填充容器

  1. 顶部表达式输入框,我们的表达式从这里输入,绑定 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)
    
  2. 模式选择,先选择 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)
    
    
  3. 菜单按钮,左侧菜单栏,通过绑定点击事件获取当前按钮文本判断执行内容。

      # 菜单按钮
            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)
    
    
  4. 测试文本框, 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)
    
    
  5. 中间底部工具切换栏, 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)
    
  6. 提示框,该部件会显示所以提示信息,通过不同的颜色标注

      # 右边提示文本框
            self.tips_text = tk.Text(self.tips_frame)
            self.tips_text.pack(side=tk.TOP, pady=5, padx=3, expand=True, fill=tk.BOTH)
    
    
  7. 匹配详情表,此表会显示匹配序号,行列位置,匹配内容

       # 右边匹配项详情
            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)
    
    
  8. 底部 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

  1. 安装pyinstaller

    pip install pyinstaller
    
  2. 修改路径

    # 打包使用
    # _current_path = Path(".").resolve() / "_internal"
    
  3. 配置文件

    # -*- 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',
    )
    
  4. 打包

    pyinstaller .\main.spec
    

项目部署

  1. 创建项目文件夹,创建虚拟环境 venv
  2. 将源码包解压,把整个文件夹移入与虚拟环境文件夹同级的位置
  3. 依赖安装
  4. 启动 main.py
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值