Python Excel转lua配置工具

convert_excel_to_lua_gui.py

python 复制代码
python -m PyInstaller --onefile --windowed --name ExcelToLuaGUI convert_excel_to_lua_gui.py

执行打包,会生成一个 .exe文件,双击即可使用

python 复制代码
# -*- coding: gb2312 -*-
import sys
import os
import json
import re
import glob
import threading
import queue
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext

# ========== 内置默认路径 ==========
DEFAULT_INPUT_DIR = ""
DEFAULT_OUTPUT_CLIENT = ""
DEFAULT_OUTPUT_SERVER = ""
CONFIG_FILE = "config.json"
# =================================

class ConvertApp:
    def __init__(self, root):
        self.root = root
        root.title("Excel 转 Lua 配置工具")
        width, height = 600, 600

        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()
        x = (screen_width - width) // 2
        y = (screen_height - height) // 2
        root.geometry(f"{width}x{height}+{x}+{y}")

        self.log_queue = queue.Queue()
        self.running = False

        self.import_libraries()
        self.config = self.load_config()
        self.create_widgets()
        self.root.after(100, self.poll_log_queue)

    def import_libraries(self):
        try:
            import pandas as pd
            import xlrd
            self.pd = pd
            self.xlrd = xlrd
        except ImportError as e:
            self.log(f"导入依赖失败: {e}", 'error')
            self.log("请确保已安装 pandas, openpyxl, xlrd==1.2.0", 'error')
            sys.exit(1)

    def load_config(self):
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                config.setdefault("INPUT_DIR", DEFAULT_INPUT_DIR)
                config.setdefault("OUTPUT_DIR_CLIENT", DEFAULT_OUTPUT_CLIENT)
                config.setdefault("OUTPUT_DIR_SERVER", DEFAULT_OUTPUT_SERVER)
                return config
            except:
                pass
        config = {
            "INPUT_DIR": DEFAULT_INPUT_DIR,
            "OUTPUT_DIR_CLIENT": DEFAULT_OUTPUT_CLIENT,
            "OUTPUT_DIR_SERVER": DEFAULT_OUTPUT_SERVER
        }
        self.save_config(config)
        return config

    def save_config(self, config=None):
        if config is None:
            config = self.config
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)

    def create_widgets(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)

        path_frame = ttk.LabelFrame(main_frame, text="路径配置", padding=10)
        path_frame.pack(fill=tk.X, pady=5)

        ttk.Label(path_frame, text="Excel 源目录:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        self.input_var = tk.StringVar(value=self.config["INPUT_DIR"])
        ttk.Entry(path_frame, textvariable=self.input_var, width=50).grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(path_frame, text="浏览...", command=self.browse_input).grid(row=0, column=2, padx=5, pady=5)

        ttk.Label(path_frame, text="客户端导出目录:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
        self.client_var = tk.StringVar(value=self.config["OUTPUT_DIR_CLIENT"])
        ttk.Entry(path_frame, textvariable=self.client_var, width=50).grid(row=1, column=1, padx=5, pady=5)
        ttk.Button(path_frame, text="浏览...", command=self.browse_client).grid(row=1, column=2, padx=5, pady=5)

        ttk.Label(path_frame, text="服务端导出目录:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=5)
        self.server_var = tk.StringVar(value=self.config["OUTPUT_DIR_SERVER"])
        ttk.Entry(path_frame, textvariable=self.server_var, width=50).grid(row=2, column=1, padx=5, pady=5)
        ttk.Button(path_frame, text="浏览...", command=self.browse_server).grid(row=2, column=2, padx=5, pady=5)

        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=10)
        self.start_btn = ttk.Button(btn_frame, text="开始转表", command=self.start_conversion)
        self.start_btn.pack(side=tk.LEFT, padx=5)
        self.save_btn = ttk.Button(btn_frame, text="保存路径配置", command=self.save_paths)
        self.save_btn.pack(side=tk.LEFT, padx=5)

        log_frame = ttk.LabelFrame(main_frame, text="转换日志", padding=10)
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=15, state='normal')
        self.log_text.pack(fill=tk.BOTH, expand=True)

        # 日志颜色标签
        self.log_text.tag_configure('error', foreground='red')
        self.log_text.tag_configure('warning', foreground='orange')
        self.log_text.tag_configure('success', foreground='green')
        self.log_text.tag_configure('info', foreground='black')

        self.log("程序已启动,点击"开始转表"进行转换。", 'info')

    def browse_input(self):
        dirpath = filedialog.askdirectory(title="选择 Excel 源目录")
        if dirpath:
            self.input_var.set(dirpath)

    def browse_client(self):
        dirpath = filedialog.askdirectory(title="选择客户端导出目录")
        if dirpath:
            self.client_var.set(dirpath)

    def browse_server(self):
        dirpath = filedialog.askdirectory(title="选择服务端导出目录")
        if dirpath:
            self.server_var.set(dirpath)

    def save_paths(self):
        self.config["INPUT_DIR"] = self.input_var.get()
        self.config["OUTPUT_DIR_CLIENT"] = self.client_var.get()
        self.config["OUTPUT_DIR_SERVER"] = self.server_var.get()
        self.save_config()
        self.log("路径配置已保存。", 'success')

    def log(self, msg, level='info'):
        self.log_queue.put((msg, level))

    def poll_log_queue(self):
        while not self.log_queue.empty():
            msg, level = self.log_queue.get()
            if level not in ('error', 'warning', 'success', 'info'):
                level = 'info'
            self.log_text.insert(tk.END, msg + "\n", level)
            self.log_text.see(tk.END)
        self.root.after(100, self.poll_log_queue)

    def start_conversion(self):
        if self.running:
            self.log("转换正在运行中,请勿重复点击!", 'warning')
            return

        self.save_paths()
        input_dir = self.config["INPUT_DIR"]
        client_dir = self.config["OUTPUT_DIR_CLIENT"]
        server_dir = self.config["OUTPUT_DIR_SERVER"]

        if not os.path.exists(input_dir):
            self.log(f"错误:输入目录 '{input_dir}' 不存在!", 'error')
            return

        os.makedirs(client_dir, exist_ok=True)
        os.makedirs(server_dir, exist_ok=True)

        self.running = True
        self.start_btn.config(state=tk.DISABLED)
        self.log("开始转换...", 'info')
        self.log(f"源目录: {input_dir}", 'info')
        self.log(f"客户端输出: {client_dir}", 'info')
        self.log(f"服务端输出: {server_dir}", 'info')

        thread = threading.Thread(target=self.run_conversion, args=(input_dir, client_dir, server_dir))
        thread.daemon = True
        thread.start()

    def run_conversion(self, input_dir, client_dir, server_dir):
        try:
            files = []
            for ext in ["*.xls", "*.xlsx"]:
                files.extend(glob.glob(os.path.join(input_dir, ext)))
            if not files:
                self.log(f"未在 {input_dir} 中找到 Excel 文件", 'warning')
                self.conversion_finished()
                return

            self.log(f"找到 {len(files)} 个文件,开始处理...", 'info')
            success = 0
            for f in files:
                try:
                    self.log(f"正在处理: {os.path.basename(f)}", 'info')
                    ok = self.process_one_excel(f, client_dir, server_dir)
                    if ok:
                        success += 1
                    else:
                        self.log(f"   处理失败: {os.path.basename(f)}", 'error')
                except Exception as e:
                    self.log(f"   处理 {os.path.basename(f)} 时发生异常: {e}", 'error')

            if success == len(files):
                self.log(f"转换完成,成功 {success} 个,失败 {len(files)-success} 个。", 'success')
            else:
                self.log(f"转换完成,成功 {success} 个,失败 {len(files)-success} 个。", 'warning')
        except Exception as e:
            self.log(f"转换过程发生错误: {e}", 'error')
        finally:
            self.conversion_finished()

    def conversion_finished(self):
        self.running = False
        self.start_btn.config(state=tk.NORMAL)
        self.log("转换结束。", 'info')

    # ---------- 核心转换函数 ----------
    def process_one_excel(self, file_path, client_dir, server_dir):
        try:
            if file_path.lower().endswith('.xls'):
                wb = self.xlrd.open_workbook(file_path)
                sheet = wb.sheet_by_index(0)
                data = [sheet.row_values(r) for r in range(sheet.nrows)]
                df = self.pd.DataFrame(data)
            else:
                df = self.pd.read_excel(file_path, sheet_name=0, header=None, engine='openpyxl')

            if len(df) < 4:
                self.log(f"   跳过 {os.path.basename(file_path)}:行数不足", 'warning')
                return False

            # 定位分类行(全大写字母行)和列名行(下一行)
            class_row_idx = None
            header_row_idx = None
            max_search = 10
            for i in range(min(max_search, len(df))):
                row = df.iloc[i]
                is_class = True
                has_content = False
                for cell in row:
                    val = str(cell).strip()
                    if val == "":
                        continue
                    has_content = True
                    # 允许大写字母、数字、下划线、斜杠、点、连字符
                    if not all(c.isupper() or c.isdigit() or c in ' /.-_' for c in val):
                        is_class = False
                        break
                if is_class and has_content:
                    class_row_idx = i
                    if i + 1 < len(df):
                        next_row = df.iloc[i + 1]
                        has_name = False
                        for cell in next_row:
                            cell_str = str(cell).strip()
                            if cell_str and not re.match(r'^[\d.]+$', cell_str):
                                has_name = True
                                break
                        if has_name:
                            header_row_idx = i + 1
                            break
            if class_row_idx is None or header_row_idx is None:
                self.log(f"   错误:{os.path.basename(file_path)} 未找到分类行或列名行", 'error')
                return False

            class_row = df.iloc[class_row_idx].astype(str).str.strip().tolist()
            header_row = df.iloc[header_row_idx].astype(str).str.strip().tolist()

            # 输出调试信息
            self.log(f"   分类行: {class_row}", 'info')
            self.log(f"   列名行: {header_row}", 'info')

            col_mapping = {}
            for col_idx, field_name in enumerate(header_row):
                if not field_name or str(field_name).strip() == "":
                    continue
                lua_field = str(field_name).strip()
                class_label = str(class_row[col_idx]).strip().upper() if col_idx < len(class_row) else ""

                # 判断归属
                belongs_client = "C" in class_label
                belongs_server = "S" in class_label
                if not belongs_client and not belongs_server:
                    belongs_client = True
                    belongs_server = True

                # 是否原样输出(_N 后缀)
                raw = class_label.endswith("_N")
                if raw:
                    self.log(f"   字段 '{lua_field}' 标记为 _N,数据将原样输出", 'info')

                col_mapping[lua_field] = (col_idx, False, False, belongs_client, belongs_server, raw)

            # 默认第一列为索引
            index_col_idx = 0
            index_field_name = header_row[0].strip() if header_row else "index"
            if not index_field_name:
                index_field_name = "index"
            if index_field_name in col_mapping:
                col_mapping[index_field_name] = (index_col_idx, False, False, True, True, False)
            else:
                col_mapping[index_field_name] = (index_col_idx, False, False, True, True, False)

            # 提取数据行
            data_rows = []
            for r in range(header_row_idx + 1, len(df)):
                row = df.iloc[r]
                if self.pd.isna(row.iloc[index_col_idx]) or str(row.iloc[index_col_idx]).strip() == "":
                    continue
                data_rows.append(row)

            if not data_rows:
                self.log(f"   警告:{os.path.basename(file_path)} 无有效数据", 'warning')
                return False

            base_name = os.path.splitext(os.path.basename(file_path))[0] + ".lua"

            client_lua = self.generate_lua_table(data_rows, col_mapping, index_field_name, index_col_idx, "client")
            out_client = os.path.join(client_dir, base_name)
            with open(out_client, "w", encoding="gb2312") as f:
                f.write(client_lua)
            self.log(f"   客户端生成:{out_client}", 'success')

            server_lua = self.generate_lua_table(data_rows, col_mapping, index_field_name, index_col_idx, "server")
            out_server = os.path.join(server_dir, base_name)
            with open(out_server, "w", encoding="gb2312") as f:
                f.write(server_lua)
            self.log(f"   服务端生成:{out_server}", 'success')

            return True
        except Exception as e:
            self.log(f"   转换失败 {os.path.basename(file_path)}: {e}", 'error')
            return False

    def generate_lua_table(self, data_rows, col_mapping, index_field_name, index_col_idx, side):
        lines = ["local config = {"]
        for row in data_rows:
            key_val = row.iloc[index_col_idx]
            if self.pd.isna(key_val) or str(key_val).strip() == "":
                continue
            try:
                key = int(key_val)
            except:
                key = key_val

            fields = []
            index_val = self.try_number(key_val)
            fields.append(f"    {index_field_name} = {self.format_lua_value(index_val)}")

            for lua_field, (col_idx, _, _, belongs_client, belongs_server, raw) in col_mapping.items():
                if lua_field == index_field_name:
                    continue
                if side == "client" and not belongs_client:
                    continue
                if side == "server" and not belongs_server:
                    continue

                raw_val = row.iloc[col_idx]
                if self.pd.isna(raw_val) or str(raw_val).strip() == "":
                    continue

                if raw:
                    # 原样输出:直接转换数字或字符串
                    val = self.try_number(raw_val)
                    fields.append(f"    {lua_field} = {self.format_lua_value(val)}")
                else:
                    # 正常解析
                    if "|" in str(raw_val):
                        parsed = self.parse_pipe_value(raw_val)
                        if parsed is None:
                            continue
                        fields.append(f"    {lua_field} = {self.format_lua_value(parsed)}")
                    else:
                        val = self.try_number(raw_val)
                        fields.append(f"    {lua_field} = {self.format_lua_value(val)}")

            if fields:
                lines.append(f"    [{key}] = {{")
                lines.append(",\n".join(fields))
                lines.append("    },")
        lines.append("}")
        lines.append("return config")
        return "\n".join(lines)

    # ---------- 辅助函数 ----------
    def try_number(self, s):
        if isinstance(s, (int, float)):
            if isinstance(s, float) and s.is_integer():
                return int(s)
            return s
        s = str(s).strip()
        if s == "":
            return s
        if '.' in s:
            try:
                f = float(s)
                if f.is_integer():
                    return int(f)
                return f
            except ValueError:
                return s
        else:
            try:
                return int(s)
            except ValueError:
                return s

    def parse_pipe_value(self, val):
        if self.pd.isna(val) or str(val).strip() == "":
            return None
        parts = str(val).split("|")
        parts = [p.strip() for p in parts if p.strip() != ""]
        if not parts:
            return None
        result = []
        for p in parts:
            if '#' in p or '^' in p:
                elements = re.split(r'[#^]', p)
                elements = [self.try_number(e.strip()) for e in elements if e.strip() != ""]
                if elements:
                    result.append(elements)
            else:
                result.append(self.try_number(p))
        return result

    def format_lua_value(self, v):
        if isinstance(v, str):
            return f'"{v}"'
        elif isinstance(v, list):
            return "{" + ", ".join(self.format_lua_value(item) for item in v) + "}"
        elif v is None:
            return "nil"
        else:
            return str(v)

def main():
    root = tk.Tk()
    app = ConvertApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

配置表格式

相关推荐
minglie19 小时前
esp32集成lua
lua
番茄炒鸡蛋加糖1 天前
Redis--Lua 脚本原子性与滑动窗口限流
数据库·redis·lua
星空露珠2 天前
28种颜色对应名称,
开发语言·数据库·算法·游戏·lua
吴声子夜歌10 天前
Redis 3.x——事务与Lua
redis·lua
豆瓣鸡12 天前
Redis Lua 脚本——把多条命令打包成一个原子操作
java·redis·lua
橙色阳光五月天14 天前
MATLAB函数如何打包成独立EXE
开发语言·matlab·lua
耀耀_很无聊17 天前
12_Redis Lua 脚本踩坑:为什么 tonumber(ARGV[x]) 会得到 nil?
java·redis·lua
章老师说18 天前
NGINX官方谈Lua风险:这其实是两条网关技术路线之争
运维·nginx·负载均衡·lua·openresty
会周易的程序员19 天前
使用 LuaBridge 封装 C++ 日志库 microLog 为 Lua 模块
c++·物联网·junit·lua·日志·iot·aiot