文件夹内容复制工具:快速提取项目代码到文本文件
将项目文件复制到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。
使用方法
-
下载并运行:
- 方法一(推荐): 下载已打包好的可执行文件(见下文“下载”部分),直接双击运行。
- 方法二: 下载源代码,确保您的系统已安装 Python 3 和以下库:
tkinter
(通常 Python 自带)chardet
:pip install chardet
pywin32
(仅 Windows):pip install pywin32
然后运行folder_copy_gui.py
脚本。
-
选择文件夹: 点击“浏览”按钮,选择您要复制内容的源文件夹。
-
选择输出文件(可选): 默认情况下,输出文件会保存在与程序相同的目录下,并以源文件夹的名称命名(加上
.txt
扩展名)。如果您想自定义输出文件,请点击“浏览”按钮选择或输入一个文件名。 -
忽略设置(可选):
- 添加目录/文件:
- 点击“添加目录”按钮,通过文件夹选择对话框选择要忽略的目录。
- 点击“添加文件”按钮,通过文件选择对话框选择要忽略的文件。
- 添加目录/文件模式:
- 点击“添加目录模式”按钮,输入用于匹配目录的正则表达式(例如,
.*[/\\]target$
忽略所有名为target
的目录)。 注意,在正则表达式中表示路径分隔符,Windows 需要两个反斜杠\\\\
,而 Linux/macOS 使用/
。 - 点击“添加文件模式”按钮,输入用于匹配文件的正则表达式(例如,
.*\.log$
忽略所有.log
文件)。
- 点击“添加目录模式”按钮,输入用于匹配目录的正则表达式(例如,
- 移除忽略项: 点击“移除”按钮,在弹出的列表中选择要移除的忽略项。
- 添加目录/文件:
-
开始复制: 点击“开始复制”按钮。程序将开始处理,并在完成后显示提示消息。
示例
假设您有一个 Java 项目文件夹 MyJavaProject
,您想复制所有源文件到 MyJavaProject.txt
。使用本工具,您只需选择 MyJavaProject
文件夹, 点击“开始复制”即可。默认情况下,target
目录和隐藏文件/目录会被忽略。
已知问题/限制
- 超大文件: 对于非常大的文件(例如,几个 GB 的日志文件),复制过程可能需要较长时间,甚至可能导致程序崩溃。建议通过正则表达式模式忽略这些文件。
- 编码检测: 虽然
chardet
库能处理大多数情况,但仍然不能保证 100% 准确地检测所有文件的编码。 - 文件签名: 由于是通过代码读取文件,无法做到 100% 区分文本文件与二进制文件。
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()