ai读项目小工具——文件夹内容复制
文件夹内容复制工具:快速提取项目代码到文本文件
将项目文件复制到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 chardetpywin32(仅 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% 区分文本文件与二进制文件。
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
301import 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()
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Jiajun's Blog!
评论



