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

 更新时间:2026年02月07日 08:51:28   作者:老歌老听老掉牙  
这篇文章主要为大家详细介绍了Python如何结合PyQt5实现一个简单的目录文件扫描与导出工具,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

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

核心功能实现

效果如下

1. 主窗口与界面设计

"""
文件扫描器主程序
功能:选择目录、扫描文件、树形展示、导出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. 文件扫描线程

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. 应用程序入口

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 进行垂直和水平布局,包含以下核心区域:

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

树形结构实现原理

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

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 实现线程间通信:

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

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

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. 添加文件筛选功能

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. 添加右键菜单功能

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. 添加统计信息面板

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")

运行说明

安装依赖

pip install PyQt5

运行程序

python file_scanner.py

使用步骤

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

注意事项

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

总结

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

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

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

以上就是Python结合PyQt5实现目录文件扫描与导出工具的详细内容,更多关于Python目录文件扫描与导出的资料请关注脚本之家其它相关文章!

相关文章

  • python读写修改Excel之xlrd&xlwt&xlutils

    python读写修改Excel之xlrd&xlwt&xlutils

    这篇文章主要介绍了python读写修改Excel之xlrd&xlwt&xlutils,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • 使用Pandas实现数据的清理的入门详解

    使用Pandas实现数据的清理的入门详解

    数据清理是数据分析过程中的关键步骤,它涉及识别缺失值、重复行、异常值和不正确的数据类型,本文将介绍6个经常使用的数据清理操作,希望对大家有所帮助
    2023-08-08
  • 关于Python核心框架tornado的异步协程的2种方法详解

    关于Python核心框架tornado的异步协程的2种方法详解

    今天小编就为大家分享一篇关于Python核心框架tornado的异步协程的2种方法详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-08-08
  • 使用NumPy读取MNIST数据的实现代码示例

    使用NumPy读取MNIST数据的实现代码示例

    这篇文章主要介绍了使用NumPy读取MNIST数据的实现代码示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • python实现进度条的多种实现

    python实现进度条的多种实现

    这篇文章主要介绍了python实现进度条的多种实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • flask实现验证码并验证功能

    flask实现验证码并验证功能

    Flask是一个用Python编写的Web应用程序框架,Flask是python的web框架,最大的特征是轻便,让开发者自由灵活的兼容要开发的feature。这篇文章主要介绍了flask实现验证码并验证,需要的朋友可以参考下
    2019-12-12
  • numpy.random.shuffle打乱顺序函数的实现

    numpy.random.shuffle打乱顺序函数的实现

    这篇文章主要介绍了numpy.random.shuffle打乱顺序函数的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • 在windows系统中实现python3安装lxml

    在windows系统中实现python3安装lxml

    本文主要给大家简单介绍了下在windows以及linux系统中使用Python安装LXML模块的教程,非常简单实用,有需要的小伙伴可以参考下
    2016-03-03
  • Python 四大核心数据结构之列表、字典、元组、集合

    Python 四大核心数据结构之列表、字典、元组、集合

    这段文章详细介绍了Python中的列表、元组、字典和集合四种数据结构的核心特点和适用场景,特别强调了列表的有序可修改、元组的有序不可修改、字典的键值对映射以及集合的自动去重功能,感兴趣的朋友跟随小编一起看看吧
    2026-06-06
  • python pip查看版本、安装、卸载等常用命令总结

    python pip查看版本、安装、卸载等常用命令总结

    pip是一个很方便的工具,可以方便安装,列出,卸载python的模块/库/包等,这篇文章主要介绍了python pip查看版本、安装、卸载等常用命令的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2026-01-01

最新评论