Python实现按数字命名合并文本文件

发布于:2025-07-17 ⋅ 阅读:(12) ⋅ 点赞:(0)

本文介绍了一个基于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()


网站公告

今日签到

点亮在社区的每一天
去签到