文件夹内容复制工具:快速提取项目代码到文本文件

将项目文件复制到txt中,可用来发送个deepseek,gemeni,ChatGPT等ai工具

下载地址

简介

在软件开发、文档整理、代码分析等场景中,我们经常需要将项目中的源代码或其他文本文件(如配置文件、文档等)提取到一个单独的文本文件中。手动复制粘贴非常繁琐,特别是当项目包含多个子文件夹和大量文件时。“文件夹内容复制工具”就是为了解决这个问题而设计的。

这个小巧而强大的工具可以:

  • 递归复制: 自动遍历指定文件夹及其所有子文件夹。
  • 类型过滤: 只复制常见的文本文件类型(如 .java, .py, .html, .css, .xml, .json, .txt 等),跳过二进制文件。
  • 编码处理: 自动检测文件编码(使用 chardet 库),并尝试使用多种编码(UTF-8, GBK, Latin-1 等)读取,最大限度地避免乱码。
  • 忽略特定目录/文件:
    • Browser 选择: 通过图形界面选择要忽略的特定目录或文件。
    • 正则表达式: 使用正则表达式灵活地匹配要忽略的目录或文件(例如,忽略所有名为 target 的目录,或所有 .log 文件)。
    • 默认忽略: 默认忽略隐藏文件/目录,以及 Java 项目中常见的 target 目录。
  • 自定义输出: 可以将结果保存到您指定的文本文件中,或者使用默认的输出文件名(与源文件夹同名,.txt 扩展名)。
  • 图形界面: 提供直观的图形界面(使用 Tkinter),易于操作。
  • 跨平台: 支持 Windows, macOS, 和 Linux。

使用方法

  1. 下载并运行:

    • 方法一(推荐): 下载已打包好的可执行文件(见下文“下载”部分),直接双击运行。
    • 方法二: 下载源代码,确保您的系统已安装 Python 3 和以下库:
      • tkinter (通常 Python 自带)
      • chardet: pip install chardet
      • pywin32 (仅 Windows): pip install pywin32
        然后运行 folder_copy_gui.py 脚本。
  2. 选择文件夹: 点击“浏览”按钮,选择您要复制内容的源文件夹。

  3. 选择输出文件(可选): 默认情况下,输出文件会保存在与程序相同的目录下,并以源文件夹的名称命名(加上 .txt 扩展名)。如果您想自定义输出文件,请点击“浏览”按钮选择或输入一个文件名。

  4. 忽略设置(可选):

    • 添加目录/文件:
      • 点击“添加目录”按钮,通过文件夹选择对话框选择要忽略的目录。
      • 点击“添加文件”按钮,通过文件选择对话框选择要忽略的文件。
    • 添加目录/文件模式:
      • 点击“添加目录模式”按钮,输入用于匹配目录的正则表达式(例如,.*[/\\]target$ 忽略所有名为 target 的目录)。 注意,在正则表达式中表示路径分隔符,Windows 需要两个反斜杠 \\\\,而 Linux/macOS 使用 /
      • 点击“添加文件模式”按钮,输入用于匹配文件的正则表达式(例如,.*\.log$ 忽略所有 .log 文件)。
    • 移除忽略项: 点击“移除”按钮,在弹出的列表中选择要移除的忽略项。
  5. 开始复制: 点击“开始复制”按钮。程序将开始处理,并在完成后显示提示消息。

示例

假设您有一个 Java 项目文件夹 MyJavaProject,您想复制所有源文件到 MyJavaProject.txt。使用本工具,您只需选择 MyJavaProject 文件夹, 点击“开始复制”即可。默认情况下,target 目录和隐藏文件/目录会被忽略。

已知问题/限制

  • 超大文件: 对于非常大的文件(例如,几个 GB 的日志文件),复制过程可能需要较长时间,甚至可能导致程序崩溃。建议通过正则表达式模式忽略这些文件。
  • 编码检测: 虽然 chardet 库能处理大多数情况,但仍然不能保证 100% 准确地检测所有文件的编码。
  • 文件签名: 由于是通过代码读取文件,无法做到 100% 区分文本文件与二进制文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    import tkinter as tk
    from tkinter import filedialog, messagebox, simpledialog
    import os
    import chardet
    import re
    import platform # 导入 platform 模块

    def 复制文件夹内容到txt(文件夹路径, txt文件路径, ignored_items):
    """
    复制文件夹内容,支持以下忽略项,合并显示:
    - Browser 选择的目录 (B:)
    - 正则表达式匹配的目录 (RD:)
    - Browser 选择的文件 (F:)
    - 正则表达式匹配的文件 (RF:)
    - 隐藏文件/目录 (H:) - 默认启用
    """
    TEXT_EXTENSIONS = [
    '.txt', '.java', '.py', '.c', '.cpp', '.h', '.hpp', '.cs', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs',
    '.swift', '.kt', '.scala', '.groovy', '.php', '.rb', '.pl', '.lua', '.m', '.mm', '.vb', '.vbs', '.bas',
    '.fs', '.f90', '.f', '.for', '.pas', '.inc', '.dpr', '.lpr', '.pp', '.ml', '.mli', '.hs', '.lhs',
    '.clj', '.cljs', '.edn', '.scm', '.ss', '.rkt', '.r', '.rmd', '.asm', '.s',
    '.html', '.htm', '.css', '.scss', '.less', '.sass', '.vue', '.svelte', '.ejs', '.pug', '.haml',
    '.xml', '.json', '.yaml', '.yml', '.md', '.rst', '.toml', '.csv', '.tsv',
    '.sql', '.ddl', '.dml',
    '.sh', '.bash', '.bat', '.ps1', '.cmd',
    '.cfg', '.ini', '.conf', '.config', '.properties', '.env', '.env.local',
    '.gitignore', '.editorconfig', '.prettierrc', '.eslintrc', '.babelrc',
    '.dockerfile', '.docker-compose.yml',
    '.mak', '.cmake', '.pro', '.sln', '.vcproj', '.csproj', '.vcxproj', '.makefile', '.am', '.in',
    'pom.xml', 'build.gradle', 'package.json', 'yarn.lock', 'composer.json', 'Gemfile', 'Pipfile',
    'requirements.txt', 'Cargo.toml',
    '.tex', '.bib', '.sty', '.cls',
    '.log', '.awk', '.sed', '.graphql',
    ]

    try:
    with open(txt文件路径, 'w', encoding='utf-8') as outfile:
    for root, dirs, files in os.walk(文件夹路径):
    # 过滤目录
    dirs[:] = [d for d in dirs if not should_ignore_dir(root, d, ignored_items)]

    for filename in files:
    filepath = os.path.join(root, filename)
    # 过滤文件
    if should_ignore_file(filepath, ignored_items) or is_hidden(filepath): # 添加 is_hidden 检查
    print(f"跳过文件(忽略/隐藏): {filepath}")
    continue

    if os.path.splitext(filename)[1].lower() in TEXT_EXTENSIONS:
    try:
    with open(filepath, 'rb') as f:
    rawdata = f.read()
    result = chardet.detect(rawdata)
    encoding = result['encoding']
    confidence = result['confidence']

    if encoding is None:
    print(f"警告: 无法确定文件 {filepath} 的编码。跳过。")
    continue
    if confidence < 0.7:
    print(f"警告: 文件 {filepath} 编码检测可信度较低 ({confidence:.2f})")

    with open(filepath, 'r', encoding=encoding, errors='replace') as infile:
    content = infile.read()
    outfile.write(f"=== 文件: {filepath} (编码: {encoding}) ===\n")
    outfile.write(content)
    outfile.write("\n\n")
    except Exception as e:
    print(f"错误: 无法读取文件 {filepath}: {e}")
    else:
    print(f"跳过文件(可能不是文本): {filepath}")

    messagebox.showinfo("完成", f"已复制到 '{txt文件路径}'")

    except Exception as e:
    messagebox.showerror("错误", str(e))



    def should_ignore_dir(root, dir_name, ignored_items):
    abs_dir_path = os.path.abspath(os.path.join(root, dir_name))
    for item in ignored_items:
    if item.startswith("B:"):
    if abs_dir_path == item[2:]:
    return True
    elif item.startswith("RD:"):
    if re.match(item[3:], os.path.join(root, dir_name)):
    return True
    elif item.startswith("H:"): # 检查隐藏目录
    if is_hidden(os.path.join(root,dir_name)):
    return True
    return False


    def should_ignore_file(filepath, ignored_items):
    abs_file_path = os.path.abspath(filepath)
    for item in ignored_items:
    if item.startswith("F:"):
    if abs_file_path == item[2:]:
    return True
    elif item.startswith("RF:"):
    if re.match(item[3:], filepath):
    return True
    return False

    def is_hidden(path):
    """
    检查文件或目录是否隐藏。
    """
    try:
    if platform.system() == "Windows":
    # Windows: 检查隐藏属性
    import win32api, win32con
    attrs = win32api.GetFileAttributes(path)
    return attrs & win32con.FILE_ATTRIBUTE_HIDDEN
    else:
    # Unix-like: 检查是否以 . 开头
    return os.path.basename(path).startswith(".")
    except Exception:
    return False # 出现异常,当作非隐藏处理

    def browse_folder():
    folder_selected = filedialog.askdirectory()
    folder_path.set(folder_selected)
    if folder_selected:
    update_default_output_file()

    def browse_output_file():
    file_selected = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
    if file_selected:
    output_file_path.set(file_selected)

    def update_default_output_file():
    folder = folder_path.get()
    if folder:
    default_output = os.path.join(os.getcwd(), os.path.basename(folder) + ".txt")
    output_file_path.set(default_output)

    def add_ignore_dir():
    if not folder_path.get():
    messagebox.showwarning("提示", "请先选择要复制的文件夹")
    return
    dir_to_ignore = filedialog.askdirectory(initialdir=folder_path.get())
    if dir_to_ignore:
    abs_path = os.path.abspath(dir_to_ignore)
    ignored_items.add(f"B:{abs_path}")
    update_ignored_items_display()

    def add_ignore_file():
    if not folder_path.get():
    messagebox.showwarning("提示", "请先选择要复制的文件夹")
    return
    file_to_ignore = filedialog.askopenfilename(initialdir=folder_path.get())
    if file_to_ignore:
    abs_path = os.path.abspath(file_to_ignore)
    ignored_items.add(f"F:{abs_path}")
    update_ignored_items_display()

    def add_ignore_dir_pattern():
    pattern = simpledialog.askstring("添加忽略目录模式", "请输入正则表达式:")
    if pattern:
    try:
    re.compile(pattern)
    ignored_items.add(f"RD:{pattern}")
    update_ignored_items_display()
    except re.error:
    messagebox.showerror("错误", "无效的正则表达式。")

    def add_ignore_file_pattern():
    pattern = simpledialog.askstring("添加忽略文件模式", "请输入正则表达式:")
    if pattern:
    try:
    re.compile(pattern)
    ignored_items.add(f"RF:{pattern}")
    update_ignored_items_display()
    except re.error:
    messagebox.showerror("错误", "无效的正则表达式。")

    def remove_ignore_item():
    if not ignored_items:
    messagebox.showinfo("提示", "没有可移除的忽略项。")
    return

    dialog = tk.Toplevel(root)
    dialog.title("选择要移除的项")
    dialog.transient(root)
    dialog.grab_set()

    listbox = tk.Listbox(dialog, selectmode=tk.SINGLE, width=60, height=min(10, len(ignored_items)))
    # 构建显示文本和原始 item 的映射
    display_to_item = {}
    for item in sorted(ignored_items):
    display_item = item.replace("B:", "Browser Dir: ").replace("RD:", "Regex Dir: ").replace("F:", "Browser File: ").replace("RF:", "Regex File: ").replace("H:", "Hidden: ")
    listbox.insert(tk.END, display_item)
    display_to_item[display_item] = item # 建立映射

    listbox.pack(padx=10, pady=10)

    def do_remove():
    selected_index = listbox.curselection()
    if selected_index:
    selected_item_display = listbox.get(selected_index[0])
    # 使用映射找到原始的 item
    selected_item = display_to_item[selected_item_display]

    ignored_items.remove(selected_item)
    update_ignored_items_display()
    dialog.destroy()
    else:
    messagebox.showinfo("提示", "请先选择要移除的项。")

    remove_button = tk.Button(dialog, text="移除", command=do_remove)
    remove_button.pack(pady=5)
    dialog.wait_window(dialog)



    def update_ignored_items_display():
    ignore_listbox.config(state=tk.NORMAL)
    ignore_listbox.delete("1.0", tk.END)
    display_items = [item.replace("B:", "Browser Dir: ").replace("RD:", "Regex Dir: ").replace("F:", "Browser File: ").replace("RF:", "Regex File: ").replace("H:","Hidden: ")
    for item in sorted(ignored_items)]
    ignore_listbox.insert("1.0", "\n".join(display_items))
    ignore_listbox.config(state=tk.DISABLED)

    def start_copy():
    folder = folder_path.get()
    output_file = output_file_path.get()
    if not folder or not output_file:
    messagebox.showerror("错误", "请选择文件夹和输出文件")
    return
    复制文件夹内容到txt(folder, output_file, ignored_items)

    # --- GUI 部分 ---
    root = tk.Tk()
    root.title("文件夹内容复制工具")
    root.geometry("680x450")
    root.minsize(600, 400)

    root.columnconfigure(1, weight=1)
    root.rowconfigure(3, weight=1)

    # 文件夹选择
    folder_path = tk.StringVar()
    folder_label = tk.Label(root, text="选择文件夹:")
    folder_label.grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
    folder_entry = tk.Entry(root, textvariable=folder_path, width=40)
    folder_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)
    browse_button = tk.Button(root, text="浏览", command=browse_folder)
    browse_button.grid(row=0, column=2, padx=5, pady=5, sticky=tk.E)

    # 输出文件选择
    output_file_path = tk.StringVar()
    output_file_label = tk.Label(root, text="输出文件:")
    output_file_label.grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
    output_file_entry = tk.Entry(root, textvariable=output_file_path, width=40)
    output_file_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW)
    browse_output_button = tk.Button(root, text="浏览", command=browse_output_file)
    browse_output_button.grid(row=1, column=2, padx=5, pady=5, sticky=tk.E)

    # 忽略项
    ignored_items = {"RD:.*/target$", "H:"} # 统一存储, 默认忽略 target 和隐藏文件/目录
    ignore_label = tk.Label(root, text="忽略:")
    ignore_label.grid(row=2, column=0, padx=5, pady=5, sticky=tk.NW)

    ignore_listbox = tk.Text(root, width=50, height=8, relief="sunken")
    ignore_listbox.grid(row=3, column=1, padx=5, pady=5, sticky=tk.NSEW)
    ignore_listbox.config(state=tk.DISABLED)

    scrollbar = tk.Scrollbar(root, command=ignore_listbox.yview)
    scrollbar.grid(row=3, column=2, sticky=tk.NS)
    ignore_listbox["yscrollcommand"] = scrollbar.set

    # 添加/移除按钮 (在同一个 Frame 中)
    button_frame = tk.Frame(root)
    button_frame.grid(row=4, column=1, padx=5, pady=5, sticky=tk.E)

    add_ignore_dir_button = tk.Button(button_frame, text="添加目录", command=add_ignore_dir)
    add_ignore_dir_button.pack(side=tk.LEFT, padx=2)

    add_ignore_file_button = tk.Button(button_frame, text="添加文件", command=add_ignore_file)
    add_ignore_file_button.pack(side=tk.LEFT, padx=2)

    add_ignore_dir_pattern_button = tk.Button(button_frame, text="添加目录模式", command=add_ignore_dir_pattern)
    add_ignore_dir_pattern_button.pack(side=tk.LEFT, padx=2)

    add_ignore_file_pattern_button = tk.Button(button_frame, text="添加文件模式", command=add_ignore_file_pattern)
    add_ignore_file_pattern_button.pack(side=tk.LEFT, padx=2)

    remove_ignore_item_button = tk.Button(button_frame, text="移除", command=remove_ignore_item)
    remove_ignore_item_button.pack(side=tk.LEFT, padx=2)

    # 开始复制按钮
    start_button = tk.Button(root, text="开始复制", command=start_copy, bg="#4CAF50", fg="white", relief=tk.FLAT)
    start_button.grid(row=5, column=0, columnspan=3, padx=10, pady=15, sticky=tk.EW)


    update_default_output_file()
    update_ignored_items_display()

    root.mainloop()