本文介绍了一个基于Tkinter的文本文件合并工具GUI应用。该工具主要功能包括:
1)支持选择源文件夹和输出文件路径;
2)可按文件扩展名(.txt/.csv等)过滤文件;
3)自动识别文件名中的数字前缀进行排序合并;
4)提供日志记录和状态显示。
核心实现包括文件遍历、数字前缀提取、内容标准化处理等功能,GUI界面包含目录选择、文件过滤、日志显示等模块。该工具适合需要按文件名顺序合并多个文本文件的场景,支持UTF-8编码处理,并能统一换行符格式。使用Python标准库实现,无需额外依赖。
import os
import re
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from tkinter.scrolledtext import ScrolledText
class FileMergerApp:
def __init__(self, root):
self.root = root
self.root.title("文本文件合并工具")
self.root.geometry("800x600")
self.center_window() # 居中窗口
self.root.resizable(True, True)
# 设置样式
self.style = ttk.Style()
self.style.configure("TButton", padding=6, font=("Arial", 10))
self.style.configure("TLabel", font=("Arial", 10))
self.style.configure("TEntry", font=("Arial", 10))
# 创建主框架
main_frame = ttk.Frame(root, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 输入目录选择
input_frame = ttk.LabelFrame(main_frame, text="源文件夹", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_dir = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.input_dir, width=50).pack(
side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
ttk.Button(input_frame, text="浏览...", command=self.select_input_dir).pack(side=tk.RIGHT)
# 输出文件选择
output_frame = ttk.LabelFrame(main_frame, text="输出文件", padding=10)
output_frame.pack(fill=tk.X, pady=(0, 10))
self.output_file = tk.StringVar()
ttk.Entry(output_frame, textvariable=self.output_file, width=50).pack(
side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
ttk.Button(output_frame, text="浏览...", command=self.select_output_file).pack(side=tk.RIGHT)
# 文件类型过滤
filter_frame = ttk.LabelFrame(main_frame, text="文件类型过滤", padding=10)
filter_frame.pack(fill=tk.X, pady=(0, 10))
self.file_types = tk.StringVar(value=".txt;.csv;.log;.md;.json")
ttk.Label(filter_frame, text="支持的扩展名 (用分号分隔):").pack(side=tk.LEFT, padx=(0, 10))
ttk.Entry(filter_frame, textvariable=self.file_types, width=40).pack(side=tk.LEFT, fill=tk.X, expand=True)
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="操作日志", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_area = ScrolledText(log_frame, height=15, wrap=tk.WORD)
self.log_area.pack(fill=tk.BOTH, expand=True)
self.log_area.config(state=tk.DISABLED)
# 按钮区域
button_frame = ttk.Frame(main_frame, padding=(0, 10))
button_frame.pack(fill=tk.X)
ttk.Button(button_frame, text="合并文件", command=self.merge_files).pack(side=tk.RIGHT, padx=(10, 0))
ttk.Button(button_frame, text="清除日志", command=self.clear_log).pack(side=tk.RIGHT)
ttk.Button(button_frame, text="退出", command=root.quit).pack(side=tk.LEFT)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def center_window(self):
"""将窗口居中显示在屏幕上"""
# 更新窗口,确保获取正确的尺寸
self.root.update_idletasks()
# 获取屏幕尺寸
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# 获取窗口尺寸
window_width = self.root.winfo_width()
window_height = self.root.winfo_height()
# 如果窗口尺寸为1,说明尚未渲染,使用初始设置尺寸
if window_width == 1 or window_height == 1:
window_width = 800
window_height = 600
# 计算居中位置
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
# 设置窗口位置
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
def log_message(self, message):
"""在日志区域添加消息"""
self.log_area.config(state=tk.NORMAL)
self.log_area.insert(tk.END, message + "\n")
self.log_area.config(state=tk.DISABLED)
self.log_area.see(tk.END) # 滚动到底部
self.root.update_idletasks() # 更新界面
def clear_log(self):
"""清除日志内容"""
self.log_area.config(state=tk.NORMAL)
self.log_area.delete(1.0, tk.END)
self.log_area.config(state=tk.DISABLED)
self.status_var.set("日志已清除")
def select_input_dir(self):
"""选择输入目录"""
directory = filedialog.askdirectory(title="选择源文件夹")
if directory:
self.input_dir.set(directory)
# 自动设置默认输出文件名
if not self.output_file.get():
default_output = os.path.join(directory, "合并结果.txt")
self.output_file.set(default_output)
def select_output_file(self):
"""选择输出文件"""
file_path = filedialog.asksaveasfilename(
title="保存合并文件",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
defaultextension=".txt"
)
if file_path:
self.output_file.set(file_path)
def extract_numeric_prefix(self, filename):
"""
从文件名中提取数字前缀和剩余部分
:param filename: 文件名
:return: (数字值, 剩余部分) 或 (None, None) 如果没有数字前缀
"""
# 匹配数字前缀(可能包含空格、括号等分隔符)
match = re.match(r'^(\d+)[\s\-_()]*(.*)', filename)
if match:
number_part = match.group(1)
remaining = match.group(2)
return int(number_part), remaining
return None, None # 返回None表示没有数字前缀
def normalize_line_endings(self, content):
"""统一换行符为LF,并确保末尾只有一个换行符"""
# 替换所有换行符为LF
content = content.replace('\r\n', '\n').replace('\r', '\n')
# 去除末尾多余的换行符
content = content.rstrip('\n')
# 添加单个换行符
return content + '\n'
def merge_files(self):
"""执行文件合并操作"""
input_dir = self.input_dir.get()
output_file = self.output_file.get()
if not input_dir or not os.path.isdir(input_dir):
messagebox.showerror("错误", "请选择有效的源文件夹")
return
if not output_file:
messagebox.showerror("错误", "请指定输出文件路径")
return
# 获取文件类型过滤
extensions = [ext.strip().lower() for ext in self.file_types.get().split(';') if ext.strip()]
try:
# 获取文件夹中所有文件列表
all_files = [f for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))]
# 处理文件并提取排序键
to_merge = [] # 待合并的文件列表
non_numeric_files = [] # 非数字开头的文件列表
for filename in all_files:
# 检查文件扩展名
if extensions and not any(filename.lower().endswith(ext) for ext in extensions):
continue
# 提取数字前缀和剩余部分
num_prefix, remaining = self.extract_numeric_prefix(filename)
if num_prefix is not None:
# 创建排序元组:(数字前缀, 剩余部分, 文件名)
to_merge.append((num_prefix, remaining, filename))
else:
non_numeric_files.append(filename)
# 按数字前缀排序,相同数字前缀时按剩余部分排序
sorted_files = sorted(to_merge, key=lambda x: (x[0], x[1]))
# 记录非数字开头的文件
if non_numeric_files:
self.log_message("以下文件不以数字开头,不参与合并:")
for filename in non_numeric_files:
self.log_message(f" - {filename}")
self.log_message(f"共跳过 {len(non_numeric_files)} 个非数字开头的文件\n")
if not sorted_files:
messagebox.showinfo("提示", "没有找到符合要求的数字开头的文件")
return
# 创建输出文件的目录(如果不存在)
output_dir = os.path.dirname(output_file)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
self.log_message(f"开始合并 {len(sorted_files)} 个数字开头的文件...")
self.status_var.set(f"正在合并 {len(sorted_files)} 个文件...")
self.root.update() # 更新UI
# 按顺序合并文件
with open(output_file, 'w', encoding='utf-8') as outfile:
for i, (num, rem, filename) in enumerate(sorted_files, 1):
file_path = os.path.join(input_dir, filename)
try:
with open(file_path, 'r', encoding='utf-8') as infile:
# 读取文件内容
content = infile.read()
# 标准化换行符并确保末尾只有一个换行符
content = self.normalize_line_endings(content)
# 写入文件内容(不带任何额外信息)
outfile.write(content)
log_msg = f"已合并文件 #{i}: {filename} (数字前缀: {num})"
self.log_message(log_msg)
self.root.update() # 更新UI
except UnicodeDecodeError:
self.log_message(f"跳过非文本文件: {filename}")
except Exception as e:
self.log_message(f"处理文件 {filename} 时出错: {str(e)}")
self.log_message(f"\n合并完成!输出文件: {os.path.abspath(output_file)}")
self.status_var.set(f"合并完成!共合并 {len(sorted_files)} 个文件")
message = f"文件合并完成!\n共合并 {len(sorted_files)} 个文件。"
if non_numeric_files:
message += f"\n跳过 {len(non_numeric_files)} 个非数字开头的文件。"
message += f"\n输出文件: {output_file}"
messagebox.showinfo("完成", message)
except Exception as e:
self.log_message(f"发生错误: {str(e)}")
messagebox.showerror("错误", f"合并过程中发生错误:\n{str(e)}")
self.status_var.set("错误: " + str(e))
if __name__ == "__main__":
root = tk.Tk()
app = FileMergerApp(root)
root.mainloop()