Python+PyQt5 实现目录文件扫描与导出工具

本文将详细介绍如何使用 Python 和 PyQt5 创建一个功能完整的目录文件扫描器。该工具能够选择目录、递归扫描所有文件、以树形结构展示结果,并将文件列表导出为文本文件。

核心功能实现

1. 主窗口与界面设计

python 复制代码
"""
文件扫描器主程序
功能:选择目录、扫描文件、树形展示、导出TXT
作者:智能助手
日期:2024年1月24日
"""

import os
import sys
from pathlib import Path
from datetime import datetime
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt5.QtGui import QFont, QColor, QIcon, QPalette, QLinearGradient
from PyQt5.QtWidgets import *

class FileScannerApp(QMainWindow):
    """主应用程序窗口,负责界面布局和功能协调"""
    
    def __init__(self):
        super().__init__()
        self.current_dir = ""
        self.file_list = []
        self.scanner = None
        self._setup_ui()
        self._setup_styles()
        self.setWindowTitle("文件扫描器")
        self.setGeometry(100, 100, 1000, 700)
        
    def _setup_ui(self):
        """初始化用户界面"""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(10)
        main_layout.setContentsMargins(15, 15, 15, 15)
        
        # 标题
        title_label = QLabel("📁 目录文件扫描器")
        title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;")
        title_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title_label)
        
        # 目录选择区域
        dir_group = QGroupBox("目录设置")
        dir_layout = QHBoxLayout()
        
        self.dir_label = QLabel("未选择目录")
        self.dir_label.setStyleSheet("color: #7f8c8d; padding: 5px; border: 1px solid #ddd; border-radius: 3px;")
        self.dir_label.setMinimumHeight(30)
        
        self.browse_btn = QPushButton("选择目录")
        self.browse_btn.setFixedWidth(100)
        self.browse_btn.clicked.connect(self.select_directory)
        
        dir_layout.addWidget(self.dir_label, 1)
        dir_layout.addWidget(self.browse_btn)
        dir_group.setLayout(dir_layout)
        main_layout.addWidget(dir_group)
        
        # 控制按钮区域
        control_group = QGroupBox("操作控制")
        control_layout = QHBoxLayout()
        
        self.scan_btn = QPushButton("🔍 开始扫描")
        self.scan_btn.setFixedWidth(120)
        self.scan_btn.clicked.connect(self.start_scan)
        self.scan_btn.setEnabled(False)
        
        self.stop_btn = QPushButton("⏹ 停止")
        self.stop_btn.setFixedWidth(100)
        self.stop_btn.clicked.connect(self.stop_scan)
        self.stop_btn.setEnabled(False)
        
        self.export_btn = QPushButton("💾 导出TXT")
        self.export_btn.setFixedWidth(120)
        self.export_btn.clicked.connect(self.export_files)
        self.export_btn.setEnabled(False)
        
        control_layout.addWidget(self.scan_btn)
        control_layout.addWidget(self.stop_btn)
        control_layout.addWidget(self.export_btn)
        control_layout.addStretch()
        control_group.setLayout(control_layout)
        main_layout.addWidget(control_group)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setVisible(False)
        main_layout.addWidget(self.progress_bar)
        
        # 文件树显示
        tree_group = QGroupBox("文件列表")
        tree_layout = QVBoxLayout()
        
        self.file_tree = QTreeWidget()
        self.file_tree.setHeaderLabels(["文件/目录", "路径"])
        self.file_tree.setColumnWidth(0, 300)
        self.file_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
        self.file_tree.header().setSectionResizeMode(1, QHeaderView.Stretch)
        
        tree_layout.addWidget(self.file_tree)
        tree_group.setLayout(tree_layout)
        main_layout.addWidget(tree_group, 1)
        
        # 状态栏
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.status_label = QLabel("就绪")
        self.status_bar.addWidget(self.status_label)
        
    def _setup_styles(self):
        """设置界面样式"""
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f5f5f5;
            }
            QGroupBox {
                font-weight: bold;
                border: 1px solid #ccc;
                border-radius: 5px;
                margin-top: 10px;
                padding-top: 10px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
            }
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                padding: 8px 15px;
                border-radius: 4px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
            QPushButton:disabled {
                background-color: #bdc3c7;
                color: #7f8c8d;
            }
            QTreeWidget {
                background-color: white;
                border: 1px solid #ddd;
                border-radius: 3px;
                font-family: 'Microsoft YaHei', 'Segoe UI';
                font-size: 12px;
            }
            QTreeWidget::item {
                padding: 3px;
            }
            QTreeWidget::item:hover {
                background-color: #ecf0f1;
            }
            QTreeWidget::item:selected {
                background-color: #3498db;
                color: white;
            }
        """)
        
    def select_directory(self):
        """选择目录"""
        dir_path = QFileDialog.getExistingDirectory(
            self, 
            "选择目录",
            str(Path.home()),
            QFileDialog.ShowDirsOnly
        )
        
        if dir_path:
            self.current_dir = dir_path
            self.dir_label.setText(f"已选择: {dir_path}")
            self.scan_btn.setEnabled(True)
            self.clear_file_tree()
            self.status_label.setText("目录已选择,点击开始扫描")
            
    def start_scan(self):
        """开始扫描文件"""
        if not self.current_dir or not os.path.exists(self.current_dir):
            QMessageBox.warning(self, "警告", "请先选择有效的目录")
            return
            
        self.clear_file_tree()
        self.scan_btn.setEnabled(False)
        self.browse_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.export_btn.setEnabled(False)
        
        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)
        
        self.scanner = FileScanner(self.current_dir)
        self.scanner.progress.connect(self.update_progress)
        self.scanner.finished.connect(self.on_scan_finished)
        self.scanner.error.connect(self.on_scan_error)
        self.scanner.start()
        
        self.status_label.setText("正在扫描文件...")
        
    def stop_scan(self):
        """停止扫描"""
        if self.scanner and self.scanner.isRunning():
            self.scanner.stop()
            self.scanner.wait()
            
        self.reset_controls()
        self.status_label.setText("扫描已停止")
        
    def update_progress(self, current, total, filename):
        """更新扫描进度"""
        if total > 0:
            progress = int((current / total) * 100)
            self.progress_bar.setMaximum(total)
            self.progress_bar.setValue(current)
            
            display_name = filename if len(filename) < 40 else f"{filename[:37]}..."
            self.status_label.setText(f"扫描中: {current}/{total} - {display_name}")
            
    def on_scan_finished(self, file_list):
        """扫描完成处理"""
        self.file_list = sorted(file_list)
        self.build_file_tree()
        self.reset_controls()
        self.export_btn.setEnabled(True)
        
        self.status_label.setText(f"扫描完成!共找到 {len(self.file_list)} 个文件")
        QMessageBox.information(self, "完成", f"扫描完成!\n找到 {len(self.file_list)} 个文件")
        
    def on_scan_error(self, error_msg):
        """扫描错误处理"""
        QMessageBox.critical(self, "错误", f"扫描失败: {error_msg}")
        self.reset_controls()
        self.status_label.setText(f"错误: {error_msg}")
        
    def reset_controls(self):
        """重置控件状态"""
        self.scan_btn.setEnabled(True)
        self.browse_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self.progress_bar.setVisible(False)
        
    def clear_file_tree(self):
        """清空文件树"""
        self.file_tree.clear()
        
    def build_file_tree(self):
        """构建文件树结构"""
        if not self.file_list:
            return
            
        root_name = os.path.basename(self.current_dir) or self.current_dir
        root_item = QTreeWidgetItem(self.file_tree, [root_name, self.current_dir])
        root_item.setExpanded(True)
        
        for file_path in self.file_list:
            self.add_to_tree(root_item, file_path)
            
    def add_to_tree(self, parent_item, file_path):
        """添加文件到树中"""
        parts = file_path.split(os.sep)
        current_item = parent_item
        
        for i, part in enumerate(parts):
            found = False
            for j in range(current_item.childCount()):
                child = current_item.child(j)
                if child.text(0) == part:
                    current_item = child
                    found = True
                    break
                    
            if not found:
                full_path = os.path.join(self.current_dir, *parts[:i+1])
                is_file = (i == len(parts) - 1)
                
                new_item = QTreeWidgetItem(current_item, [part, full_path])
                if is_file:
                    new_item.setForeground(0, QColor(0, 128, 0))
                    
                current_item = new_item
                
    def export_files(self):
        """导出文件列表到TXT"""
        if not self.file_list:
            QMessageBox.warning(self, "警告", "没有文件可导出")
            return
            
        default_name = f"文件列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存文件列表",
            str(Path.home() / default_name),
            "文本文件 (*.txt);;所有文件 (*)"
        )
        
        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(f"目录: {self.current_dir}\n")
                    f.write(f"扫描时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                    f.write(f"文件总数: {len(self.file_list)}\n")
                    f.write("=" * 50 + "\n\n")
                    
                    for file in self.file_list:
                        f.write(file + "\n")
                        
                QMessageBox.information(self, "成功", f"文件已保存到:\n{file_path}")
                self.status_label.setText(f"已保存: {os.path.basename(file_path)}")
                
            except Exception as e:
                QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")

2. 文件扫描线程

python 复制代码
class FileScanner(QThread):
    """后台文件扫描线程,避免界面卡顿"""
    
    progress = pyqtSignal(int, int, str)  # 当前进度, 总数, 当前文件
    finished = pyqtSignal(list)  # 文件列表
    error = pyqtSignal(str)  # 错误信息
    
    def __init__(self, root_dir):
        super().__init__()
        self.root_dir = root_dir
        self._running = True
        
    def run(self):
        """线程执行函数"""
        try:
            if not os.path.exists(self.root_dir):
                self.error.emit(f"目录不存在: {self.root_dir}")
                return
                
            file_list = []
            total_files = 0
            
            # 先统计总文件数
            for root, dirs, files in os.walk(self.root_dir):
                if not self._running:
                    return
                total_files += len(files)
                
            processed = 0
            for root, dirs, files in os.walk(self.root_dir):
                if not self._running:
                    return
                    
                for file in files:
                    if not self._running:
                        return
                        
                    try:
                        full_path = os.path.join(root, file)
                        rel_path = os.path.relpath(full_path, self.root_dir)
                        file_list.append(rel_path)
                        
                        processed += 1
                        if total_files > 0:
                            self.progress.emit(processed, total_files, rel_path)
                            
                    except Exception:
                        continue
                        
            if self._running:
                self.finished.emit(file_list)
                
        except Exception as e:
            self.error.emit(str(e))
            
    def stop(self):
        """停止扫描"""
        self._running = False

3. 应用程序入口

python 复制代码
def main():
    """应用程序主函数"""
    app = QApplication(sys.argv)
    app.setApplicationName("文件扫描器")
    app.setFont(QFont("Microsoft YaHei", 10))
    
    window = FileScannerApp()
    window.show()
    
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

代码解析与实现细节

界面布局设计

界面采用 QVBoxLayoutQHBoxLayout 进行垂直和水平布局,包含以下核心区域:

  1. 标题区域:显示应用程序名称
  2. 目录选择区:显示当前选择的目录路径和浏览按钮
  3. 控制按钮区:开始扫描、停止、导出三个主要功能按钮
  4. 进度显示区:显示扫描进度
  5. 文件树展示区:以树形结构展示目录和文件
  6. 状态栏 :显示当前状态信息

树形结构实现原理

文件树的构建采用递归算法,但通过迭代方式实现以避免递归深度限制。对于每个文件的相对路径,按路径分隔符分割,逐级检查并创建树节点:

python 复制代码
def add_to_tree(parent_item, file_path):
    """添加文件到树中的算法实现"""
    parts = file_path.split(os.sep)  # 分割路径
    current_item = parent_item
    
    for i, part in enumerate(parts):
        # 检查是否已存在该节点
        found = False
        for j in range(current_item.childCount()):
            if current_item.child(j).text(0) == part:
                current_item = current_item.child(j)
                found = True
                break
                
        # 创建新节点
        if not found:
            is_file = (i == len(parts) - 1)
            new_item = QTreeWidgetItem(current_item, [part, ""])
            if is_file:
                new_item.setForeground(0, QColor(0, 128, 0))
            current_item = new_item

多线程处理机制

扫描过程在独立线程中执行,避免界面卡顿。使用 QThreadpyqtSignal 实现线程间通信:

  1. 进度信号:实时更新扫描进度
  2. 完成信号:扫描完成后传输文件列表
  3. 错误信号:传递扫描过程中的异常信息

线程安全的停止机制通过标志变量控制:

python 复制代码
def stop(self):
    """安全的线程停止方法"""
    self._running = False
    
def run(self):
    """线程运行循环"""
    while self._running and not_finished:
        # 扫描逻辑...

文件导出格式

生成的 TXT 文件包含标准化格式:

复制代码
目录: /path/to/directory
扫描时间: 2024-01-24 10:30:00
文件总数: 1234
==================================================

folder1/file1.txt
folder1/file2.py
folder2/subfolder/document.docx
...

扩展功能建议

1. 添加文件筛选功能

python 复制代码
class FilterDialog(QDialog):
    """文件筛选对话框"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("文件筛选")
        layout = QVBoxLayout()
        
        # 扩展名筛选
        ext_layout = QHBoxLayout()
        ext_label = QLabel("扩展名:")
        self.ext_input = QLineEdit()
        self.ext_input.setPlaceholderText("例如: txt,py,jpg (逗号分隔)")
        ext_layout.addWidget(ext_label)
        ext_layout.addWidget(self.ext_input, 1)
        
        # 大小筛选
        size_layout = QHBoxLayout()
        size_label = QLabel("最小大小(KB):")
        self.min_size = QSpinBox()
        self.min_size.setRange(0, 999999)
        size_layout.addWidget(size_label)
        size_layout.addWidget(self.min_size)
        
        layout.addLayout(ext_layout)
        layout.addLayout(size_layout)
        
        # 按钮
        btn_layout = QHBoxLayout()
        ok_btn = QPushButton("确定")
        cancel_btn = QPushButton("取消")
        ok_btn.clicked.connect(self.accept)
        cancel_btn.clicked.connect(self.reject)
        btn_layout.addWidget(ok_btn)
        btn_layout.addWidget(cancel_btn)
        
        layout.addLayout(btn_layout)
        self.setLayout(layout)

2. 添加右键菜单功能

python 复制代码
class CustomTreeWidget(QTreeWidget):
    """自定义树控件,添加右键菜单"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        
    def show_context_menu(self, position):
        """显示右键菜单"""
        menu = QMenu()
        
        open_action = QAction("打开文件", self)
        open_folder_action = QAction("打开所在文件夹", self)
        copy_path_action = QAction("复制路径", self)
        
        menu.addAction(open_action)
        menu.addAction(open_folder_action)
        menu.addSeparator()
        menu.addAction(copy_path_action)
        
        menu.exec_(self.viewport().mapToGlobal(position))

3. 添加统计信息面板

python 复制代码
class StatsPanel(QWidget):
    """统计信息面板"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout()
        
        # 文件类型统计
        self.type_stats = {}
        self.type_label = QLabel("文件类型: 0")
        
        # 大小统计
        self.total_size = 0
        self.size_label = QLabel("总大小: 0 B")
        
        # 时间统计
        self.scan_time = 0
        self.time_label = QLabel("扫描时间: 0s")
        
        layout.addWidget(self.type_label)
        layout.addWidget(self.size_label)
        layout.addWidget(self.time_label)
        self.setLayout(layout)
        
    def update_stats(self, file_list, scan_time):
        """更新统计信息"""
        # 计算文件类型分布
        self.type_stats.clear()
        for file in file_list:
            ext = os.path.splitext(file)[1].lower()
            self.type_stats[ext] = self.type_stats.get(ext, 0) + 1
            
        # 计算总大小
        self.total_size = 0
        
        # 更新显示
        self.type_label.setText(f"文件类型: {len(self.type_stats)} 种")
        self.time_label.setText(f"扫描时间: {scan_time:.2f}s")

运行说明

安装依赖

bash 复制代码
pip install PyQt5

运行程序

bash 复制代码
python file_scanner.py

使用步骤

  1. 点击"选择目录"按钮,选择要扫描的目录
  2. 点击"开始扫描"按钮,开始扫描文件
  3. 在树形结构中浏览扫描结果
  4. 点击"导出TXT"按钮,保存文件列表
  5. 可随时点击"停止"按钮中断扫描

注意事项

  1. 权限问题:某些系统目录可能需要管理员权限才能访问
  2. 符号链接:当前版本不处理符号链接,避免无限循环
  3. 大目录处理:扫描非常大的目录时可能需要较长时间
  4. 内存使用:扫描结果完全存储在内存中,超大型目录需注意内存限制
  5. 编码问题:确保系统支持文件的字符编码

总结

本文实现了一个功能完整的目录文件扫描器,具有以下特点:

  1. 直观的图形界面:基于 PyQt5 的现代化界面
  2. 高效的扫描机制:多线程后台扫描,不阻塞界面
  3. 清晰的树形展示:层次化的文件结构展示
  4. 完整的导出功能:标准化的 TXT 文件输出
  5. 良好的用户体验:进度显示、状态反馈、错误处理

该工具适用于文件整理、项目分析、数据备份等多种场景,可有效提高文件管理效率。通过模块化设计和清晰的代码结构,便于进一步的功能扩展和维护。

相关推荐
七夜zippoe2 小时前
HTTP协议深度解析与实现:从请求响应到HTTP/3的完整指南
python·网络协议·http·quic·帧结构
电化学仪器白超2 小时前
③YT讨论
开发语言·python·单片机·嵌入式硬件
b2077212 小时前
Flutter for OpenHarmony 身体健康状况记录App实战 - 数据导出实现
python·flutter·harmonyos
licheng99672 小时前
工具、测试与部署
jvm·数据库·python
訫悦2 小时前
体验在Qt中简单使用C++20的协程
qt·c++20·协程
xmRao2 小时前
Qt+FFmpeg 实现 PCM 转 WAV
qt·ffmpeg·pcm
小二·3 小时前
Python Web 开发进阶实战:AI 原生安全防护 —— 在 Flask + Suricata 中构建智能网络威胁狩猎平台
前端·人工智能·python
yuankoudaodaokou3 小时前
精准与高效:3D扫描技术如何重塑康复辅具设计与制造
python·3d·制造
嫂子开门我是_我哥3 小时前
第十七节:项目实战1:猜数字游戏(模块化开发实现)
开发语言·python·算法·游戏