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()
配置表格式
