python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python目录文件扫描与导出

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

作者:老歌老听老掉牙

这篇文章主要为大家详细介绍了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

使用步骤

注意事项

总结

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

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

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

您可能感兴趣的文章:
阅读全文