自动将文件按类型分类到对应文件夹的小工具

查看 5|回复 0
作者:xiaomizha   
xuyou-file-classifier
这是一个自动将文件按类型分类到对应文件夹的小工具
项目背景: 原有的某个项目,需要将文件分门别类,灵光一现,它灵光一现了
Github
https://github.com/xuyouer/xuyou-file-classifier
TODO
  • ✅ 可自选输出分类路径(默认路径: f"./{日期+唯一ID}分类")
  • ✅ 分类完成后,继续分类将排除输出分类路径,防止二次重复
  • ✅ 可选分类时是否保持原有的子目录结构
  • ✅ 可选分类后是否保留原有的文件夹(未成功分类的除外)
  •  分类规则存放云端(Github/……),本地可联网下载使用(视情况决定后续版本是否更新此内容)

    TIPS
    注意
    [ol]
  • 内置的默认分类规则可能部分不符(直接使用的百度搜索结果),如有不符或需要扩展内置请指出,感谢
    [/ol]
    版本日志
    v1.0.1
    更新 [FEAT]:
    [ol]
  • 所有设置存放SettingsManager进行管理
  • 部分功能
    [/ol]
    修复 [FIX]:
    [ol]
  • 部分不合理的分类规则
    [/ol]
    v1.0.0
    贡献
    欢迎提交Issue和Pull Request来帮助改进这个工具。
    许可证
    本项目采用MIT许可证 - 详见LICENSE文件。
    个人开发,贴图如下:



    部分代码(篇幅有限,详细代码见底部源码):
    """
    文件自动分类器
    功能: 自动把文件分类到对应的文件夹下
    版本: 1.0.1
    作者: xuyou & xiaomizha
    """
    import os
    import sys
    import json
    import yaml
    import shutil
    import webbrowser
    import uuid
    from datetime import datetime
    from pathlib import Path
    from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
                                 QWidget, QPushButton, QLabel, QLineEdit, QTextEdit,
                                 QFileDialog, QMessageBox, QProgressBar, QGroupBox,
                                 QCheckBox, QScrollArea, QMenuBar, QAction,
                                 QFrame, QSplitter, QTabWidget, QSpinBox, QComboBox,
                                 QDialog, QDialogButtonBox, QGridLayout, QFormLayout)
    from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
    from PyQt5.QtGui import QFont, QIcon, QPixmap, QPainter, QCursor
    class SettingsManager:
        """设置管理器"""
        def __init__(self):
            self.settings = QSettings("FileClassifier", "Settings")
            self._load_default_settings()
        def _load_default_settings(self):
            """加载默认设置"""
            self.default_settings = {
                # 常规设置
                'auto_save': True,
                'confirm_action': True,
                'log_level': '详细',
                # 界面设置
                'theme': '默认',
                'font_size': 10,
                # 分类设置
                'default_output_pattern': './{date}_{id}_分类',
                'move_files': True,
                'recursive': False,
                'preserve_structure': True,
                'remove_empty_folders': False,
                'exclude_output_dirs': True,
                # 高级设置
                'backup_rules': True,
                'max_log_lines': 1000,
            }
        def get(self, key, default=None):
            """获取设置值"""
            if default is None:
                default = self.default_settings.get(key)
            return self.settings.value(key, default)
        def set(self, key, value):
            """设置值"""
            self.settings.setValue(key, value)
        def get_bool(self, key):
            """获取布尔值设置"""
            return self.settings.value(key, self.default_settings.get(key, False), type=bool)
        def get_int(self, key):
            """获取整数值设置"""
            return self.settings.value(key, self.default_settings.get(key, 0), type=int)
        def reset_to_defaults(self):
            """重置为默认设置"""
            self.settings.clear()
            for key, value in self.default_settings.items():
                self.settings.setValue(key, value)
    class QCollapsibleGroupBox(QGroupBox):
        """可折叠的GroupBox"""
        ...
    class SettingsDialog(QDialog):
        """设置对话框"""
        ...
    class AboutDialog(QDialog):
        """关于对话框"""
        ...
    class RuleEditDialog(QDialog):
        """规则编辑对话框"""
        ...
    class FileClassifier:
        """文件分类器核心类"""
        def __init__(self, settings_manager, log_callback=None):
            self.settings_manager = settings_manager
            self.config_file = "classifier_rules.yaml"
            self.backup_dir = "rule_backups"
            # 禁用分类
            self.disabled_categories = set()
            # 记录输出目录历史
            self.output_dirs_history = set()
            # 回调函数
            self.log_callback = log_callback
            # 默认分类规则
            self.default_categories = {
                ...
            }
            self.categories = self.default_categories.copy()
            self.load_rules()
        def _log(self, message):
            ...
        def generate_output_path(self, base_dir, pattern=None):
            """生成输出路径"""
            ...
        def is_output_directory(self, path):
            """检查路径是否为输出目录"""
            ...
        def toggle_category(self, category, enabled):
            """切换分类的启用状态"""
            ...
        def backup_rules(self):
            """备份规则文件"""
            ...
        def load_rules(self):
            """从YAML文件加载规则"""
            ...
        def save_rules(self):
            """保存规则到YAML文件"""
            ...
        def add_category_rule(self, extension, category):
            """添加新的分类规则"""
            ...
        def remove_category_rule(self, extension):
            """移除分类规则"""
            ...
        def get_file_category(self, filename):
            """根据文件名获取分类"""
            ...
        def remove_empty_folders(self, path):
            """递归删除空文件夹"""
            ...
        def classify_files(self, src_dir, output_dir=None, move_files=True, callback=None,
                           recursive=False, preserve_structure=None):
            """
            分类文件
            Args:
                src_dir: 源目录
                output_dir: 输出目录, 如果为None则自动生成
                move_files: True移动文件, False复制文件
                callback: 进度回调函数
                recursive: 是否递归处理子文件夹
                preserve_structure: 是否保持目录结构
            Returns:
                tuple: (成功数量, 失败列表, 总数量, 输出目录)
            """
            if not os.path.exists(src_dir):
                raise ValueError(f"目录不存在: {src_dir}")
            # 生成输出目录
            if output_dir is None:
                output_dir = self.generate_output_path(src_dir)
            # 确保输出目录存在
            os.makedirs(output_dir, exist_ok=True)
            if preserve_structure is None:
                preserve_structure = self.settings_manager.get_bool('preserve_structure')
            # 获取所有文件
            all_files = []
            if recursive:
                # 递归获取所有文件
                for root, dirs, files in os.walk(src_dir):
                    # 跳过输出目录
                    if self.is_output_directory(root):
                        continue
                    for file in files:
                        file_path = os.path.join(root, file)
                        # 存储相对于源目录的路径信息
                        rel_path = os.path.relpath(file_path, src_dir)
                        all_files.append((file_path, file, rel_path))
            else:
                # 只获取当前目录的文件
                for f in os.listdir(src_dir):
                    file_path = os.path.join(src_dir, f)
                    if os.path.isfile(file_path) and not self.is_output_directory(file_path):
                        all_files.append((file_path, f, f))
            total_files = len(all_files)
            success_count = 0
            failed_files = []
            for i, (file_path, filename, rel_path) in enumerate(all_files):
                try:
                    # 获取文件分类
                    category = self.get_file_category(filename)
                    if category:
                        # 创建分类目录
                        category_dir = os.path.join(output_dir, category)
                        os.makedirs(category_dir, exist_ok=True)
                        # 构建目标文件路径
                        if preserve_structure and recursive:
                            # 保持原有的子目录结构
                            rel_dir = os.path.dirname(rel_path)
                            if rel_dir and rel_dir != '.':
                                target_subdir = os.path.join(category_dir, rel_dir)
                                os.makedirs(target_subdir, exist_ok=True)
                                dst_file = os.path.join(target_subdir, filename)
                            else:
                                dst_file = os.path.join(category_dir, filename)
                        else:
                            dst_file = os.path.join(category_dir, filename)
                        # 处理文件名冲突
                        counter = 1
                        base_name, ext = os.path.splitext(filename)
                        original_dst = dst_file
                        while os.path.exists(dst_file):
                            new_filename = f"{base_name}_{counter}{ext}"
                            # dst_file = os.path.join(category_dir, new_filename)
                            dst_file = os.path.join(os.path.dirname(original_dst), new_filename)
                            counter += 1
                        # 移动或复制文件
                        if move_files:
                            shutil.move(file_path, dst_file)
                        else:
                            shutil.copy2(file_path, dst_file)
                        success_count += 1
                    else:
                        failed_files.append(f"无法识别: {rel_path}")
                except Exception as e:
                    failed_files.append(f"{rel_path}: {str(e)}")
                # 调用进度回调
                if callback:
                    callback(i + 1, total_files, rel_path)
            # 清理空文件夹
            if move_files and self.settings_manager.get_bool('remove_empty_folders'):
                self.remove_empty_folders(src_dir)
            return success_count, failed_files, total_files, output_dir
    class ClassificationThread(QThread):
        """文件分类线程"""
        progress_updated = pyqtSignal(int, int, str)  # 当前进度, 总数, 当前文件
        classification_finished = pyqtSignal(int, list, int, str)  # 成功数, 失败列表, 总数, 输出目录
        def __init__(self, classifier, src_dir, output_dir=None, move_files=True,
                     recursive=False, preserve_structure=None):
            super().__init__()
            self.classifier = classifier
            self.src_dir = src_dir
            self.output_dir = output_dir
            self.move_files = move_files
            self.recursive = recursive
            self.preserve_structure = preserve_structure
        def run(self):
            """运行分类任务"""
            try:
                success_count, failed_files, total_files, output_dir = self.classifier.classify_files(
                    self.src_dir,
                    output_dir=self.output_dir,
                    move_files=self.move_files,
                    callback=self.progress_callback,
                    recursive=self.recursive,
                    preserve_structure=self.preserve_structure
                )
                self.classification_finished.emit(success_count, failed_files, total_files, output_dir)
            except Exception as e:
                self.classification_finished.emit(0, [f"错误: {str(e)}"], 0)
        def progress_callback(self, current, total, filename):
            """进度回调"""
            self.progress_updated.emit(current, total, filename)
    class FileClassifierGUI(QMainWindow):
        """文件分类器GUI主窗口"""
        def __init__(self):
            ...
        def center_window(self):
            """将窗口居中显示"""
            ...
        def init_ui(self):
            """初始化用户界面"""
            ...
        def init_basic_tab(self):
            """初始化基本操作选项卡"""
            ...
        def init_rules_tab(self):
            """初始化规则管理选项卡"""
            ...
        def init_log_tab(self):
            """初始化日志选项卡"""
            ...
        def apply_settings(self):
            """应用加载的设置到UI"""
            ...
        def save_log(self):
            """保存日志到文件"""
            ...
        def create_menu_bar(self):
            """创建菜单栏"""
            ...
        def open_settings(self):
            """打开设置对话框"""
            ...
        def show_about(self):
            """显示关于对话框"""
            ...
        def show_stats(self):
            """显示统计信息"""
            ...
        def import_rules(self):
            """导入规则"""
            ...
        def export_rules(self):
            """导出规则"""
            ...
        def filter_rules(self):
            """过滤规则显示"""
            ...
        def clear_search(self):
            """清空搜索框并显示所有规则"""
            ...
        def clear_all_rules(self):
            """清空所有规则"""
            ...
        def add_rule(self):
            """添加新规则"""
            ...
        def reset_rules(self):
            """重置为默认规则"""
            ...
        def update_rules_management(self):
            """更新规则管理区域"""
            ...
        def edit_rule(self, category, extensions):
            """编辑规则"""
            ...
        def on_rule_toggled(self, category_name, enabled):
            """处理规则组的启用/禁用"""
            ...
        def delete_category(self, category):
            """删除整个分类"""
            ...
        def update_rules_preview(self):
            """更新分类规则预览"""
            ...
        def browse_source_directory(self):
            """选择要分类的源目录"""
            ...
        def browse_output_directory(self):
            """选择输出目录"""
            ...
        def start_classification(self):
            """开始文件分类"""
            src_dir = self.dir_input.text().strip()
            output_dir = self.output_dir_input.text().strip() or None
            if not src_dir:
                QMessageBox.warning(self, "警告", "请先选择要分类的目录")
                return
            if not os.path.exists(src_dir):
                QMessageBox.warning(self, "警告", "选择的目录不存在")
                return
            # 获取选项
            recursive = self.recursive_cb.isChecked()
            preserve_structure = self.preserve_structure_cb.isChecked()
            # 检查目录中是否有文件
            if recursive:
                # 递归检查所有子目录
                files = []
                for root, dirs, file_list in os.walk(src_dir):
                    files.extend([os.path.join(root, f) for f in file_list])
            else:
                # 只检查当前目录
                files = [f for f in os.listdir(src_dir)
                         if os.path.isfile(os.path.join(src_dir, f))]
            if not files:
                QMessageBox.information(self, "信息", "选择的目录中没有文件需要分类")
                return
            # 确认操作
            move_files = self.move_files_cb.isChecked()
            action = "移动" if move_files else "复制"
            recursive_text = "(包含子文件夹)" if recursive else ""
            # 检查设置中是否需要确认
            if self.settings_manager.get_bool('confirm_action'):
                reply = QMessageBox.question(
                    self,
                    "确认操作",
                    f"确定要{action}目录中的 {len(files)} 个文件吗?{recursive_text}",
                    QMessageBox.Yes | QMessageBox.No
                )
                if reply != QMessageBox.Yes:
                    return
            # 开始分类
            self.log_text.clear()
            self.log_text.append(f"开始{action}文件...")
            self.log_text.append(f"源目录: {src_dir}")
            self.log_text.append(f"输出目录: {'自动创建' if not output_dir else output_dir}")
            self.log_text.append(f"递归处理: {'是' if recursive else '否'}")
            self.log_text.append(f"文件数量: {len(files)}")
            self.log_text.append("-" * 50)
            # 设置UI状态
            self.classify_btn.setEnabled(False)
            self.stop_btn.setEnabled(True)
            self.progress_bar.setVisible(True)
            self.progress_bar.setValue(0)
            self.status_label.setText("正在分类...")
            # 创建并启动分类线程
            self.classification_thread = ClassificationThread(
                self.classifier, src_dir, output_dir, move_files, recursive, preserve_structure
            )
            self.classification_thread.progress_updated.connect(self.update_progress)
            self.classification_thread.classification_finished.connect(self.classification_complete)
            self.classification_thread.start()
        def stop_classification(self):
            """停止文件分类"""
            ...
        def update_progress(self, current, total, filename):
            """更新进度"""
            ...
        def classification_complete(self, success_count, failed_files, total_files, output_dir):
            """分类完成"""
            ...
        def reset_ui_state(self, classifying=False):
            """重置UI状态"""
            ...
        def closeEvent(self, event):
            """关闭事件处理"""
            ...
    def main():
        app = QApplication(sys.argv)
        app.setStyle('Fusion')
        app.setApplicationName("文件自动分类器")
        app.setApplicationVersion("1.0.1")
        app.setOrganizationName("xuyou & xiaomizha")
        window = FileClassifierGUI()
        window.show()
        sys.exit(app.exec_())
    if __name__ == "__main__":
        main()
    源码(无密码):

    xuyou-file-classifier v1.0.1.7z
    (552.88 KB, 下载次数: 0)
    2025-6-25 18:38 上传
    点击文件名下载附件

    文件, 规则

  • 您需要登录后才可以回帖 登录 | 立即注册

    返回顶部