☰
Python实现按关键词自动分类整理文件
2026/9/30 15:26:02 网站建设 项目流程

做文件整理这事,大多数人都是被逼出来的。我手头这个项目代号叫795,起因特别朴素:一个塞了2000多个文件的下载目录,混着合同扫描件、家电说明书、课程截图、报销发票、临时导出的Excel、各种软件安装包……真要找一份某个项目的合同,只能靠搜索框一个一个试关键词,运气好三分钟,运气不好半小时。于是决定写个小脚本,让文件自己找到属于自己的文件夹。

这个1850行(后面精简到300多行)的脚本解决的问题其实不复杂:读取一组关键词规则,扫描指定目录下的文件,命中关键词就自动创建对应文件夹并挪进去,没命中的统一丢进一个未分类目录。适合谁?手头有成百上千个散乱文件、又不想花一天时间手动建文件夹拖文件的人,而且稍微懂一点Python基础就能改着用。

1. 乱到什么程度才会想写这个工具:从2000个文件的下载目录说起

1.1 我做的第一件事:给混乱的文件台账拍了张照

动手之前,我先把下载目录整个列了一份清单,按大小排了个序,然后对着屏幕发了几分钟呆。2000多个文件,看起来好像什么都有,但真要说"有哪些类别",一时间还说不上来。这里有一个很关键的感知:人眼分类的时候,大脑会自动给每个文件提取一个"语义标签",但这个标签来自文件名,而不是文件内容。

所以我做的第一件事,是给现有的文件名做了一次词频统计。用一段极简单的Python读取所有文件名,按空格、下划线、横杠做粗切分,数一下哪些词出现次数最多。结果很直观:合同、发票、简历、报价、模板、说明、安装包、截图、最终版……这些词就是天然的聚类中心。我的需求其实一句话就能概括:这些高频关键词就是打开文件整理局面的钥匙。

1.2 整理文件夹这件事的本质是两件事

我后来才意识到,手动整理文件之所以慢,不是因为手上动作慢,而是因为每个文件都要经历一次"决策":先看一眼文件名,在脑子里检索一遍所有分类,确认应该放哪个文件夹,然后再新建文件夹或者找到已有文件夹,最后拖过去。2000个文件,就要做2000次决策。人做重复决策又慢又容易错,而机器做这件事又快又稳。

所以整个工具的本质就两条:

  1. 把分类规则固化下来:什么关键词的文件去哪个文件夹。
  2. 把移动操作批量化:一次扫描,一批文件自动归位。

这个思路听起来特别简单,但能覆盖绝大多数个人文件整理场景。不是所有整理都要靠AI理解文档内容,大多数时候,文件名就是我们自己命名的,只要规则定好,名字里就藏着分类信息。

1.3 为什么一定要"创建文件夹"而不是先建好一堆空目录

在设计方案时,我纠结过一个选择:是预制一批分类文件夹(合同、发票、学习资料……),还是让脚本按需创建?

预制文件夹的好处是分类结构稳定,但坏处也很明显——你永远猜不准未来会收到什么文件。今天建了"家电说明书",明天来了个"产品保修卡",你又得手动加一个。按需创建的好处更加贴近实际使用:文件夹是跟着文件长出来的,有的规则才建目录,没命中就什么都不动,保持目录结构干净。

比如规则里有"合同"这个词,但当前目录里一个合同都没有,那脚本就跳过,不会生成一个空文件夹占地方。后面我实测下来,这种"懒创建"策略在长期使用中特别舒服,不会积累一堆空的僵尸目录。

2. 方案选型对比:扩展名、时间、关键词,到底哪条路更贴近日常使用

2.1 三种自动化分类思路的实测情况

写脚本之前,我把自己能想到的方案全部列了一遍,还做了个小对比测试:

方案实现难度分类粒度找文件是否方便典型问题
按扩展名分类最低,os.path.splitext取后缀即可太粗一般PDF里合同和说明书混在一起,分完等于没分
按修改时间归档低,os.path.getmtime只有时间轴依赖记忆"上个月的文件"这种记忆极不可靠,找的时候头大
按文件名关键词中等,需要维护规则贴合人的认知很好规则需要迭代,覆盖不全时会有漏网之鱼
用NLP理解文件名/内容高,需引入模型细好本地部署成本高,对轻量需求是杀鸡用牛刀

我最后选了"文件名关键词包含匹配",不是因为它最聪明,而是因为它最符合我整理文件时的实际心智模型。我整理文件时想的是"这是合同"“这是发票”,而不是"这是PDF"“这是上个月的”。关键词分类的粒度正好卡在人的直觉上。

2.2 包含匹配与完全匹配的取舍

匹配方式上,我用的是"包含匹配"而不是"完全一致"。为什么?因为我们的规则词往往是短词,而文件名是完整句子。比如规则词是"合同",文件名叫"2024年度采购合同-最终版.pdf",这种情况用完全匹配就永远匹配不上,包含匹配“合同”两个汉字在不在文件名里出现,出现就归位。

这里有一个需要警惕的地方:包含匹配会带来误命中。比如规则词是"项目",一个叫"无项目计划说明.docx"的文件也会被命中。这就是我在后面第四节要详细说的翻车原因之一。处理思路不是放弃包含匹配,而是给规则词加权重、加优先级。

2.3 这个方案另外回答了一个热搜里反复出现的疑问

写的时候看到网上有朋友在问"文件夹分类"相关的问题,我发现大家纠结的点其实不在怎么移动文件,而是在**"分完类之后,文件夹会不会反而变得更难找文件"**。这里我用自己一段时间的实测回答:只要分类规则是用高频关键词反推出来的,分类后查找效率一定是提升的。因为你知道自己要找的东西大概率应该叫什么名字,顺着命名规律就能锁定文件夹;而不会像以前一样,只知道个大概内容,但要在一堆混合文件里大海捞针。分类本身不产生混乱,产生混乱的是分类规则和人对文件的记忆不一致。

3. 落地实现:关键词规则库、文件夹创建、文件挪移的三段式脚本

3.1 目录结构与配置设计

目录结构定得很简单,三个层级:

工作目录/ ├── 00-未分类/ # 没有命中任何规则的文件 ├── 01-合同文件/ ├── 02-财务票据/ ├── 03-求职资料/ └── 04-学习教程/

规则部分,我用了一个带优先级顺序的列表。每条规则包含:关键词组、目标文件夹名、优先级。关键词组和文件夹名是字典型的映射,一个文件夹可以对应多个关键词:

RULES = [ # 规则越靠前优先级越高,同一个文件命中多条规则时取最前面的 {"keywords": ["劳动合同", "采购合同", "合作协议", "合同"], "folder": "01-合同文件"}, {"keywords": ["发票", "报销", "付款", "流水"], "folder": "02-财务票据"}, {"keywords": ["简历", "个人简介", "求职"], "folder": "03-求职资料"}, ]

注意这里的顺序设计:"劳动合同"排在"合同"前面,表面看没有区别,但当多个规则同时命中一个文件时,我会按“命中规则出现的顺序”来归属。比如"劳动合同"既落在第一条也落在第二条(如果第二条写了"劳动"),这时靠顺序强制决定归属,就不会出现文件两边随机跑的情况。

3.2 核心脚本主流程

脚本主流程就四个函数来回调用:

import os import shutil import re BASE_DIR = r"D:\work_files\待整理" def load_rules(): """加载规则列表,每条规则按顺序判定""" return [ {"keywords": ["劳动合同", "采购合同", "合作协议", "合同"], "folder": "01-合同文件"}, {"keywords": ["发票", "报销", "付款", "流水"], "folder": "02-财务票据"}, {"keywords": ["简历", "个人简介", "求职"], "folder": "03-求职资料"}, {"keywords": ["教程", "课程", "学习笔记"], "folder": "04-学习教程"}, ] def classify_file(filename, rules): """根据文件名返回目标文件夹名,没命中返回None""" for rule in rules: for kw in rule["keywords"]: if kw in filename: return rule["folder"] return None def unique_dest_path(dest_dir, filename): """避免重名覆盖,同名自动加序号""" base, ext = os.path.splitext(filename) candidate = os.path.join(dest_dir, filename) counter = 1 while os.path.exists(candidate): candidate = os.path.join(dest_dir, f"{base}_{counter}{ext}") counter += 1 return candidate def organize(directory): rules = load_rules() dest_dir = os.path.join(directory, "00-未分类") os.makedirs(dest_dir, exist_ok=True) for item in os.listdir(directory): full_path = os.path.join(directory, item) if os.path.isdir(full_path): continue # 跳过文件夹,只处理文件 target_folder = classify_file(item, rules) if target_folder is None: target_folder = "00-未分类" target_dir = os.path.join(directory, target_folder) os.makedirs(target_dir, exist_ok=True) target_path = unique_dest_path(target_dir, item) shutil.move(full_path, target_path) print(f"[移动] {item} -> {target_folder}/") if __name__ == "__main__": organize(BASE_DIR)

这段代码没有高深的逻辑,但把它写稳需要处理不少边界。以下每个函数都有我实际踩过坑之后加的补丁。

3.3 为什么遍历文件时必须"跳过目录"

os.listdir会把文件夹和文件一起返回,如果不判断isdir,脚本就会尝试把已存在的文件夹移动到另一个目录里,轻则报错,重则把目录结构挪乱。这是一个入门级但极容易翻车的点,你如果在网上搜"文件夹分类 python 脚本"相关代码,会发现很多半成品都有这个bug——它们只处理文件,但遍历时并没有过滤目录。

3.4 中文文件名的匹配:看似简单,实际有两个暗坑

第一个坑是全半角符号混用。有的文件名里带全角括号"(最终版)",有的是半角"(final)",匹配关键词本身不受影响,但如果你后面想用正则做更精细的规则,就得提前统一字符宽度,否则规则会漏。我在验证阶段遇到过:文件名里含全角空格(U+3000),用普通空格切分特征词时直接失效。

第二个坑是系统保留字和特殊字符。创建文件夹时,Windows下目录名不能含有\/:*?"<>|这些符号。虽然我的文件夹名都是自己定义的,但文件名里这些字符不会影响移动操作,没有踩到坑。真正要小心的是把文件夹目标路径和文件名做字符串拼接时,不要漏了分隔符。

# 错误的拼接方式:漏了路径分隔符 target_path = dest_dir + filename # D:/files01-合同文件xxx.pdf # 正确的拼接方式 target_path = os.path.join(dest_dir, filename)

4. 第一次批量运行就翻车:误命中、文件占用、重名覆盖的完整排查过程

4.1 现象一:规则词太短导致大量误分类

脚本第一次跑完,我兴冲冲地打开"01-合同文件",发现里面躺着一个叫"合同制员工手册.pdf"的文件——这确实包含"合同"两个字,但它根本不是合同,是人事制度手册。这时候我才意识到,严格包含匹配没有人类那样的语境理解能力,必须用更具体的词组来消歧。

排查思路是这样:把误分类的文件挑出来,倒推应该用什么规则词兜住它们。然后我把规则词做了两个层级的区分:

  • 强规则词:命中后几乎不会误判,比如"劳动合同""合作协议""购销合同"。
  • 弱规则词:作为兜底,比如单独的"合同"。

修改后的判定逻辑改为:先按顺序匹配强规则词,全部强规则都没命中,再匹配弱规则词。相当于给规则加了权重。调整后误判率明显下降,从一二十个误判降到两三个可接受范围。

4.2 现象二:文件正被占用导致移动失败

跑到一半脚本报了个权限错误,说是某个PDF被另一个程序打开着。之前处理文档时开着的WPS没退,文件句柄被占用了,shutil.move在Windows下就直接抛PermissionError。

我当时的第一反应是加一个异常捕获,但更根本的问题在于这个错误是可以预判并提前处理的。最终方案是两层:

  1. 程序启动时先检查目标文件是否能正常打开(用os.access判断可读性,虽然不绝对可靠,但能筛掉大部分占用情况)。
  2. 移动时统一包一层try/except,遇到权限错误就把文件路径记录到skipped.log里,不中断整个批次。
try: shutil.move(full_path, target_path) except PermissionError as e: with open("skipped.log", "a", encoding="utf-8") as f: f.write(f"[权限错误] {full_path} : {e}\n") print(f"[跳过] {item}(文件可能被占用)")

这个改动让脚本从"一遇到错误就崩溃"变成"跑完全程,最后复制日志里的遗漏文件"。批量工具稳定运行的秘诀不是消灭所有异常,而是把异常兜住并且留痕。

4.3 现象三:重名文件被直接覆盖

这个坑最隐蔽。shutil.move在目标文件已存在时,行为分两种情况:目标文件是个目录,会移动进去;目标文件是个普通文件,会直接覆盖掉。我当时两个不同日期导出的Excel文件同名,后一个就把前一个顶掉了,数据直接丢失。

好在这个问题可以用"查重改名"策略解决,也就是上面代码里的unique_dest_path函数。核心逻辑很简单:文件放进目标文件夹前,先检查路径是否已存在,如果存在,就在文件名主干后面加_1、_2序号。

# 例如:报表.xlsx 已存在 # 自动生成:报表_1.xlsx → 报表_2.xlsx → 直到不冲突

这个函数必须放在shutil.move之前调用,而且最好在拼接路径时就用上。我当时第一版是先move再判断,结果判断永远在覆盖之后执行,等于白写。

4.4 现象四:误把目标文件夹当文件处理

还有一个隐藏问题:**如果目标文件夹恰好位于被扫描的目录里,第一轮跑完后,下一轮再跑脚本,这些文件夹也会被listdir扫描出来。**如果代码里忘了加isdir判断,第二轮运行就会把这些已经建好的文件夹强行移动到别的目录里,目录结构当场混乱。

我在前面已经提到这个判断,但这里再说一次是因为它的重要性被我严重低估了。排查时看到脚本把01-合同文件移动到了00-未分类里面,当时整个人是懵的,后来追日志才发现就是缺了一个continue。

if os.path.isdir(full_path): continue # 千万不能漏,否则会搬走自己创建的文件夹

4.5 排查过程的完整思路沉淀

这四次翻车让我总结出一个固定的排查套路,后面再做类似批量脚本时照着走:**先在小范围样品目录上运行,把日志打开,每一条移动记录都看一眼;再扩大到中等规模,重点观察是否有误分类和重名;最后全量运行前,再跑一次Dry Run(只打印不移动),确认无误再动真格。**这个套路下文第5节会展开讲。

5. 规则外置与匹配精度升级:从写死脚本到可配置的轻量工具

5.1 把规则改成JSON配置:一个目录一套规则

第一版规则是直接写在Python文件里的,好处是跑得快,坏处是每换一个使用场景就要改代码。后来我整理公司共享目录和家里的下载目录时,发现规则完全不一样——公司里合同、发票、项目文档是主角,家里更多是电影、音乐、电子书安装包。与其改代码,不如把规则挪到外部配置。

{ "base_dir": "D:/work_files/待整理", "rules": [ {"keywords": ["劳动合同", "采购合同", "合作协议", "合同"], "folder": "01-合同文件"}, {"keywords": ["发票", "报销", "付款", "流水"], "folder": "02-财务票据"}, {"keywords": ["简历", "个人简介", "求职"], "folder": "03-求职资料"} ], "fallback_folder": "00-未分类" }

加载配置的代码很直白:

import json def load_config(path="config.json"): with open(path, "r", encoding="utf-8") as f: config = json.load(f) return config["base_dir"], config["rules"], config["fallback_folder"]

用JSON外置规则最大的收益是整理不同场景时不用再碰代码。我后来又做了一份"家庭媒体库"规则,把电影、剧集、音乐、电子书分别归档,两个配置文件并行存在,要整理哪边就加载哪边的配置,整个工具的适用范围一下子从"下载目录专用"扩展成了"通用文件归类器"。

5.2 相似度兜底:没有命中任何关键词的文件怎么处理

总有一批文件是规则词覆盖不到的,比如一个文件叫"扫描件_20241015.pdf",里面有没有合同、发票,单看文件名完全猜不到。最初设计方案时,我把这类文件一律丢进00-未分类,但没过多久,这个文件夹又积累了两三百个文件,等于把原来的混乱换了个地方放着。

后来我加了一道相似度兜底:**如果文件名没有命中任何强规则,就用difflib的字符串相似度,和目标文件夹名字做一次贴近度比较,超过一定阈值就认为可以归入。**比如difflib.SequenceMatcher计算"扫描件_20241015"和"合同文件"的相似度可能不高,但"采购合同扫描件"和"合同文件"的相似度就会比较高。

from difflib import SequenceMatcher def fuzzy_match(filename, folder_name, threshold=0.25): # 取文件名去掉扩展名前半部分与文件夹名比较 name_without_ext = os.path.splitext(filename)[0] ratio = SequenceMatcher(None, name_without_ext, folder_name).ratio() return ratio >= threshold, ratio

这里要特别提醒一个教训:阈值不能拍脑袋设得低。我一开始把阈值设成0.3,结果"学习笔记"和"求职资料"这种语义上毫无关联但字符串上有相似字符的文件全被拖走了。最终调试下来0.4左右比较稳,但具体值依赖你的命名习惯,建议跑一次Dry Run,观察相似度命中列表再定。

相似度匹配属于兜底方案,宁可少匹配也不要乱匹配。兜底命中率能到50%我就满意,它的主要作用是防止00-未分类膨胀,而不是追求准确识别——准确识别还得靠关键词规则。

5.3 Dry Run模式与自动运行日志

批量移动类工具,最应该加上的功能其实是演习模式,不实际移动,只打印计划操作。我加的开关很简单:

DRY_RUN = True # False时执行真实移动 if DRY_RUN: print(f"[演练] {item} -> {target_folder}/") else: shutil.move(full_path, target_path)

为什么一定要这个开关?因为人眼检查大量真实移动后的目录结构是不可靠的,但检查演练模式打印出来的几百行文本却非常高效。第一次跑全量整理之前,我用Dry Run模式跑了三遍,每次调整规则,直到确认输出列表里没有误分类和重名问题,才真正让它动手。实测之后这个习惯帮我躲过了至少两次灾难级的误分类。

日志方面,真实执行时我把每次移动的源路径、目标路径、时间都追加写进organize.log:

import datetime log_line = f"{datetime.datetime.now().isoformat()} | {full_path} -> {target_path}\n" with open("organize.log", "a", encoding="utf-8") as f: f.write(log_line)

后来要反查某个文件去哪了,直接打开日志Ctrl+F搜文件名,一秒定位。这个日志机制看着不起眼,但一旦文件数量上千,它的价值比整个脚本还大。

5.4 对"关键词分类文件"这件事的最终判断

做完整个795项目,我的感受是这类工具的门槛不在写代码,而在规则设计。代码几百行就能撑起一个非常顺手的文件管理工具,但真正决定它好不好用的,是关键词选得准不准、优先级排得对不对、兜底策略稳不稳。这三点没有一次到位的,都是边跑边调出来的。

需要说明的是,上面所有路径处理逻辑都以Windows环境为准,Linux/macOS下路径分隔符不同但os.path.join会帮你处理好;如果你要处理的目录在共享网络位置,运行前记得确认有权限写入,不然会出现第一轮能建文件夹、第二轮就提示拒绝访问的情况。这些都是我实际运行中碰到过的问题,列出来给后面要做类似工具的朋友排个雷。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询