当前位置: 首页 > news >正文

Python编写的yaml编辑器

效果预览

图片

图片

代码

代码采用AI辅助编写

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
树形结构 YAML 编辑器 (修复版)
支持可视化编辑 YAML 文件的层级结构
"""import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import yaml
import json
import os
from typing import Any, Dict, List, Optionalclass TreeYAMLEditor:"""基于树形结构的 YAML 编辑器"""def __init__(self, root: tk.Tk):self.root = rootself.root.title("YAML Editor")self.is_modified: bool = Falseself.yaml_data: Any = {}          # ✅ 修复:初始化数据容器self.current_file: Optional[str] = None  # ✅ 修复:初始化文件路径self.tree_items: Dict[str, tuple] = {}  # item_id -> (key, value, path)self._create_toolbar()self._create_main_area()self._create_statusbar()self.root.protocol("WM_DELETE_WINDOW", self._on_close)def _create_toolbar(self):toolbar = ttk.Frame(self.root)toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)buttons_info = [("新建", self.new_file), ("打开", self.open_file), ("保存", self.save_file),("separator", None), ("刷新", self.reload_file), ("separator", None),("添加", self.add_node), ("编辑值", self.edit_value), ("删除", self.delete_node),("separator", None), ("JSON", self.show_json_preview), ("校验", self.validate_yaml),]for text, cmd in buttons_info:if text == "separator":ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=8)else:btn = ttk.Button(toolbar, text=text, command=cmd)btn.pack(side=tk.LEFT, padx=2)def _create_main_area(self):paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)left_frame = ttk.LabelFrame(paned, text="YAML 结构树", padding=5)paned.add(left_frame, weight=2)tree_toolbar = ttk.Frame(left_frame)tree_toolbar.pack(fill=tk.X, pady=(0, 5))ttk.Button(tree_toolbar, text="展开", command=self.expand_all).pack(side=tk.LEFT, padx=2)ttk.Button(tree_toolbar, text="折叠", command=self.collapse_all).pack(side=tk.LEFT, padx=2)self.search_var = tk.StringVar()ttk.Entry(tree_toolbar, textvariable=self.search_var, width=12).pack(side=tk.RIGHT, padx=5)ttk.Button(tree_toolbar, text="搜索", command=self.search_node).pack(side=tk.RIGHT, padx=2)tree_frame = ttk.Frame(left_frame)tree_frame.pack(fill=tk.BOTH, expand=True)vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL)hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL)columns = ("value", "type")self.tree = ttk.Treeview(tree_frame, columns=columns, yscrollcommand=vsb.set, xscrollcommand=hsb.set, selectmode="extended", show=["tree"])self.tree.column("#0", width=300, minwidth=200)self.tree.column("value", width=200, minwidth=100)self.tree.column("type", width=100, minwidth=80)self.tree.heading("#0", text="键")self.tree.heading("value", text="值")self.tree.heading("type", text="类型")vsb.config(command=self.tree.yview)hsb.config(command=self.tree.xview)self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)vsb.pack(side=tk.RIGHT, fill=tk.Y)hsb.pack(side=tk.BOTTOM, fill=tk.X)self.tree.bind("<Double-Button-1>", lambda e: self.edit_value())self.tree.bind("<Return>", lambda e: self.edit_value())self.tree.bind("<F2>", lambda e: self.edit_value())self.tree.bind("<Delete>", lambda e: self.delete_node())right_frame = ttk.LabelFrame(paned, text="节点编辑", padding=10)paned.add(right_frame, weight=1)ttk.Label(right_frame, text="当前路径:", font=("Arial", 10, "bold")).pack(anchor=tk.W)self.path_label = ttk.Label(right_frame, text="/", background="#e8f4e8", relief=tk.SUNKEN, font=("Consolas", 10), anchor=tk.W, padding=5)self.path_label.pack(fill=tk.X, pady=(0, 10))ttk.Label(right_frame, text="节点信息:", font=("Arial", 10, "bold")).pack(anchor=tk.W)info_frame = ttk.Frame(right_frame)info_frame.pack(fill=tk.X, pady=(0, 10))ttk.Label(info_frame, text="键名:").grid(row=0, column=0, sticky=tk.W, padx=5)self.key_var = tk.StringVar()ttk.Entry(info_frame, textvariable=self.key_var, width=20).grid(row=0, column=1, sticky=tk.EW, padx=5)ttk.Label(info_frame, text="类型:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)self.type_label = ttk.Label(info_frame, text="无", background="#f0f0f0", padding=3)self.type_label.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)info_frame.columnconfigure(1, weight=1)ttk.Label(right_frame, text="值:", font=("Arial", 10, "bold")).pack(anchor=tk.W)type_frame = ttk.Frame(right_frame)type_frame.pack(fill=tk.X, pady=(0, 5))self.value_type_var = tk.StringVar(value="str")for val_type, label in [("str", "文本"), ("int", "整数"), ("float", "小数"), ("bool", "布尔"), ("null", "空值")]:ttk.Radiobutton(type_frame, text=label, variable=self.value_type_var, value=val_type).pack(side=tk.LEFT, padx=2)self.value_text = tk.Text(right_frame, font=("Consolas", 11), wrap=tk.WORD, height=8,background="#ffffff", foreground="#333333", insertbackground="#0066cc", relief=tk.GROOVE, borderwidth=1)self.value_text.pack(fill=tk.BOTH, expand=True, pady=(0, 5))btn_frame = ttk.Frame(right_frame)btn_frame.pack(fill=tk.X, pady=(10, 0))ttk.Button(btn_frame, text="应用", command=self.apply_changes).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="重置", command=self.reset_value).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="添加", command=self.add_node).pack(side=tk.RIGHT, padx=2)preview_frame = ttk.LabelFrame(right_frame, text="JSON 预览", padding=5)preview_frame.pack(fill=tk.BOTH, expand=True, pady=(15, 0))self.json_text = tk.Text(preview_frame, font=("Consolas", 9), wrap=tk.WORD, height=6, background="#f8f8f8", state=tk.DISABLED)self.json_text.pack(fill=tk.BOTH, expand=True)self.tree.bind("<<TreeviewSelect>>", self._on_tree_select)def _create_statusbar(self):status_frame = ttk.Frame(self.root)status_frame.pack(side=tk.BOTTOM, fill=tk.X)self.status_label = ttk.Label(status_frame, text="就绪", relief=tk.SUNKEN, anchor=tk.W)self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)self.info_label = ttk.Label(status_frame, text="", relief=tk.SUNKEN, anchor=tk.E)self.info_label.pack(side=tk.RIGHT)def load_yaml_to_tree(self, data):self._clear_tree()if data is None:self.yaml_data = {}returnself.yaml_data = dataif isinstance(data, (dict, list)):self._build_tree(data, parent="")else:self.tree.insert("", tk.END, text="  root", values=[str(data), self._get_type_name(data)])self.tree_items[""] = ("root", data, "")def _clear_tree(self):for item in self.tree.get_children():self.tree.delete(item)self.tree_items.clear()# ✅ 修复:移除错误的 key 参数,统一使用 current_path 追踪完整路径def _build_tree(self, data, parent="", current_path=""):if isinstance(data, dict):for k, v in data.items():new_path = f"{current_path}/{k}" if current_path else kif isinstance(v, dict):display_value, node_type = "{...}", "dict"elif isinstance(v, list):display_value, node_type = f"[{len(v)}]", "list"else:display_value = str(v) if v is not None else "null"node_type = self._get_type_name(v)item_id = self.tree.insert(parent, tk.END, text=f"  {k}", values=[display_value, node_type])self.tree_items[item_id] = (k, v, new_path)if isinstance(v, (dict, list)):self._build_tree(v, item_id, new_path)elif isinstance(data, list):for i, v in enumerate(data):new_path = f"{current_path}[{i}]" if current_path else f"[{i}]"if isinstance(v, dict):display_value, node_type = "{...}", "dict"elif isinstance(v, list):display_value, node_type = f"[{len(v)}]", "list"else:display_value = str(v) if v is not None else "null"node_type = self._get_type_name(v)item_id = self.tree.insert(parent, tk.END, text=f"  [{i}]", values=[display_value, node_type])self.tree_items[item_id] = (f"[{i}]", v, new_path)if isinstance(v, (dict, list)):self._build_tree(v, item_id, new_path)def _get_type_name(self, value: Any) -> str:if value is None: return "null"if isinstance(value, bool): return "bool"if isinstance(value, int): return "int"if isinstance(value, float): return "float"if isinstance(value, str): return "str"return type(value).__name__def _on_tree_select(self, event=None):selection = self.tree.selection()if not selection: returnitem = selection[0]if item not in self.tree_items: returnkey, value, path = self.tree_items[item]self.path_label.config(text=f"/{path}" if path else "/")self.key_var.set(str(key))type_name = self._get_type_name(value)self.type_label.config(text=type_name)# ✅ 修复:同步类型单选框状态(仅标量类型有效)if type_name in ("str", "int", "float", "bool", "null"):self.value_type_var.set(type_name)if isinstance(value, (dict, list)):self.value_text.config(state=tk.NORMAL)self.value_text.delete("1.0", tk.END)self.value_text.insert("1.0", json.dumps(value, ensure_ascii=False, indent=2))self.value_text.config(state=tk.DISABLED)else:self.value_text.config(state=tk.NORMAL)self.value_text.delete("1.0", tk.END)self.value_text.insert("1.0", str(value) if value is not None else "")self._update_json_preview()def edit_value(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请先选择要编辑的节点")returnitem = selection[0]if item in self.tree_items:_, value, _ = self.tree_items[item]if isinstance(value, (dict, list)):self._edit_complex_value(item)else:self.value_text.config(state=tk.NORMAL)self.value_text.focus()def _edit_complex_value(self, item: str):key, value, path = self.tree_items[item]dialog = tk.Toplevel(self.root)dialog.title(f"编辑: {key}")dialog.geometry("500x400")dialog.transient(self.root)dialog.grab_set()ttk.Label(dialog, text=f"路径: /{path}", font=("Arial", 10)).pack(pady=5)text = tk.Text(dialog, font=("Consolas", 11), wrap=tk.WORD)text.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)text.insert("1.0", json.dumps(value, ensure_ascii=False, indent=2))def save():try:raw = text.get("1.0", tk.END).strip()# ✅ 修复:安全解析 JSON,空内容时根据原类型提供默认值new_value = json.loads(raw) if raw else ({} if isinstance(value, dict) else [])self._update_yaml_data(path, new_value)self.load_yaml_to_tree(self.yaml_data)self._set_modified()dialog.destroy()except json.JSONDecodeError as e:messagebox.showerror("错误", f"JSON 格式错误:\n{e}")btn_frame = ttk.Frame(dialog)btn_frame.pack(pady=10)ttk.Button(btn_frame, text="保存", command=save).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)def apply_changes(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请先选择节点")return Falseitem = selection[0]if item not in self.tree_items: return Falsekey, old_value, path = self.tree_items[item]if isinstance(old_value, (dict, list)):self._edit_complex_value(item)return Truevalue_type = self.value_type_var.get()text_content = self.value_text.get("1.0", tk.END).strip()try:if value_type == "null": new_value = Noneelif value_type == "str": new_value = text_contentelif value_type == "int": new_value = int(text_content)elif value_type == "float": new_value = float(text_content)elif value_type == "bool": new_value = text_content.lower() in ("true", "1", "yes")else: new_value = text_contentself._update_yaml_data(path, new_value)self.load_yaml_to_tree(self.yaml_data)self._set_modified()self.status_label.config(text=f"已更新: {key} = {new_value}")return Trueexcept ValueError as e:messagebox.showerror("错误", f"值转换失败:\n{e}")return Falsedef reset_value(self):self._on_tree_select()def _update_yaml_data(self, path: str, new_value: Any):parts = self._parse_path(path)if not parts:self.yaml_data = new_value  # ✅ 修复:空路径表示根节点returndata = self.yaml_datafor part in parts[:-1]:try:data = data[part]except (KeyError, IndexError, TypeError) as e:raise RuntimeError(f"路径导航失败: {path} -> {part}") from elast_part = parts[-1]try:data[last_part] = new_valueexcept (KeyError, IndexError, TypeError) as e:raise RuntimeError(f"更新值失败: {path} -> {last_part}") from edef _parse_path(self, path: str) -> List:if not path: return []parts = []current = ""in_bracket = Falsefor char in path:if char == "/" and not in_bracket:if current: parts.append(current)current = ""elif char == "[":if current: parts.append(current)current = ""in_bracket = Trueelif char == "]":if current: parts.append(int(current))current = ""in_bracket = Falseelse:current += charif current: parts.append(current)return partsdef add_node(self):selection = self.tree.selection()if selection:parent_item = selection[0]if parent_item in self.tree_items:_, parent_value, parent_path = self.tree_items[parent_item]if not isinstance(parent_value, (dict, list)):messagebox.showwarning("提示", "只能在字典或列表下添加")returnelse:parent_item = ""parent_path = ""dialog = tk.Toplevel(self.root)dialog.title("添加节点")dialog.geometry("350x220")dialog.transient(self.root)dialog.grab_set()ttk.Label(dialog, text="添加新节点", font=("Arial", 12, "bold")).pack(pady=10)input_frame = ttk.Frame(dialog)input_frame.pack(pady=10, padx=20, fill=tk.X)ttk.Label(input_frame, text="键名:").grid(row=0, column=0, sticky=tk.W, pady=5)key_var = tk.StringVar()ttk.Entry(input_frame, textvariable=key_var, width=25).grid(row=0, column=1, pady=5, padx=5)ttk.Label(input_frame, text="类型:").grid(row=1, column=0, sticky=tk.W, pady=5)type_var = tk.StringVar(value="str")ttk.Combobox(input_frame, textvariable=type_var, values=["str", "int", "float", "bool", "list", "dict"], width=10, state="readonly").grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)ttk.Label(input_frame, text="值:").grid(row=2, column=0, sticky=tk.W, pady=5)value_var = tk.StringVar()ttk.Entry(input_frame, textvariable=value_var, width=25).grid(row=2, column=1, pady=5, padx=5)def do_add():key = key_var.get().strip()value_type = type_var.get()val_text = value_var.get()if not key:messagebox.showwarning("提示", "键名不能为空")returntry:if value_type == "str": value = val_textelif value_type == "int": value = int(val_text) if val_text else 0elif value_type == "float": value = float(val_text) if val_text else 0.0elif value_type == "bool": value = val_text.lower() in ("true", "1", "yes")elif value_type == "list": value = []elif value_type == "dict": value = {}else: value = val_textexcept ValueError:messagebox.showerror("错误", "值格式不正确")returnif parent_item and self.yaml_data:parent_parts = self._parse_path(parent_path)target = self.yaml_datafor p in parent_parts:target = target[p]if isinstance(target, dict):target[key] = valueelif isinstance(target, list):target.append(value)else:if not isinstance(self.yaml_data, dict):self.yaml_data = {}self.yaml_data[key] = valueself.load_yaml_to_tree(self.yaml_data)self._set_modified()dialog.destroy()self.status_label.config(text=f"已添加: {key}")btn_frame = ttk.Frame(dialog)btn_frame.pack(pady=15)ttk.Button(btn_frame, text="添加", command=do_add).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)def delete_node(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请选择要删除的节点")returnitem = selection[0]if item not in self.tree_items: returnkey, _, path = self.tree_items[item]if not messagebox.askyesno("确认", f"确定删除 '{key}' 吗?"): returntry:parts = self._parse_path(path)if not parts:self.yaml_data = {}  # ✅ 修复:安全处理根节点删除else:data = self.yaml_datafor part in parts[:-1]:data = data[part]last_part = parts[-1]del data[last_part]self.load_yaml_to_tree(self.yaml_data)self._set_modified()self.status_label.config(text=f"已删除: {key}")except Exception as e:messagebox.showerror("错误", f"删除失败:\n{e}")def new_file(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否继续?"): returnself.yaml_data = {}self.current_file = Noneself.is_modified = Falseself.load_yaml_to_tree(self.yaml_data)self._update_title()self._update_json_preview()self.status_label.config(text="已创建新文件")def open_file(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否继续?"): returnfile_path = filedialog.askopenfilename(title="打开 YAML 文件", filetypes=[("YAML文件", "*.yaml *.yml"), ("所有文件", "*.*")])if file_path:try:with open(file_path, "r", encoding="utf-8") as f:self.yaml_data = yaml.safe_load(f.read()) or {}self.current_file = file_pathself.is_modified = Falseself.load_yaml_to_tree(self.yaml_data)self._update_title()self._update_json_preview()self.status_label.config(text=f"已打开: {file_path}")except Exception as e:messagebox.showerror("错误", f"无法打开文件:\n{e}")def save_file(self):if self.current_file:return self._do_save(self.current_file)return self.save_file_as()def save_file_as(self):file_path = filedialog.asksaveasfilename(title="保存 YAML 文件", defaultextension=".yaml", filetypes=[("YAML文件", "*.yaml"), ("YML文件", "*.yml")])if file_path:return self._do_save(file_path)return Falsedef _do_save(self, file_path: str) -> bool:try:with open(file_path, "w", encoding="utf-8") as f:yaml.dump(self.yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False, indent=2)self.current_file = file_pathself.is_modified = Falseself._update_title()self.status_label.config(text=f"已保存: {file_path}")return Trueexcept Exception as e:messagebox.showerror("错误", f"保存失败:\n{e}")return Falsedef reload_file(self):if self.current_file:try:with open(self.current_file, "r", encoding="utf-8") as f:self.yaml_data = yaml.safe_load(f.read()) or {}self.load_yaml_to_tree(self.yaml_data)self._update_json_preview()self.status_label.config(text="已刷新")except Exception as e:messagebox.showerror("错误", f"刷新失败:\n{e}")else:self.load_yaml_to_tree(self.yaml_data)def expand_all(self):for item in self.tree.get_children(""): self._expand_recursive(item)def _expand_recursive(self, item: str):self.tree.item(item, open=True)for child in self.tree.get_children(item): self._expand_recursive(child)def collapse_all(self):for item in self.tree.get_children(""): self._collapse_recursive(item)def _collapse_recursive(self, item: str):self.tree.item(item, open=False)for child in self.tree.get_children(item): self._collapse_recursive(child)def search_node(self):keyword = self.search_var.get().strip().lower()if not keyword: returnself.collapse_all()found = Falsefor item in self.tree.get_children(""):if self._search_recursive(item, keyword): found = Trueif not found: messagebox.showinfo("搜索", f"未找到: '{keyword}'")def _search_recursive(self, item: str, keyword: str) -> bool:found = Falsetext = self.tree.item(item, "text").strip().lower()values = self.tree.item(item, "values")display = values[0].lower() if values else ""if keyword in text or keyword in display:self.tree.see(item)self.tree.selection_set(item)self.tree.focus(item)self.tree.item(item, open=True)found = Truefor child in self.tree.get_children(item):if self._search_recursive(child, keyword): found = Truereturn founddef validate_yaml(self):try:yaml.dump(self.yaml_data, allow_unicode=True)messagebox.showinfo("校验", "YAML 格式正确!")self.status_label.config(text="校验通过")except Exception as e:messagebox.showerror("校验失败", f"格式错误:\n{e}")def show_json_preview(self): self._update_json_preview()def _update_json_preview(self):self.json_text.config(state=tk.NORMAL)self.json_text.delete("1.0", tk.END)try:if self.yaml_data is not None:self.json_text.insert("1.0", json.dumps(self.yaml_data, ensure_ascii=False, indent=2))else:self.json_text.insert("1.0", "null")except Exception as e:  # ✅ 修复:捕获具体异常self.json_text.insert("1.0", f"无法转换: {e}")self.json_text.config(state=tk.DISABLED)def _set_modified(self):self.is_modified = Trueself._update_title()def _update_title(self):filename = os.path.basename(self.current_file) if self.current_file else "未命名"modified = " *" if self.is_modified else ""self.root.title(f"YAML 树形编辑器 - {filename}{modified}")def _on_close(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否退出?"): returnself.root.destroy()def main():root = tk.Tk()style = ttk.Style()try:style.theme_use("clam")except tk.TclError:passapp = TreeYAMLEditor(root)root.mainloop()if __name__ == "__main__":main()
http://www.zskr.cn/news/1383515.html

相关文章:

  • 1985-2025年 专利质押数据 xlsx
  • 长春全屋定制源头工厂选哪家 - 资讯快报
  • 从浪潮到戴尔:不同品牌服务器IPMI配置的‘坑’与避坑指南(附ipmitool通用命令)
  • 突破传统纺织质检的AI革命:YDFID-1色织物图像数据集深度解析
  • 打造智能电商客服 Agent:基于 LLM 的工具调用与多轮推理实践
  • Pikachu暴力破解实战:从Burp Intruder入门到认证逻辑分析
  • 机器学习破解量子测量诱导纠缠难题:从数据中学习量子关联
  • UE5跨关卡数据持久化:SaveGame与GameInstance实战指南
  • 网盘文件下载速度提升方案:LinkSwift直链获取工具全解析
  • 湖北省荆门CPPMSCMP官网报考入口,官方授权双证报考中心 - 众智商学院课程中心
  • JMeter HTTP接口测试全链路实战:从协议合规到业务归因
  • comfyui Z-Image运行报错:error in loading state dict for llama2 size mismatch 的解决方法
  • Windows运行 Pascal Editor 源码报错:环境变量 -a 没有定义解决方法
  • Unity初音跳舞资源深度适配指南:从导入崩溃到群舞流畅
  • 基于大模型 RAG 应用开发与优化|企业级 LLM 应用构建
  • 微信抢红包神器:Android自动抢红包插件深度体验指南
  • 内容创作团队如何通过Taotoken Token Plan套餐控制AI生成成本
  • 软件可维护性评估:CodeScene、SonarQube与ML模型性能对比与工程实践
  • 【Claude测试效能跃迁计划】:为什么92%的团队在v3.5升级后端到端测试失效?3步重建可信性
  • 随身 wifi 性价比高的推荐,2026多场景使用便携上网设备深度测评 - 资讯快报
  • 2026年建材围挡厂家口碑推荐榜:施工围挡、钢结构围挡、市政围挡、工地围挡、彩钢围挡、地铁围挡、工程围挡、建筑围挡、地产围挡、临时围挡厂家选择指南 - 海棠依旧大
  • 2026年C++与C语言结构差异解析:C++非C语言超集,迁移规则需明确
  • 破界而生:AI驱动的下一轮产业革命
  • 为你的开源项目配置 Taotoken 实现低成本 AI 功能
  • JMeter接口签名与加解密实战:从原理到可复用工程化方案
  • 2026年AI编程终极对决:Claude Code vs Codex,谁才是你的最佳AI同事?
  • 浏览器端音乐解锁终极方案:告别加密音乐播放限制
  • 2026 合肥家具工厂直营店性价比排行:3 家本地人公认的省钱好店 - 资讯快报
  • 工业云脑:06 现在就能干:树莓派边缘盒子+PLC,10分钟缺陷检测小案例
  • JMeter接口测试底层原理:从HTTP协议到线程级状态管理