当前位置:首页>python>Python 通用配置读取器:告别硬编码,让配置管理优雅起来

Python 通用配置读取器:告别硬编码,让配置管理优雅起来

  • 2026-07-04 01:54:37
Python 通用配置读取器:告别硬编码,让配置管理优雅起来

你有没有遇到过这种情况——项目跑了半年,突然要改个数据库地址,结果发现这个 IP 被硬编码在七八个文件里,改得头皮发麻?或者配置文件格式五花八门,JSON、YAML、INI 各自为政,每次读取都要写一堆重复代码?

咱们今天就来彻底解决这个问题。


🤔 配置管理,到底难在哪?

说实话,配置管理这件事,很多人觉得"不就是读个文件嘛",但真正在项目里栽过跟头的人才知道——坑深着呢。

我在一个工控项目里见过这样的代码:

HOST = "192.168.1.100"
PORT = 5432
DB_NAME = "production_db"

硬编码直接写死在业务逻辑里。开发环境、测试环境、生产环境全用同一套,出了问题排查半天,最后发现只是个地址没改。这种"意大利面条式"的配置方式,在小项目里凑合,一旦规模上去,维护成本直线飙升。

更麻烦的是格式问题。老项目用 .ini,新模块喜欢 .yaml,前端同学提交了个 .json,偶尔还有人整个 .toml——每种格式都要单独写解析逻辑,代码冗余不说,还容易出错。

那有没有一种方案,能自动扫描目录、识别格式、统一加载,还带个可视化界面?

有。今天咱们就用 Python + Tkinter 从零撸一个出来。


🏗️ 整体设计思路

整个系统分两层:

底层是 ConfigLoader 核心类,负责扫描目录、按后缀匹配解析器、深度合并配置、提供点号路径访问。

上层是 ConfigLoaderApp GUI 界面,基于 Tkinter 实现,包含配置树、节点详情、路径查询、运行日志四个功能区。

两层之间通过回调函数 log_callback 解耦——核心类完全不依赖 GUI,可以单独在命令行项目里使用。这个设计挺重要,别把业务逻辑和界面逻辑搅在一起。


运行效果

⚙️ 核心类:ConfigLoader

解析器注册表

_PARSERS: Dict[strstr] = {
".json""_parse_json",
".yaml""_parse_yaml",
".yml":  "_parse_yaml",
".toml""_parse_toml",
".ini":  "_parse_ini",
".cfg":  "_parse_ini",
}

这是整个设计里我最喜欢的一个细节——用字典把文件后缀映射到方法名,扩展新格式只需要两步:注册后缀、实现解析方法。不用改任何已有逻辑,典型的开闭原则。

INI 文件的一个坑

用 configparser 读取 logging 配置时,有个经典陷阱:

# ❌ 默认的 ConfigParser 会把 %(asctime)s 当成插值变量
parser = configparser.ConfigParser()

# ✅ 用 RawConfigParser,原样返回字符串
parser = configparser.RawConfigParser()

ConfigParser 默认开启插值功能,遇到 %(asctime)s 会尝试在同一 section 里找名为 asctime 的 key,找不到直接报错:

Bad value substitution: option 'format' in section 'handler_01'
contains an interpolation key 'asctime' which is not a valid option name.

换成 RawConfigParser 就完全没这个问题,logging 格式字符串原样保留。凡是配置文件里有 %(...)s 这种写法的,一律用 Raw 版本。

深度合并,不是浅覆盖

@staticmethod
def_deep_merge(base: dict, override: dict) -> dict:
    result = dict(base)
for k, v in override.items():
if k in result andisinstance(result[k], dictandisinstance(v, dict):
            result[k] = ConfigLoader._deep_merge(result[k], v)
else:
            result[k] = v
return result

浅合并的问题在于,如果两个配置文件都有 database 这个 key,后加载的会把前面的整个覆盖掉,连带把没冲突的子字段也丢了。深度合并会递归处理嵌套字典,只覆盖真正冲突的叶子节点。

点号路径访问

defget(self, key: str, default: Any = None) -> Any:
    keys = key.split(".")
    node = self._data
for k in keys:
ifnotisinstance(node, dictor k notin node:
return default
        node = node[k]
return node

cfg.get("database.pool.max") 比 cfg["database"]["pool"]["max"] 优雅太多了,而且不会因为中间某层不存在就直接抛 KeyError,返回 default 值,调用方自己决定怎么处理。


🖥️ GUI 界面:ConfigLoaderApp

自动加载,省掉手动点击

程序启动后自动扫描默认目录,这个需求看起来简单,实现时有个细节要注意:

def__init__(self):
# ... 初始化代码 ...
self._build_ui()
self._apply_treeview_style()

# 延迟 100ms 再触发,等界面渲染完成
self.after(100self._auto_load)

为啥不直接调 self._on_load()?因为 __init__ 执行完之前,Tkinter 的主循环还没启动,日志面板、树形控件虽然对象已创建,但实际渲染还没完成。直接调用可能导致日志写入时控件状态异常。

self.after(100, ...) 把加载动作推迟到主循环启动后 100 毫秒执行,界面已经完整渲染,日志滚动、树形填充都能正常工作。

def_auto_load(self):  
"""程序启动后自动加载默认配置目录(若目录存在)。"""
    default_dir = Path(self._path_var.get())  
if default_dir.exists():  
self._log_append("[ 自动加载 ] 检测到配置目录,开始自动加载...""info")  
self._on_load()  
else:  
self._log_append(f"[ 自动加载 ] 默认目录不存在: {default_dir}""warn")  
self._log_append("请点击「📂 选择目录」手动指定配置路径""dim")

目录存在就加载,不存在就给提示——不报错、不崩溃,用户体验友好。

树形过滤:实时搜索节点

def_on_tree_search(self, *args):
    keyword = self._search_var.get().strip().lower()
ifnotself._loader:
return
self._populate_tree(self._loader.all(), keyword)

搜索框绑定了 trace_add("write", ...) 事件,每次输入字符都会重新过滤树节点。配置项多的时候这个功能特别实用,比如直接输入 host 就能把所有包含这个词的节点过滤出来。

日志分级着色

self._log_text.tag_config("ok",    foreground="#50fa7b")  # 绿色
self._log_text.tag_config("warn",  foreground="#ffb86c")  # 橙色
self._log_text.tag_config("error", foreground="#ff5555")  # 红色
self._log_text.tag_config("info",  foreground="#8be9fd")  # 蓝色

Tkinter 的 Text 控件支持 tag 机制,不同级别的日志用不同颜色标注,加载成功是绿色,警告是橙色,报错是红色,一眼就能看出哪里出了问题。比全白色日志好用多了。


🚀 完整使用示例

import os  
import json  
import configparser  
import tkinter as tk  
from tkinter import ttk, filedialog, messagebox, scrolledtext  
from pathlib import Path  
from typing importAnyDictOptional
from datetime import datetime  

try:  
import yaml  
    _YAML_AVAILABLE = True
except ImportError:  
    _YAML_AVAILABLE = False

try:  
import tomllib  
except ImportError:  
try:  
import tomli as tomllib  
        _TOML_AVAILABLE = True
except ImportError:  
        _TOML_AVAILABLE = False
        tomllib = None
else:  
    _TOML_AVAILABLE = True

classConfigLoader:  
    _PARSERS: Dict[strstr] = {  
".json""_parse_json",  
".yaml""_parse_yaml",  
".yml":  "_parse_yaml",  
".toml""_parse_toml",  
".ini":  "_parse_ini",  
".cfg":  "_parse_ini",  
    }  

def__init__(
        self,  
        config_dir: str = "config",  
        recursive: bool = False,  
        namespace_by_filename: bool = True,  
        override_order: Optional[list] = None,  
        log_callback=None,  
):  
self.config_dir = Path(config_dir)  
self.recursive = recursive  
self.namespace_by_filename = namespace_by_filename  
self.override_order = override_order or []  
self.log_callback = log_callback  # GUI 日志回调  
self._data: Dict[strAny] = {}  
self._loaded_files = []  
self._load_all()  

def_log(self, msg: str):  
ifself.log_callback:  
self.log_callback(msg)  
else:  
print(msg)  

defget(self, key: str, default: Any = None) -> Any:  
        keys = key.split(".")  
        node = self._data  
for k in keys:  
ifnotisinstance(node, dictor k notin node:  
return default  
            node = node[k]  
return node  

def__getitem__(self, key: str) -> Any:  
        result = self.get(key)  
if result isNone:  
raise KeyError(f"Config key '{key}' not found.")  
return result  

def__contains__(self, key: str) -> bool:  
returnself.get(key) isnotNone

defall(self) -> Dict[strAny]:  
returndict(self._data)  

defreload(self):  
self._data.clear()  
self._loaded_files.clear()  
self._load_all()  
self._log(f"[Reload] 已重新加载目录: {self.config_dir}")  

def_load_all(self):  
ifnotself.config_dir.exists():  
self._log(f"[Error] 配置目录不存在: {self.config_dir}")  
return

        pattern = "**/*"ifself.recursive else"*"
        files = sorted(self.config_dir.glob(pattern))  

defsort_key(p: Path):  
try:  
returnself.override_order.index(p.stem)  
except ValueError:  
return -1

        files = sorted(files, key=sort_key)  

for filepath in files:  
ifnot filepath.is_file():  
continue
            suffix = filepath.suffix.lower()  
            parser_name = self._PARSERS.get(suffix)  
if parser_name isNone:  
continue
try:  
                parsed = getattr(self, parser_name)(filepath)  
self._merge(filepath.stem, parsed)  
self._loaded_files.append(str(filepath))  
self._log(f"[OK]  已加载: {filepath.name}  ({suffix})")  
except Exception as e:  
self._log(f"[WARN] 加载失败: {filepath.name} → {e}")  

def_merge(self, filename_stem: str, parsed: Dict[strAny]):  
ifself.namespace_by_filename:  
            existing = self._data.get(filename_stem, {})  
self._data[filename_stem] = self._deep_merge(existing, parsed)  
else:  
self._data = self._deep_merge(self._data, parsed)  

    @staticmethod  
def_deep_merge(base: dict, override: dict) -> dict:  
        result = dict(base)  
for k, v in override.items():  
if k in result andisinstance(result[k], dictandisinstance(v, dict):  
                result[k] = ConfigLoader._deep_merge(result[k], v)  
else:  
                result[k] = v  
return result  

    @staticmethod  
def_parse_json(filepath: Path) -> Dict:  
withopen(filepath, "r", encoding="utf-8"as f:  
return json.load(f)  

    @staticmethod  
def_parse_yaml(filepath: Path) -> Dict:  
ifnot _YAML_AVAILABLE:  
raise ImportError("PyYAML 未安装,请执行: pip install pyyaml")  
withopen(filepath, "r", encoding="utf-8"as f:  
return yaml.safe_load(f) or {}  

    @staticmethod  
def_parse_toml(filepath: Path) -> Dict:  
ifnot _TOML_AVAILABLE:  
raise ImportError("TOML 解析器未安装,请执行: pip install tomli")  
withopen(filepath, "rb"as f:  
return tomllib.load(f)  

    @staticmethod  
def_parse_ini(filepath: Path) -> Dict:  
# 使用 RawConfigParser 禁用 %(key)s 插值,避免 logging 格式字符串报错  
        parser = configparser.RawConfigParser()  
        parser.read(filepath, encoding="utf-8")  
        result = {}  
for section in parser.sections():  
            result[section] = dict(parser[section])  
return result  

    @classmethod  
defregister_parser(cls, suffix: str, method_name: str):  
        cls._PARSERS[suffix.lower()] = method_name  


classConfigLoaderApp(tk.Tk):  
#  主题配色   
COLORS = {  
"bg":           "#1e1e2e",  
"panel":        "#2a2a3d",  
"border":       "#3a3a55",  
"accent":       "#7c6af7",  
"accent_hover""#9d8fff",  
"success":      "#50fa7b",  
"warning":      "#ffb86c",  
"error":        "#ff5555",  
"info":         "#8be9fd",  
"text":         "#cdd6f4",  
"text_dim":     "#6c7086",  
"tree_sel":     "#45475a",  
"entry_bg":     "#313244",  
    }  

def__init__(self):  
super().__init__()  
self.title("ConfigLoader — 通用配置读取器")  
self.geometry("1100x720")  
self.minsize(900600)  
self.configure(bg=self.COLORS["bg"])  
self._loader: Optional[ConfigLoader] = None
self._build_ui()  
self._apply_treeview_style()  
self.after(100self._auto_load)  

def_auto_load(self):  
"""程序启动后自动加载默认配置目录(若目录存在)。"""
        default_dir = Path(self._path_var.get())  
if default_dir.exists():  
self._log_append("[ 自动加载 ] 检测到配置目录,开始自动加载...""info")  
self._on_load()  
else:  
self._log_append(f"[ 自动加载 ] 默认目录不存在: {default_dir}""warn")  
self._log_append("请点击「📂 选择目录」手动指定配置路径""dim")  

#  UI 构建 ─  
def_build_ui(self):  
#  顶部工具栏   
toolbar = tk.Frame(self, bg=self.COLORS["panel"], pady=10, padx=14)  
        toolbar.pack(fill=tk.X, side=tk.TOP)  

        tk.Label(  
            toolbar, text="⚙  ConfigLoader",  
            font=("Helvetica"15"bold"),  
            bg=self.COLORS["panel"], fg=self.COLORS["accent"]  
        ).pack(side=tk.LEFT)  

# 右侧按钮组  
        btn_frame = tk.Frame(toolbar, bg=self.COLORS["panel"])  
        btn_frame.pack(side=tk.RIGHT)  

self._btn_reload = self._make_button(  
            btn_frame, "↺  重新加载"self._on_reload, disabled=True
        )  
self._btn_reload.pack(side=tk.RIGHT, padx=(60))  

self._make_button(  
            btn_frame, "📂  选择目录"self._on_browse  
        ).pack(side=tk.RIGHT, padx=(60))  

# 目录路径输入框  
        path_frame = tk.Frame(toolbar, bg=self.COLORS["panel"])  
        path_frame.pack(side=tk.RIGHT, padx=(2012))  

        tk.Label(  
            path_frame, text="配置目录:",  
            bg=self.COLORS["panel"], fg=self.COLORS["text_dim"],  
            font=("Helvetica"10)  
        ).pack(side=tk.LEFT)  

self._path_var = tk.StringVar(value=str(Path("config").resolve()))  
        path_entry = tk.Entry(  
            path_frame, textvariable=self._path_var, width=36,  
            bg=self.COLORS["entry_bg"], fg=self.COLORS["text"],  
            insertbackground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Consolas"10), bd=4
        )  
        path_entry.pack(side=tk.LEFT, ipady=3)  

# 选项区(递归 / 命名空间)  
        opt_frame = tk.Frame(toolbar, bg=self.COLORS["panel"])  
        opt_frame.pack(side=tk.RIGHT, padx=(016))  

self._recursive_var = tk.BooleanVar(value=False)  
self._namespace_var = tk.BooleanVar(value=True)  

self._make_checkbox(opt_frame, "递归扫描"self._recursive_var).pack(side=tk.LEFT, padx=4)  
self._make_checkbox(opt_frame, "文件名命名空间"self._namespace_var).pack(side=tk.LEFT, padx=4)  

#  主体区域(左树 + 右详情)  
        main = tk.PanedWindow(  
self, orient=tk.HORIZONTAL,  
            bg=self.COLORS["border"], sashwidth=4, sashrelief=tk.FLAT  
        )  
        main.pack(fill=tk.BOTH, expand=True, padx=10, pady=(60))  

# 左侧:配置树  
        left = tk.Frame(main, bg=self.COLORS["bg"])  
        main.add(left, minsize=280, width=340)  
self._build_tree_panel(left)  

# 右侧:详情 + 查询 + 日志  
        right = tk.Frame(main, bg=self.COLORS["bg"])  
        main.add(right, minsize=400)  
self._build_right_panel(right)  

#  状态栏   
status_bar = tk.Frame(self, bg=self.COLORS["panel"], pady=4)  
        status_bar.pack(fill=tk.X, side=tk.BOTTOM)  
self._status_var = tk.StringVar(value="就绪 — 请选择配置目录并加载")  
        tk.Label(  
            status_bar, textvariable=self._status_var,  
            bg=self.COLORS["panel"], fg=self.COLORS["text_dim"],  
            font=("Helvetica"9), anchor=tk.W, padx=12
        ).pack(fill=tk.X)  

def_build_tree_panel(self, parent):  
        header = tk.Frame(parent, bg=self.COLORS["panel"], pady=6, padx=10)  
        header.pack(fill=tk.X)  
        tk.Label(  
            header, text="配置结构树",  
            font=("Helvetica"11"bold"),  
            bg=self.COLORS["panel"], fg=self.COLORS["text"]  
        ).pack(side=tk.LEFT)  

        tk.Button(  
            header, text="全部展开",  
            command=self._expand_all,  
            bg=self.COLORS["border"], fg=self.COLORS["text_dim"],  
            activebackground=self.COLORS["tree_sel"],  
            activeforeground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Helvetica"8), cursor="hand2",  
            padx=6, pady=1
        ).pack(side=tk.RIGHT, padx=(40))  

        tk.Button(  
            header, text="全部折叠",  
            command=self._collapse_all,  
            bg=self.COLORS["border"], fg=self.COLORS["text_dim"],  
            activebackground=self.COLORS["tree_sel"],  
            activeforeground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Helvetica"8), cursor="hand2",  
            padx=6, pady=1
        ).pack(side=tk.RIGHT)  

# 搜索框  
        search_frame = tk.Frame(parent, bg=self.COLORS["bg"], pady=4, padx=8)  
        search_frame.pack(fill=tk.X)  
self._search_var = tk.StringVar()  
self._search_var.trace_add("write"self._on_tree_search)  
        tk.Entry(  
            search_frame, textvariable=self._search_var,  
            bg=self.COLORS["entry_bg"], fg=self.COLORS["text"],  
            insertbackground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Consolas"10), bd=4
        ).pack(fill=tk.X, ipady=4)  
        tk.Label(  
            search_frame, text="🔍 过滤节点",  
            bg=self.COLORS["bg"], fg=self.COLORS["text_dim"],  
            font=("Helvetica"8)  
        ).pack(anchor=tk.W)  

# Treeview  
        tree_frame = tk.Frame(parent, bg=self.COLORS["bg"])  
        tree_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(08))  

self._tree = ttk.Treeview(  
            tree_frame, show="tree headings",  
            columns=("value",), selectmode="browse"
        )  
self._tree.heading("#0", text="键")  
self._tree.heading("value", text="值")  
self._tree.column("#0", width=160, stretch=True)  
self._tree.column("value", width=150, stretch=True)  

        vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self._tree.yview)  
self._tree.configure(yscrollcommand=vsb.set)  
        vsb.pack(side=tk.RIGHT, fill=tk.Y)  
self._tree.pack(fill=tk.BOTH, expand=True)  
self._tree.bind("<<TreeviewSelect>>"self._on_tree_select)  

def_build_right_panel(self, parent):  
        right_pane = tk.PanedWindow(  
            parent, orient=tk.VERTICAL,  
            bg=self.COLORS["border"], sashwidth=4, sashrelief=tk.FLAT  
        )  
        right_pane.pack(fill=tk.BOTH, expand=True)  

#  上半:节点详情       
        detail_frame = tk.Frame(right_pane, bg=self.COLORS["bg"])  
        right_pane.add(detail_frame, minsize=120, height=220)  

        tk.Label(  
            detail_frame, text="节点详情",  
            font=("Helvetica"11"bold"),  
            bg=self.COLORS["panel"], fg=self.COLORS["text"],  
            pady=6, padx=10, anchor=tk.W  
        ).pack(fill=tk.X)  

self._detail_text = scrolledtext.ScrolledText(  
            detail_frame,  
            bg=self.COLORS["entry_bg"], fg=self.COLORS["info"],  
            font=("Consolas"11), relief=tk.FLAT,  
            insertbackground=self.COLORS["text"],  
            wrap=tk.WORD, state=tk.DISABLED, padx=10, pady=8
        )  
self._detail_text.pack(fill=tk.BOTH, expand=True, padx=8, pady=(48))  

#  中间:点号路径查询        
        query_frame = tk.Frame(right_pane, bg=self.COLORS["bg"])  
        right_pane.add(query_frame, minsize=100, height=160)  

        tk.Label(  
            query_frame, text="点号路径查询",  
            font=("Helvetica"11"bold"),  
            bg=self.COLORS["panel"], fg=self.COLORS["text"],  
            pady=6, padx=10, anchor=tk.W  
        ).pack(fill=tk.X)  

        q_input = tk.Frame(query_frame, bg=self.COLORS["bg"], padx=8, pady=6)  
        q_input.pack(fill=tk.X)  

self._query_var = tk.StringVar()  
        q_entry = tk.Entry(  
            q_input, textvariable=self._query_var,  
            bg=self.COLORS["entry_bg"], fg=self.COLORS["text"],  
            insertbackground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Consolas"11), bd=4
        )  
        q_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=5)  
        q_entry.bind("<Return>"lambda e: self._on_query())  

self._make_button(q_input, "查询"self._on_query).pack(side=tk.LEFT, padx=(80))  

self._query_result = scrolledtext.ScrolledText(  
            query_frame,  
            bg=self.COLORS["entry_bg"], fg=self.COLORS["success"],  
            font=("Consolas"11), relief=tk.FLAT,  
            insertbackground=self.COLORS["text"],  
            wrap=tk.WORD, state=tk.DISABLED, height=4, padx=10, pady=6
        )  
self._query_result.pack(fill=tk.BOTH, expand=True, padx=8, pady=(08))  

#  下半:日志面板        
        log_frame = tk.Frame(right_pane, bg=self.COLORS["bg"])  
        right_pane.add(log_frame, minsize=100, height=180)  

        log_header = tk.Frame(log_frame, bg=self.COLORS["panel"], pady=6, padx=10)  
        log_header.pack(fill=tk.X)  
        tk.Label(  
            log_header, text="运行日志",  
            font=("Helvetica"11"bold"),  
            bg=self.COLORS["panel"], fg=self.COLORS["text"]  
        ).pack(side=tk.LEFT)  
        tk.Button(  
            log_header, text="清空",  
            command=self._clear_log,  
            bg=self.COLORS["border"], fg=self.COLORS["text_dim"],  
            activebackground=self.COLORS["tree_sel"],  
            activeforeground=self.COLORS["text"],  
            relief=tk.FLAT, font=("Helvetica"8), cursor="hand2",  
            padx=6, pady=1
        ).pack(side=tk.RIGHT)  

self._log_text = scrolledtext.ScrolledText(  
            log_frame,  
            bg=self.COLORS["bg"], fg=self.COLORS["text"],  
            font=("Consolas"10), relief=tk.FLAT,  
            insertbackground=self.COLORS["text"],  
            wrap=tk.WORD, state=tk.DISABLED, padx=10, pady=6
        )  
self._log_text.pack(fill=tk.BOTH, expand=True, padx=8, pady=(48))  

# 日志颜色 tag        
self._log_text.tag_config("ok",      foreground=self.COLORS["success"])  
self._log_text.tag_config("warn",     foreground=self.COLORS["warning"])  
self._log_text.tag_config("error",    foreground=self.COLORS["error"])  
self._log_text.tag_config("info",     foreground=self.COLORS["info"])  
self._log_text.tag_config("dim",      foreground=self.COLORS["text_dim"])  

#  样式   
def_apply_treeview_style(self):  
        style = ttk.Style(self)  
        style.theme_use("clam")  
        C = self.COLORS  
        style.configure(  
"Treeview",  
            background=C["panel"],  
            foreground=C["text"],  
            fieldbackground=C["panel"],  
            borderwidth=0,  
            rowheight=24,  
            font=("Consolas"10),  
        )  
        style.configure(  
"Treeview.Heading",  
            background=C["border"],  
            foreground=C["text_dim"],  
            relief=tk.FLAT,  
            font=("Helvetica"10"bold"),  
        )  
        style.map(  
"Treeview",  
            background=[("selected", C["tree_sel"])],  
            foreground=[("selected", C["accent"])],  
        )  
        style.configure(  
"Vertical.TScrollbar",  
            troughcolor=C["bg"],  
            background=C["border"],  
            borderwidth=0,  
            arrowsize=12,  
        )  

def_make_button(self, parent, text, command, disabled=False):  
        C = self.COLORS  
        btn = tk.Button(  
            parent, text=text, command=command,  
            bg=C["accent"], fg="#ffffff",  
            activebackground=C["accent_hover"], activeforeground="#ffffff",  
            relief=tk.FLAT, font=("Helvetica"10"bold"),  
            cursor="hand2", padx=12, pady=5,  
            disabledforeground="#888",  
            state=tk.DISABLED if disabled else tk.NORMAL,  
        )  
return btn  

def_make_checkbox(self, parent, text, variable):  
        C = self.COLORS  
return tk.Checkbutton(  
            parent, text=text, variable=variable,  
            bg=C["panel"], fg=C["text"],  
            activebackground=C["panel"], activeforeground=C["accent"],  
            selectcolor=C["entry_bg"],  
            font=("Helvetica"9),  
        )  

#  事件处理   
def_on_browse(self):  
        directory = filedialog.askdirectory(  
            title="选择配置目录",  
            initialdir=self._path_var.get()  
        )  
if directory:  
self._path_var.set(directory)  
self._on_load()  

def_on_load(self):  
        path = self._path_var.get().strip()  
ifnot path:  
            messagebox.showwarning("提示""请先输入或选择配置目录")  
return
self._log_append(f"{'─'*50}""dim")  
self._log_append(f"[{self._now()}] 开始加载目录: {path}""info")  

self._loader = ConfigLoader(  
            config_dir=path,  
            recursive=self._recursive_var.get(),  
            namespace_by_filename=self._namespace_var.get(),  
            log_callback=self._log_from_loader,  
        )  
self._refresh_tree()  
self._btn_reload.config(state=tk.NORMAL)  
        count = len(self._loader._loaded_files)  
self._set_status(f"已加载 {count} 个配置文件  |  目录: {path}")  
self._log_append(f"[{self._now()}] 完成,共加载 {count} 个文件""ok")  

def_on_reload(self):  
ifnotself._loader:  
return
self._log_append(f"{'─'*50}""dim")  
self._log_append(f"[{self._now()}] 热重载中...""info")  
self._loader.recursive = self._recursive_var.get()  
self._loader.namespace_by_filename = self._namespace_var.get()  
self._loader.reload()  
self._refresh_tree()  
        count = len(self._loader._loaded_files)  
self._set_status(f"热重载完成,共 {count} 个文件  |  {self._now()}")  

def_on_query(self):  
ifnotself._loader:  
self._set_query_result("⚠ 尚未加载任何配置", error=True)  
return
        key = self._query_var.get().strip()  
ifnot key:  
return
        result = self._loader.get(key)  
if result isNone:  
self._set_query_result(f'键 "{key}" 不存在', error=True)  
self._log_append(f'[Query] "{key}" → 未找到'"warn")  
else:  
            formatted = json.dumps(result, ensure_ascii=False, indent=2) \  
ifisinstance(result, (dictlist)) elsestr(result)  
self._set_query_result(formatted)  
self._log_append(f'[Query] "{key}" → {repr(result)}'"ok")  

def_on_tree_select(self, event):  
        selected = self._tree.selection()  
ifnot selected:  
return
        item = selected[0]  
# 收集从根到当前节点的路径  
        path_parts = []  
        node = item  
while node:  
            label = self._tree.item(node, "text")  
            path_parts.insert(0, label)  
            node = self._tree.parent(node)  

        dot_path = ".".join(path_parts)  
ifself._loader:  
            val = self._loader.get(dot_path)  
if val isNone:  
# 尝试去掉根节点再查  
                val = self._loader.get(".".join(path_parts[1:]))  
            display = json.dumps(val, ensure_ascii=False, indent=2) \  
ifisinstance(val, (dictlist)) elsestr(val) if val isnotNoneelse"(无值)"
self._set_detail(f"路径: {dot_path}\n\n{display}")  

def_on_tree_search(self, *args):  
        keyword = self._search_var.get().strip().lower()  
ifnotself._loader:  
return
self._populate_tree(self._loader.all(), keyword)  

#  树形填充   
def_refresh_tree(self):  
ifnotself._loader:  
return
        keyword = self._search_var.get().strip().lower()  
self._populate_tree(self._loader.all(), keyword)  

def_populate_tree(self, data: dict, keyword: str = ""):  
# 清空  
for item inself._tree.get_children():  
self._tree.delete(item)  
self._insert_nodes("", data, keyword)  

def_insert_nodes(self, parent: str, data: Any, keyword: str = "") -> bool:  
"""递归插入节点,返回是否有可见节点(用于过滤)。"""
ifnotisinstance(data, dict):  
returnTrue

        has_visible = False
for key, value in data.items():  
            key_str = str(key)  
ifisinstance(value, dict):  
                node_id = self._tree.insert(  
                    parent, tk.END,  
                    text=key_str, values=("",),  
open=bool(keyword)  
                )  
                child_visible = self._insert_nodes(node_id, value, keyword)  
if keyword andnot child_visible and keyword notin key_str.lower():  
self._tree.delete(node_id)  
else:  
                    has_visible = True
else:  
                val_str = str(value)  
if keyword and keyword notin key_str.lower() and keyword notin val_str.lower():  
continue
self._tree.insert(  
                    parent, tk.END,  
                    text=key_str, values=(val_str,)  
                )  
                has_visible = True
return has_visible  

def_expand_all(self):  
defexpand(node):  
self._tree.item(node, open=True)  
for child inself._tree.get_children(node):  
                expand(child)  
for node inself._tree.get_children():  
            expand(node)  

def_collapse_all(self):  
defcollapse(node):  
self._tree.item(node, open=False)  
for child inself._tree.get_children(node):  
                collapse(child)  
for node inself._tree.get_children():  
            collapse(node)  

#  日志 / 详情 / 状态   
def_log_from_loader(self, msg: str):  
"""ConfigLoader 内部日志回调,自动识别级别。"""
if"[OK]"in msg or"[Reload]"in msg:  
            tag = "ok"
elif"[WARN]"in msg:  
            tag = "warn"
elif"[Error]"in msg:  
            tag = "error"
else:  
            tag = "info"
self._log_append(msg, tag)  

def_log_append(self, msg: str, tag: str = ""):  
self._log_text.config(state=tk.NORMAL)  
self._log_text.insert(tk.END, msg + "\n", tag)  
self._log_text.see(tk.END)  
self._log_text.config(state=tk.DISABLED)  

def_clear_log(self):  
self._log_text.config(state=tk.NORMAL)  
self._log_text.delete("1.0", tk.END)  
self._log_text.config(state=tk.DISABLED)  

def_set_detail(self, text: str):  
self._detail_text.config(state=tk.NORMAL)  
self._detail_text.delete("1.0", tk.END)  
self._detail_text.insert(tk.END, text)  
self._detail_text.config(state=tk.DISABLED)  

def_set_query_result(self, text: str, error: bool = False):  
self._query_result.config(state=tk.NORMAL)  
self._query_result.delete("1.0", tk.END)  
        tag = "error"if error else"ok"
self._query_result.tag_config(  
"error", foreground=self.COLORS["error"]  
        )        self._query_result.tag_config(  
"ok", foreground=self.COLORS["success"]  
        )        self._query_result.insert(tk.END, text, tag)  
self._query_result.config(state=tk.DISABLED)  

def_set_status(self, msg: str):  
self._status_var.set(msg)  

    @staticmethod  
def_now() -> str:  
return datetime.now().strftime("%H:%M:%S")  


if __name__ == "__main__":  
    app = ConfigLoaderApp()  

    app.mainloop()

准备好测试配置文件:

config/app.json

{
"name":"MyApp",
"version":"1.0.0",
"debug":true
}

config/database.yaml

host:localhost
port:5432
pool:
min:2
max:10

config/logging.ini

[handler_01]
level = DEBUG
format = %(asctime)s %(message)s

config/feature_flags.toml

dark_mode = true

[experiment]
ab_test = "group_a"
rollout = 0.25

运行程序后,界面自动加载,点号路径查询直接上手:

cfg = ConfigLoader(config_dir="config")

cfg.get("database.pool.max")        # → 10
cfg.get("app.debug")                # → True
cfg.get("feature_flags.experiment.rollout")  # → 0.25
cfg.get("logging.handler_01.level"# → "DEBUG"

📦 依赖安装

# Python 3.11+ 内置 tomllib,无需安装
pip install pyyaml

# Python 3.10 及以下还需要:
pip install pyyaml tomli

tkinter 是标准库自带,Windows 下安装 Python 时默认包含,无需额外处理。


💡 三句话总结

第一句:用注册表模式管理解析器,扩展新格式只需两步,不动已有代码。

第二句:INI 文件务必用 RawConfigParser,否则 logging 格式字符串会触发插值报错。

第三句:Tkinter 的 after() 方法是延迟执行的正确姿势,别在 __init__ 里直接操作还没渲染好的控件。


🔖 扩展方向

如果这套方案还不够用,可以往这几个方向继续深挖:

  • • 环境隔离:支持 config/dev/config/prod/ 分目录,按环境变量自动切换
  • • 配置热更新:集成 watchdog 库监听文件变化,修改配置文件后自动触发 reload()
  • • 加密配置:敏感字段(密码、Token)加密存储,读取时自动解密
  • • Schema 校验:接入 pydantic 对配置结构做类型校验,启动时就发现配置错误

配置管理这件事,做好了是基础设施,做差了是定时炸弹。希望这套方案能给你的项目提供一个干净的起点。


#Python#配置管理#Tkinter#开发工具#工程实践

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-04 11:03:17 HTTP/2.0 GET : https://f.mffb.com.cn/a/488497.html
  2. 运行时间 : 0.102615s [ 吞吐率:9.75req/s ] 内存消耗:4,930.37kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=28d1dfac7992d5098d6133e970586c61
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000514s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000759s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.002261s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000300s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000518s ]
  6. SELECT * FROM `set` [ RunTime:0.000202s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000603s ]
  8. SELECT * FROM `article` WHERE `id` = 488497 LIMIT 1 [ RunTime:0.004957s ]
  9. UPDATE `article` SET `lasttime` = 1783134197 WHERE `id` = 488497 [ RunTime:0.010819s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000345s ]
  11. SELECT * FROM `article` WHERE `id` < 488497 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000549s ]
  12. SELECT * FROM `article` WHERE `id` > 488497 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000424s ]
  13. SELECT * FROM `article` WHERE `id` < 488497 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000993s ]
  14. SELECT * FROM `article` WHERE `id` < 488497 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001636s ]
  15. SELECT * FROM `article` WHERE `id` < 488497 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.009866s ]
0.104195s