核心功能
1. 目录结构检索
-
递归扫描 :深度遍历指定目录及其所有子目录
-
多种检索模式 :
- 仅文件夹模式:只显示目录结构
- 仅文件模式:只显示文件列表
- 文件+文件夹模式:完整显示目录树结构(默认模式)
2. 智能过滤系统
-
文件后缀过滤 :支持多后缀过滤(如:.txt; .py; .jpg)
-
屏蔽词管理 :
- 支持通配符(如 .tmp; backup_ )
- 可创建和管理多个屏蔽配置
- 支持导入/导出配置
3. 结果输出
-
树形结构展示 :直观显示目录层级关系
-
可视化标识 :
-
统计信息 :自动生成项目数量统计
-
结果导出 :一键导出为文本文件
特色功能
4. 用户友好界面
- 直观操作 :清晰的按钮布局和分组
- 深色主题 :减轻视觉疲劳
- 实时状态提示 :显示当前操作状态
- 智能路径建议 :自动生成默认输出路径
5. 配置管理
- 配置文件存储 :配置文件保存在程序同目录
- 多配置支持 :可创建和管理多个屏蔽配置
- 配置导出 :支持将配置导出为JSON文件
6. 高效性能
- 快速扫描 :优化递归算法提高效率
- 错误处理 :自动跳过无权限访问的目录
- 排序功能 :文件和文件夹按名称排序
使用场景
- 项目结构分析 :快速查看项目目录结构
- 文件系统清理 :识别特定类型的文件(如临时文件)
- 文档编制 :生成项目目录树文档
- 资产盘点 :统计特定类型的文件数量
- 系统维护 :查找分散的配置文件或日志文件
已编译好的工具: https://pan.quark.cn/s/ce0c21b939b6
附源代码
import os
import sys
import re
import json
import fnmatch
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,QPushButton, QRadioButton, QButtonGroup, QGroupBox, QFileDialog, QTextEdit,QComboBox, QMessageBox, QCheckBox, QListWidget, QListWidgetItem, QInputDialog
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QPalette, QColor# 获取脚本所在目录
if getattr(sys, 'frozen', False):# 如果是打包后的可执行文件APP_DIR = os.path.dirname(sys.executable)
else:# 如果是脚本文件APP_DIR = os.path.dirname(os.path.abspath(__file__))# 修改配置文件路径为脚本所在目录
CONFIG_FILE = os.path.join(APP_DIR, "config.json")class FileSearchApp(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("目录/文件递归检索工具")self.setGeometry(300, 300, 800, 650)# 初始化变量self.ignore_configs = []self.current_ignore_config = {"name": "默认配置", "patterns": []}self.load_config()# 创建UIself.init_ui()class FileSearchApp(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("目录/文件递归检索工具 V1.0 by:Sunf10wer")self.setGeometry(300, 300, 800, 650)# 初始化变量self.ignore_configs = []self.current_ignore_config = {"name": "默认配置", "patterns": []}self.load_config()# 创建UIself.init_ui()def init_ui(self):# 主布局main_widget = QWidget()main_layout = QVBoxLayout()main_widget.setLayout(main_layout)self.setCentralWidget(main_widget)# 设置标题样式title_label = QLabel("目录/文件递归检索工具")title_font = QFont("Arial", 16, QFont.Bold)title_label.setFont(title_font)title_label.setAlignment(Qt.AlignCenter)title_label.setStyleSheet("color: #FFFFFF; padding: 10px;")main_layout.addWidget(title_label)# 目录选择dir_layout = QHBoxLayout()dir_label = QLabel("目标目录:")self.dir_entry = QLineEdit()self.dir_entry.setPlaceholderText("请选择或输入要检索的目录...")browse_button = QPushButton("浏览...")browse_button.clicked.connect(self.browse_directory)dir_layout.addWidget(dir_label)dir_layout.addWidget(self.dir_entry, 4)dir_layout.addWidget(browse_button, 1)main_layout.addLayout(dir_layout)# 输出文件选择output_layout = QHBoxLayout()output_label = QLabel("输出文件:")self.output_entry = QLineEdit()self.output_entry.setPlaceholderText("输出文件名...")output_browse_button = QPushButton("浏览...")output_browse_button.clicked.connect(self.browse_output_file)output_layout.addWidget(output_label)output_layout.addWidget(self.output_entry, 4)output_layout.addWidget(output_browse_button, 1)main_layout.addLayout(output_layout)# 检索类型选择search_type_group = QGroupBox("检索类型")search_layout = QHBoxLayout()self.folder_radio = QRadioButton("仅文件夹")self.file_radio = QRadioButton("仅文件")self.both_radio = QRadioButton("文件和文件夹")self.both_radio.setChecked(True)self.search_type_group = QButtonGroup()self.search_type_group.addButton(self.folder_radio)self.search_type_group.addButton(self.file_radio)self.search_type_group.addButton(self.both_radio)# 文件后缀过滤suffix_layout = QHBoxLayout()suffix_label = QLabel("文件后缀(用分号分隔):")self.suffix_entry = QLineEdit()self.suffix_entry.setPlaceholderText("例如: .txt; .py; .jpg")suffix_layout.addWidget(suffix_label)suffix_layout.addWidget(self.suffix_entry)search_layout.addWidget(self.folder_radio)search_layout.addWidget(self.file_radio)search_layout.addWidget(self.both_radio)search_layout.addStretch()search_type_group.setLayout(search_layout)main_layout.addWidget(search_type_group)main_layout.addLayout(suffix_layout)# 屏蔽词管理ignore_group = QGroupBox("屏蔽词管理")ignore_layout = QVBoxLayout()# 屏蔽词配置选择config_layout = QHBoxLayout()config_label = QLabel("当前配置:")self.config_combo = QComboBox()self.config_combo.setMinimumWidth(150)self.config_combo.currentIndexChanged.connect(self.config_selected)new_config_btn = QPushButton("新建配置")new_config_btn.clicked.connect(self.create_new_config)config_layout.addWidget(config_label)config_layout.addWidget(self.config_combo, 1)config_layout.addWidget(new_config_btn)# 屏蔽词列表ignore_list_layout = QVBoxLayout()list_label = QLabel("屏蔽词列表(支持通配符,如 *.tmp; backup_*)")self.ignore_list = QListWidget()self.ignore_list.setAlternatingRowColors(True)add_btn = QPushButton("添加屏蔽词")add_btn.clicked.connect(self.add_ignore_pattern)remove_btn = QPushButton("移除选中")remove_btn.clicked.connect(self.remove_selected_pattern)list_btn_layout = QHBoxLayout()list_btn_layout.addWidget(add_btn)list_btn_layout.addWidget(remove_btn)ignore_list_layout.addWidget(list_label)ignore_list_layout.addWidget(self.ignore_list)ignore_list_layout.addLayout(list_btn_layout)ignore_layout.addLayout(config_layout)ignore_layout.addLayout(ignore_list_layout)ignore_group.setLayout(ignore_layout)main_layout.addWidget(ignore_group)# 操作按钮button_layout = QHBoxLayout()self.search_btn = QPushButton("开始检索")self.search_btn.setStyleSheet("background-color: #3498db; color: white; font-weight: bold; padding: 8px;")self.search_btn.clicked.connect(self.start_search)export_btn = QPushButton("导出配置")export_btn.clicked.connect(self.export_config)button_layout.addStretch()button_layout.addWidget(self.search_btn)button_layout.addWidget(export_btn)button_layout.addStretch()main_layout.addLayout(button_layout)# 状态栏self.status_bar = self.statusBar()self.status_label = QLabel("就绪")self.status_bar.addWidget(self.status_label)# 更新UIself.update_config_combo()self.update_ignore_list()# 连接信号self.dir_entry.textChanged.connect(self.update_output_filename)def load_config(self):"""从配置文件加载屏蔽词配置"""if os.path.exists(CONFIG_FILE):try:with open(CONFIG_FILE, 'r', encoding='utf-8') as f:self.ignore_configs = json.load(f)# 确保至少有一个默认配置if not any(cfg['name'] == '默认配置' for cfg in self.ignore_configs):self.ignore_configs.insert(0, {"name": "默认配置", "patterns": []})except:self.ignore_configs = [{"name": "默认配置", "patterns": []}]else:self.ignore_configs = [{"name": "默认配置", "patterns": []}]self.current_ignore_config = self.ignore_configs[0]def save_config(self):"""保存屏蔽词配置到文件"""try:with open(CONFIG_FILE, 'w', encoding='utf-8') as f:json.dump(self.ignore_configs, f, ensure_ascii=False, indent=2)return Trueexcept Exception as e:QMessageBox.critical(self, "保存错误", f"保存配置时出错: {str(e)}")return Falsedef update_config_combo(self):"""更新配置下拉框"""self.config_combo.clear()for config in self.ignore_configs:self.config_combo.addItem(config['name'])# 选择当前配置current_index = next((i for i, config in enumerate(self.ignore_configs) if config['name'] == self.current_ignore_config['name']),0)self.config_combo.setCurrentIndex(current_index)def update_ignore_list(self):"""更新屏蔽词列表"""self.ignore_list.clear()for pattern in self.current_ignore_config['patterns']:self.ignore_list.addItem(pattern)def config_selected(self, index):"""配置选择改变事件"""if 0 <= index < len(self.ignore_configs):self.current_ignore_config = self.ignore_configs[index]self.update_ignore_list()def create_new_config(self):"""创建新的屏蔽词配置"""name, ok = QInputDialog.getText(self, "新建配置", "输入配置名称:", text=f"配置{len(self.ignore_configs)+1}")if ok and name:# 检查名称是否已存在if any(cfg['name'] == name for cfg in self.ignore_configs):QMessageBox.warning(self, "名称冲突", f"配置名 '{name}' 已存在!")returnnew_config = {"name": name, "patterns": []}self.ignore_configs.append(new_config)self.current_ignore_config = new_configself.update_config_combo()self.update_ignore_list()self.save_config()def add_ignore_pattern(self):"""添加屏蔽词"""pattern, ok = QInputDialog.getText(self, "添加屏蔽词", "请输入屏蔽词(支持通配符):")if ok and pattern:if pattern not in self.current_ignore_config['patterns']:self.current_ignore_config['patterns'].append(pattern)self.update_ignore_list()self.save_config()def remove_selected_pattern(self):"""移除选中的屏蔽词"""selected_items = self.ignore_list.selectedItems()if not selected_items:returnfor item in selected_items:pattern = item.text()if pattern in self.current_ignore_config['patterns']:self.current_ignore_config['patterns'].remove(pattern)self.update_ignore_list()self.save_config()def browse_directory(self):"""浏览目录"""directory = QFileDialog.getExistingDirectory(self, "选择目录")if directory:self.dir_entry.setText(directory)self.update_output_filename()def browse_output_file(self):"""浏览输出文件"""# 获取默认输出路径default_path = self.output_entry.text() or self.get_default_output_path()# 确保默认路径包含文件名if os.path.isdir(default_path):default_path = os.path.join(default_path, self.get_default_filename())# 打开文件对话框file_path, _ = QFileDialog.getSaveFileName(self, "保存结果", default_path, "文本文件 (*.txt)")if file_path:# 确保文件扩展名正确if not file_path.endswith('.txt'):file_path += '.txt'self.output_entry.setText(file_path)def get_default_output_path(self):"""获取默认输出目录"""# 优先使用目标目录所在位置if self.dir_entry.text():return os.path.dirname(self.dir_entry.text())# 使用当前工作目录作为备选return os.getcwd()def update_output_filename(self):"""当目录改变时更新默认输出文件名"""# 仅当输出框为空时更新if not self.output_entry.text():self.output_entry.setText(self.get_default_output_path())def get_default_filename(self):"""获取默认文件名"""directory = self.dir_entry.text()if directory:# 获取目录名作为文件名基础base_name = os.path.basename(directory) or "root"return f"{base_name}_检索结果.txt"return "检索结果.txt"def export_config(self):"""导出当前配置"""if not self.current_ignore_config['patterns']:QMessageBox.information(self, "导出配置", "当前配置没有屏蔽词!")returnfile_path, _ = QFileDialog.getSaveFileName(self, "导出配置", "", "JSON文件 (*.json)")if file_path:if not file_path.endswith('.json'):file_path += '.json'try:with open(file_path, 'w', encoding='utf-8') as f:json.dump(self.current_ignore_config, f, ensure_ascii=False, indent=2)QMessageBox.information(self, "导出成功", f"配置已导出到:\n{file_path}")except Exception as e:QMessageBox.critical(self, "导出错误", f"导出配置时出错: {str(e)}")def start_search(self):"""开始检索"""# 获取输入参数target_dir = self.dir_entry.text().strip()if not target_dir or not os.path.isdir(target_dir):QMessageBox.warning(self, "目录错误", "请选择有效的目标目录!")return# 获取输出文件路径output_file = self.output_entry.text().strip()if not output_file:# 如果没有指定输出文件,使用默认文件名output_file = os.path.join(self.get_default_output_path(),self.get_default_filename())self.output_entry.setText(output_file)if not output_file.endswith('.txt'):output_file += '.txt'# 确保输出目录存在output_dir = os.path.dirname(output_file)if output_dir and not os.path.exists(output_dir):try:os.makedirs(output_dir)except Exception as e:QMessageBox.critical(self, "目录错误", f"无法创建输出目录: {str(e)}")return# 检查检索类型search_folders = self.folder_radio.isChecked()search_files = self.file_radio.isChecked()search_both = self.both_radio.isChecked()file_extensions = []if search_files or search_both:ext_str = self.suffix_entry.text().strip()if ext_str:file_extensions = [ext.strip().lower() for ext in ext_str.split(';') if ext.strip()]# 获取屏蔽词ignore_patterns = self.current_ignore_config['patterns']# 执行检索self.status_label.setText("正在检索...")QApplication.processEvents() # 更新UItry:# 获取层级结构的检索结果results = []self.recursive_traverse(target_dir, target_dir, results, 0,search_folders, search_files,search_both,file_extensions, ignore_patterns)# 写入文件with open(output_file, 'w', encoding='utf-8') as f:# 写入头部信息f.write(f"检索目录: {target_dir}\n")if search_folders:f.write(f"检索类型: 仅文件夹\n")elif search_files:f.write(f"检索类型: 仅文件\n")else:f.write(f"检索类型: 文件和文件夹\n")if (search_files or search_both) and file_extensions:f.write(f"文件后缀: {', '.join(file_extensions)}\n")if ignore_patterns:f.write(f"屏蔽配置: {self.current_ignore_config['name']}\n")f.write(f"屏蔽词: {', '.join(ignore_patterns)}\n")f.write("\n" + "=" * 70 + "\n\n")# 写入层级结构total_items = 0total_folders = 0total_files = 0for item in results:# 根据层级深度添加缩进indent = "│ " * (item['depth'] - 1)prefix = "├── " if item['depth'] > 0 else ""# 添加文件夹/文件标识if item['type'] == 'folder':line = f"{indent}{prefix}📁 {item['name']}/"total_folders += 1else:line = f"{indent}{prefix}📄 {item['name']}"total_files += 1f.write(line + "\n")total_items += 1# 添加统计信息f.write("\n" + "=" * 70 + "\n\n")f.write(f"统计信息:\n")f.write(f"总项目数: {total_items}\n")f.write(f"文件夹数: {total_folders}\n")f.write(f"文件数: {total_files}\n")self.status_label.setText(f"检索完成!找到 {total_items} 个项目,结果已保存到: {output_file}")QMessageBox.information(self, "完成", f"检索完成!\n"f"总项目数: {total_items}\n"f"文件夹数: {total_folders}\n"f"文件数: {total_files}\n"f"结果已保存到:\n{output_file}")except Exception as e:self.status_label.setText("检索出错")QMessageBox.critical(self, "错误", f"检索过程中出错: {str(e)}")def recursive_traverse(self, root_dir, current_dir, results, depth, search_folders, search_files, search_both,file_extensions, ignore_patterns):"""递归遍历目录,保持实际目录结构"""try:# 获取当前目录下的条目entries = os.listdir(current_dir)except Exception as e:# 跳过无权访问的目录return# 排序条目entries.sort(key=lambda s: s.lower())# 获取当前目录相对于根目录的相对路径rel_dir = os.path.relpath(current_dir, root_dir)# 如果是根目录,添加根目录项if current_dir == root_dir:results.append({'name': os.path.basename(root_dir) or os.path.splitdrive(root_dir)[0],'path': root_dir,'depth': depth,'type': 'folder'})# 处理文件夹folders = [e for e in entries if os.path.isdir(os.path.join(current_dir, e))]for folder in folders:folder_path = os.path.join(current_dir, folder)rel_path = os.path.relpath(folder_path, root_dir)# 检查是否在屏蔽列表中if self.is_ignored(rel_path, ignore_patterns):continue# 添加到结果(如果需要检索文件夹)if search_folders or search_both:results.append({'name': folder,'path': folder_path,'depth': depth + 1,'type': 'folder'})# 递归处理子目录self.recursive_traverse(root_dir, folder_path, results, depth + 1,search_folders, search_files,search_both,file_extensions, ignore_patterns)# 处理文件files = [e for e in entries if os.path.isfile(os.path.join(current_dir, e))]for file in files:file_path = os.path.join(current_dir, file)rel_path = os.path.relpath(file_path, root_dir)# 检查是否在屏蔽列表中if self.is_ignored(rel_path, ignore_patterns):continue# 检查文件后缀if (search_files or search_both) and file_extensions:ext = os.path.splitext(file)[1].lower()if ext not in file_extensions:continue# 添加到结果if search_files or search_both:results.append({'name': file,'path': file_path,'depth': depth + 1,'type': 'file'})def is_ignored(self, path, patterns):"""检查路径是否与任何屏蔽模式匹配"""for pattern in patterns:if fnmatch.fnmatch(path, pattern):return Trueif pattern in path:return Truereturn Falseif __name__ == "__main__":app = QApplication([])app.setStyle("Fusion")# 设置应用样式palette = QPalette()palette.setColor(QPalette.Window, QColor(53, 53, 53))palette.setColor(QPalette.WindowText, Qt.white)palette.setColor(QPalette.Base, QColor(35, 35, 35))palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))palette.setColor(QPalette.ToolTipBase, Qt.white)palette.setColor(QPalette.ToolTipText, Qt.white)palette.setColor(QPalette.Text, Qt.white)palette.setColor(QPalette.Button, QColor(53, 53, 53))palette.setColor(QPalette.ButtonText, Qt.white)palette.setColor(QPalette.BrightText, Qt.red)palette.setColor(QPalette.Highlight, QColor(142, 45, 197).lighter())palette.setColor(QPalette.HighlightedText, Qt.black)app.setPalette(palette)window = FileSearchApp()window.show()app.exec_()
文件递归检索工具依赖库
PyQt5==5.15.9
pip install PyQt5
pyinstaller --onefile -w so.py