在日常开发和项目初始化过程中,我们经常需要按照某种预设的架构创建大量的文件夹和空文件。特别是当我们在使用 ai 生成项目方案时,它通常会给出一个视觉化的树状图(tree structure)。手动一个个新建显然太慢。
今天,我们将深入分析一个基于 python 和 wxpython 编写的脚本,它不仅能将“文本树”瞬间转化为“真实目录”,还集成了文件扫描、预览和备注管理功能。
背景 (background)
在软件开发、学术研究或复杂的文档管理中,维持一致的文件组织结构至关重要。常见的痛点包括:
- 重复劳动:每次新项目都要手动创建
src/,tests/,docs/等目录。 - 割裂感:ai 或文档给出了目录结构,但开发者需要“肉眼阅读”并“手动复现”。
- 文件同步麻烦:每天产生的新文件(如日志、导出的临时代码)需要快速覆盖到项目对应的占位文件中。
该工具正是在这种“快速落地项目结构”和“日常文件维护”的需求下诞生的。
目标 (goal)
该程序的核心目标是打造一个 轻量级的桌面效率工具,具体实现以下功能:
- 文本转目录:解析 ascii 树状文本,并在本地磁盘一键生成对应的文件夹和文件。
- 可视化管理:通过 gui 树状控件直观展示生成的目录。
- 增量扫描:自动识别计算机中“今天”修改过的非媒体文件,方便快速同步。
- 文件操作集成:支持文件内容预览、快速覆盖(copy2)以及备注记录。
实现方法 (method)
为了实现上述目标,开发者采用了以下技术栈和设计模式:
gui 框架:使用 wxpython。它提供了原生的 windows/macos/linux 控件体验,适合开发这类工具类应用。
核心逻辑库:
os&pathlib:处理路径拼接、目录创建和文件存在性检查。shutil:执行高保真的文件复制(保留元数据)。json:实现配置的持久化存储,记录用户上次选择的路径。
解析算法:采用栈(stack)数据结构处理树状结构的深度级联。通过计算字符串前缀的空格和符号数量来判断层级关系。
过程 (process)
1. 核心解析引擎:从字符串到磁盘
这是程序最精彩的部分(on_create_structure 方法)。它通过以下步骤处理输入的文本:
- 符号清洗:通过
replace去掉├──,└──,│等装饰性符号。 - 层级推算:利用循环计数每行开头的空格和特殊字符,确定当前文件处于第几层。
- 栈式追踪:维护一个包含
(当前路径, 层级)的栈。当新一行的层级减少时,不断弹出栈顶,直到找到其父目录。 - 智能识别:以
/结尾的行识别为文件夹,否则识别为文件并创建空文件。
2. 界面布局逻辑
程序使用了 wx.boxsizer 进行响应式布局。
- 顶部:目标路径选择。
- 中部:左右分栏。左侧输入 ascii 文本,右侧即时呈现生成的
wx.treectrl树状视图。 - 底部:集成文件扫描器、预览框和剪贴板备注工具。
3. 文件扫描与过滤机制
scan_today_files 函数通过 os.walk 遍历目录:
- 时间过滤:使用
os.path.getmtime获取最后修改时间,并与datetime.now().date()比对。 - 类型过滤:定义了
media_extensions集合,自动排除图片、视频、音频等大文件,聚焦于脚本和文档。
结果 (results)
通过运行该源代码,用户可以获得一个功能完备的桌面应用:
- 高效初始化:输入
my_project/ \n ├── main.py \n └── config/,点击按钮,磁盘上立即出现对应结构。 - 配置记忆:程序启动时会自动加载
file_manager_config.json,用户无需反复选择目标文件夹。 - 闭环操作:用户可以在左侧看到今天写了哪些文件,在右侧树状图中选择目标,一键“覆盖”,极大地简化了代码片段或配置文件的同步流程。
- 预览与备注:无需打开外部编辑器即可查看文件内容,点击备注即可快速复制到剪贴板。
运行结果

完整代码
import wx
import os
import shutil
import json
from datetime import datetime
from pathlib import path
class filestructuremanager(wx.frame):
def __init__(self):
super().__init__(parent=none, title='文件结构管理工具', size=(1200, 800))
self.target_folder = ""
self.tree_root_path = ""
self.created_root_folder = "" # 记录创建的根文件夹路径
self.config_file = "file_manager_config.json"
self.last_scan_folder = "" # 记录上次扫描的文件夹
self.current_file_path = "" # 当前预览的文件路径
# 加载配置
self.load_config()
panel = wx.panel(self)
main_sizer = wx.boxsizer(wx.vertical)
# 目标文件夹选择区域
folder_sizer = wx.boxsizer(wx.horizontal)
self.folder_label = wx.statictext(panel, label="目标文件夹: 未选择")
folder_btn = wx.button(panel, label="选择目标文件夹")
folder_btn.bind(wx.evt_button, self.on_select_folder)
folder_sizer.add(self.folder_label, 1, wx.all | wx.expand, 5)
folder_sizer.add(folder_btn, 0, wx.all, 5)
main_sizer.add(folder_sizer, 0, wx.expand)
# 树状结构输入区域
input_sizer = wx.boxsizer(wx.horizontal)
# 左侧:memo输入框
left_sizer = wx.boxsizer(wx.vertical)
left_sizer.add(wx.statictext(panel, label="输入树状结构:"), 0, wx.all, 5)
self.memo = wx.textctrl(panel, style=wx.te_multiline, size=(300, 200))
self.memo.setvalue("excel-sql-ai/\n├── server.js\n├── public/\n│ └── index.html\n├── uploads/\n└── .env")
left_sizer.add(self.memo, 1, wx.all | wx.expand, 5)
create_btn = wx.button(panel, label="创建文件结构")
create_btn.bind(wx.evt_button, self.on_create_structure)
left_sizer.add(create_btn, 0, wx.all | wx.expand, 5)
input_sizer.add(left_sizer, 1, wx.expand)
# 右侧:tree组件
right_sizer = wx.boxsizer(wx.vertical)
right_sizer.add(wx.statictext(panel, label="文件结构预览:"), 0, wx.all, 5)
# --- 新增:加载树按钮 ---
load_tree_btn = wx.button(panel, label="加载/刷新目录树")
load_tree_btn.bind(wx.evt_button, self.on_load_tree)
right_sizer.add(load_tree_btn, 0, wx.all | wx.expand, 5)
# ----------------------
self.tree = wx.treectrl(panel, size=(300, 200), style=wx.tr_default_style | wx.tr_hide_root)
self.tree.bind(wx.evt_tree_sel_changed, self.on_tree_select)
right_sizer.add(self.tree, 1, wx.all | wx.expand, 5)
open_btn = wx.button(panel, label="打开根目录")
open_btn.bind(wx.evt_button, self.on_open_root)
right_sizer.add(open_btn, 0, wx.all | wx.expand, 5)
input_sizer.add(right_sizer, 1, wx.expand)
main_sizer.add(input_sizer, 0, wx.expand)
# 文件列表区域
list_sizer = wx.boxsizer(wx.horizontal)
# listbox1:当天非媒体文件
list1_sizer = wx.boxsizer(wx.vertical)
list1_label_sizer = wx.boxsizer(wx.horizontal)
list1_label_sizer.add(wx.statictext(panel, label="今日非媒体文件:"), 1, wx.all, 5)
scan_btn = wx.button(panel, label="扫描文件")
scan_btn.bind(wx.evt_button, self.on_scan_files)
list1_label_sizer.add(scan_btn, 0, wx.all, 5)
refresh_btn = wx.button(panel, label="刷新")
refresh_btn.bind(wx.evt_button, self.on_refresh_scan)
list1_label_sizer.add(refresh_btn, 0, wx.all, 5)
list1_sizer.add(list1_label_sizer, 0, wx.expand)
self.listbox1 = wx.listbox(panel, size=(250, 150))
list1_sizer.add(self.listbox1, 1, wx.all | wx.expand, 5)
copy_btn = wx.button(panel, label="覆盖")
copy_btn.bind(wx.evt_button, self.on_copy_file)
list1_sizer.add(copy_btn, 0, wx.all | wx.expand, 5)
list_sizer.add(list1_sizer, 1, wx.expand)
# 中间:预览区域
preview_sizer = wx.boxsizer(wx.vertical)
preview_label_sizer = wx.boxsizer(wx.horizontal)
preview_label_sizer.add(wx.statictext(panel, label="文件预览:"), 1, wx.all, 5)
save_preview_btn = wx.button(panel, label="保存修改")
save_preview_btn.bind(wx.evt_button, self.on_save_preview)
preview_label_sizer.add(save_preview_btn, 0, wx.all, 5)
preview_sizer.add(preview_label_sizer, 0, wx.expand)
self.preview = wx.textctrl(panel, style=wx.te_multiline, size=(300, 150))
preview_sizer.add(self.preview, 1, wx.all | wx.expand, 5)
list_sizer.add(preview_sizer, 1, wx.expand)
# listbox2:备注列表
list2_sizer = wx.boxsizer(wx.vertical)
list2_sizer.add(wx.statictext(panel, label="备注列表:"), 0, wx.all, 5)
edit_sizer = wx.boxsizer(wx.horizontal)
self.edit1 = wx.textctrl(panel)
submit_btn = wx.button(panel, label="提交")
submit_btn.bind(wx.evt_button, self.on_submit_note)
edit_sizer.add(self.edit1, 1, wx.all, 5)
edit_sizer.add(submit_btn, 0, wx.all, 5)
list2_sizer.add(edit_sizer, 0, wx.expand)
self.listbox2 = wx.listbox(panel, size=(250, 100))
self.listbox2.bind(wx.evt_listbox, self.on_note_select)
list2_sizer.add(self.listbox2, 1, wx.all | wx.expand, 5)
list_sizer.add(list2_sizer, 1, wx.expand)
main_sizer.add(list_sizer, 1, wx.expand)
# 状态栏
self.status = wx.statictext(panel, label="就绪")
main_sizer.add(self.status, 0, wx.all | wx.expand, 5)
panel.setsizer(main_sizer)
self.centre()
self.show()
def on_load_tree(self, event):
"""点击加载树按钮的回调"""
if not self.target_folder:
wx.messagebox("请先选择目标文件夹", "提示", wx.ok | wx.icon_warning)
return
if not os.path.exists(self.target_folder):
wx.messagebox("目标文件夹路径不存在,请重新选择", "错误", wx.ok | wx.icon_error)
return
# 如果用户点击“加载树”,通常是想看整个目标文件夹的内容
# 我们可以清除掉“记录的已创建根目录”,强制刷新整个目标目录
self.created_root_folder = ""
self.status.setlabel(f"正在加载: {self.target_folder}")
self.refresh_tree()
self.status.setlabel("目录树加载完成")
def load_config(self):
"""加载配置文件"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.target_folder = config.get('target_folder', '')
self.last_scan_folder = config.get('last_scan_folder', '')
self.created_root_folder = config.get('created_root_folder', '')
except exception as e:
print(f"加载配置失败: {e}")
def save_config(self):
"""保存配置文件"""
try:
config = {
'target_folder': self.target_folder,
'last_scan_folder': self.last_scan_folder,
'created_root_folder': self.created_root_folder
}
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=false, indent=2)
except exception as e:
print(f"保存配置失败: {e}")
def on_select_folder(self, event):
dlg = wx.dirdialog(self, "选择目标文件夹")
if dlg.showmodal() == wx.id_ok:
self.target_folder = dlg.getpath()
self.folder_label.setlabel(f"目标文件夹: {self.target_folder}")
self.status.setlabel(f"已选择: {self.target_folder}")
self.save_config() # 保存配置
dlg.destroy()
def on_create_structure(self, event):
if not self.target_folder:
wx.messagebox("请先选择目标文件夹", "错误", wx.ok | wx.icon_error)
return
text = self.memo.getvalue()
lines = text.split('\n')
try:
# 解析并创建文件结构
stack = [(self.target_folder, -1)] # (路径, 层级)
root_created = false
self.created_root_folder = "" # 重置创建的根文件夹路径
for line_num, line in enumerate(lines):
original_line = line
if not line.strip():
continue
# 移除树形符号并获取文件/文件夹名
clean_line = line
# 移除树形字符: ├── └── │ ─
for symbol in ['├──', '└──', '│', '─']:
clean_line = clean_line.replace(symbol, '')
clean_line = clean_line.strip()
if not clean_line:
continue
# 移除注释部分(# 后面的内容)
if '#' in clean_line:
clean_line = clean_line.split('#')[0].strip()
if not clean_line:
continue
# 计算层级
level = 0
for char in original_line:
if char in ' │':
level += 1
else:
break
# 如果是第一行且以/结尾,创建根文件夹
if not root_created and clean_line.endswith('/'):
folder_name = clean_line.rstrip('/')
full_path = os.path.join(self.target_folder, folder_name)
os.makedirs(full_path, exist_ok=true)
stack = [(full_path, 0)]
root_created = true
self.created_root_folder = full_path # 记录根文件夹
continue
# 根据层级找到父目录
while len(stack) > 1 and stack[-1][1] >= level:
stack.pop()
parent_path = stack[-1][0]
if clean_line.endswith('/'):
# 创建文件夹
folder_name = clean_line.rstrip('/')
full_path = os.path.join(parent_path, folder_name)
os.makedirs(full_path, exist_ok=true)
stack.append((full_path, level))
else:
# 创建文件
full_path = os.path.join(parent_path, clean_line)
dir_path = os.path.dirname(full_path)
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path, exist_ok=true)
if not os.path.exists(full_path):
with open(full_path, 'w', encoding='utf-8') as f:
f.write("")
# 刷新树形显示
self.refresh_tree()
self.save_config() # 保存配置
self.status.setlabel("文件结构创建成功")
wx.messagebox("文件结构创建成功!", "成功", wx.ok | wx.icon_information)
except exception as e:
import traceback
error_msg = f"创建失败: {str(e)}\n\n详细信息:\n{traceback.format_exc()}"
wx.messagebox(error_msg, "错误", wx.ok | wx.icon_error)
self.status.setlabel(f"创建失败: {str(e)}")
def refresh_tree(self):
self.tree.deleteallitems()
root = self.tree.addroot("root")
# 如果有创建的根文件夹,显示它;否则显示目标文件夹
display_path = self.created_root_folder if self.created_root_folder else self.target_folder
if display_path and os.path.exists(display_path):
self.tree_root_path = display_path
try:
self.add_tree_nodes(root, display_path, depth=0, max_depth=10)
self.tree.expandall() # 展开所有节点以显示创建的结构
except exception as e:
self.status.setlabel(f"刷新树失败: {str(e)}")
def add_tree_nodes(self, parent, path, depth=0, max_depth=10):
# 限制递归深度,避免死循环
if depth >= max_depth:
return
try:
items = sorted(os.listdir(path))
# 限制每层最多显示100个项目
if len(items) > 100:
items = items[:100]
for item in items:
full_path = os.path.join(path, item)
# 跳过隐藏文件和系统文件
if item.startswith('.') and item not in ['.env', '.gitignore']:
continue
try:
node = self.tree.appenditem(parent, item)
self.tree.setitemdata(node, full_path)
if os.path.isdir(full_path):
# 递归添加子节点
self.add_tree_nodes(node, full_path, depth + 1, max_depth)
except (permissionerror, oserror):
# 跳过无权限访问的文件/文件夹
continue
except (permissionerror, oserror) as e:
# 无法访问该目录,跳过
pass
def on_tree_select(self, event):
item = event.getitem()
if item:
path = self.tree.getitemdata(item)
if path and os.path.isfile(path):
self.current_file_path = path # 记录当前文件路径
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
self.preview.setvalue(content)
except:
self.preview.setvalue("无法预览此文件")
else:
self.current_file_path = ""
self.preview.setvalue("")
def on_scan_files(self, event):
dlg = wx.dirdialog(self, "选择要扫描的文件夹")
if dlg.showmodal() == wx.id_ok:
folder = dlg.getpath()
self.last_scan_folder = folder # 记录最后一次扫描的文件夹
self.save_config() # 保存到配置文件
self.scan_today_files(folder)
dlg.destroy()
# 修复缺失的刷新方法
def on_refresh_scan(self, event):
if self.last_scan_folder and os.path.exists(self.last_scan_folder):
self.scan_today_files(self.last_scan_folder)
self.status.setlabel(f"已刷新扫描: {self.last_scan_folder}")
else:
wx.messagebox("没有记录上次扫描的文件夹,请先点击'扫描文件'", "提示")
# 修复缺失的保存预览方法
def on_save_preview(self, event):
if not self.current_file_path:
wx.messagebox("当前没有打开的文件", "错误")
return
content = self.preview.getvalue()
try:
with open(self.current_file_path, 'w', encoding='utf-8') as f:
f.write(content)
self.status.setlabel(f"已保存修改: {os.path.basename(self.current_file_path)}")
wx.messagebox("文件保存成功!", "成功")
except exception as e:
wx.messagebox(f"保存失败: {str(e)}", "错误")
def scan_today_files(self, folder):
self.listbox1.clear()
today = datetime.now().date()
media_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.mp4', '.avi', '.mp3', '.wav', '.mov'}
try:
for root, dirs, files in os.walk(folder):
for file in files:
full_path = os.path.join(root, file)
ext = os.path.splitext(file)[1].lower()
if ext not in media_extensions:
mod_time = datetime.fromtimestamp(os.path.getmtime(full_path)).date()
if mod_time == today:
self.listbox1.append(full_path)
self.status.setlabel(f"找到 {self.listbox1.getcount()} 个今日文件")
except exception as e:
wx.messagebox(f"扫描失败: {str(e)}", "错误", wx.ok | wx.icon_error)
def on_copy_file(self, event):
# 获取选中的tree文件
tree_item = self.tree.getselection()
if not tree_item or not tree_item.isok():
wx.messagebox("请先在树中选择目标文件", "提示", wx.ok | wx.icon_warning)
return
target_path = self.tree.getitemdata(tree_item)
if not target_path or os.path.isdir(target_path):
wx.messagebox("请选择一个文件而不是文件夹", "提示", wx.ok | wx.icon_warning)
return
# 获取选中的源文件
selection = self.listbox1.getselection()
if selection == wx.not_found:
wx.messagebox("请先在列表中选择源文件", "提示", wx.ok | wx.icon_warning)
return
source_path = self.listbox1.getstring(selection)
try:
shutil.copy2(source_path, target_path)
self.status.setlabel(f"已覆盖: {os.path.basename(target_path)}")
# 刷新预览
with open(target_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(1000)
self.preview.setvalue(content)
except exception as e:
wx.messagebox(f"复制失败: {str(e)}", "错误", wx.ok | wx.icon_error)
def on_open_root(self, event):
open_path = self.created_root_folder if self.created_root_folder else self.tree_root_path
if open_path and os.path.exists(open_path):
if os.name == 'nt': # windows
os.startfile(open_path)
elif os.name == 'posix': # macos/linux
os.system(f'open "{open_path}"' if os.uname().sysname == 'darwin'
else f'xdg-open "{open_path}"')
else:
wx.messagebox("根目录不存在", "错误", wx.ok | wx.icon_error)
def on_submit_note(self, event):
note = self.edit1.getvalue()
if note:
self.listbox2.append(note)
self.edit1.clear()
self.status.setlabel("备注已添加")
def on_note_select(self, event):
selection = self.listbox2.getselection()
if selection != wx.not_found:
text = self.listbox2.getstring(selection)
if wx.theclipboard.open():
wx.theclipboard.setdata(wx.textdataobject(text))
wx.theclipboard.close()
self.status.setlabel("已复制到剪贴板")
if __name__ == '__main__':
app = wx.app()
frame = filestructuremanager()
app.mainloop()
以上就是深度解析如何基于python实现文件结构管理工具的详细内容,更多关于python文件结构管理的资料请关注代码网其它相关文章!
发表评论