
你有没有过这种时刻打开一个记事本文件里面躺着上百条从手机便签、临时备忘录、聊天记录里复制过来的碎片文字找一条两周前的地址要翻半天想删旧记录又怕误删。我这个“清理记事本”项目就是专门解决这个痛点的——用一个本地运行的Python脚本把杂乱无章的纯文本笔记自动完成编码修复、内容清洗、分类打标、去除重复、时间归档最后输出一份按主题归并的干净目录。它适合两类人一是手里攒了大量无格式文本、想在不依赖任何在线笔记服务的情况下做一次历史数据整理的人二是刚接触数据清洗的开发者这个项目能让你在几百行代码内完整经历一次“数据整理流水线”。1. 这个清理记事本项目到底在解决什么问题1.1 为什么我会搭这样一个工具写这个项目不是一时兴起。我个人的“记事本”其实是一个txt文件几年下来往里面扔了上千条内容快递单号、临时灵感、待办事项、会议纪要、软件授权信息、网站账号、摘抄句子。由于长期只是往上追加几乎从不整理这个文件慢慢变成了一个“数据垃圾场”。每次想查找一条旧记录都得在搜索框里碰运气想清理又不敢乱删因为里面确实夹着一些还要用的信息。后来我意识到问题不在于“不愿意整理”而在于整理一个上千条混杂内容的纯文本记录靠手工根本做不到可持续。于是干脆做了“清理记事本”这个项目写了一套脚本先扫描、再清洗、再归类最后把结果输出成按主题拆分的小文件。整套逻辑跑完不需要我逐个看内容省下来的时间非常可观。这个项目最有价值的地方在于它不是针对某一类特定文本的一次性脚本而是一套可以反复使用的流程。每一次跑清理我都能看到自己的记录习惯有了哪些变化然后调整规则让脚本越来越贴近自己的真实用法。1.2 它具体能完成哪些事情我给它定义的核心能力有四项自动识别并修复乱码文件兼容多种常见编码处理BOM头。按规则把每一条记录归入大类待办、灵感、信息资料、摘抄、临时片段。检测并合并重复内容既包括完全相同的行也包括高度相似的内容。输出结构化清理结果按分类生成独立文件同时保留一份原样备份绝不直接覆盖原始数据。除了这四项脚本还附带一个简单的统计报告告诉你每个分类有多少条、清理掉了多少重复项、耗时多久。虽然不是必须但这个报告对我来说很重要——整理完看到数字变化会有一种“确实清干净了”的实感。设计时我刻意遵守一个原则宁可生成一堆分类文件也不要直接改动原始文件。因为自动清理总有误判风险原文件是最后的退路。这一点在后面踩坑部分我会重点展开。2. 动手前先想清楚整体设计与规则定义2.1 先诊断“乱”的三种类型做清理脚本之前我先把自己那份上千行的记事本抽样看了一遍总结出文字资料混乱的三个根源。第一种是编码混乱。同一份txt里一部分是手机便签导出的UTF-8 with BOM一部分是旧Windows记事本保存的ANSIGBK混在一起后脚本读取时很容易出现“锟斤拷”这类典型的编码替换乱码。它不是一个“好不好看”的问题而是信息是否能被正确读取的问题。第二种是格式混乱。有的记录自带换行和缩进有的把多件事挤在一行有的条目前有日期有的没有甚至还有几处以“”分割的老式日志风格。这种混乱导致我根本没法用统一的格式去解析必须先做清洗标准化。第三种是内容冗余。复制粘贴了太多重复信息同一条灵感在不同时间被粘贴了两次同一条地址在对话里出现了多遍。这些冗余不处理清理出来的文件仍然臃肿。对“乱”做了分类之后脚本的设计方向就明确了先做编码统一和文本清洗再做解析归类最后做去重。顺序不能反因为如果先判断一行文字是不是重复编码出了问题会导致同样的字显示成不同的乱码重复检测就失效了。2.2 给记录定一套轻量规则我始终不推荐用复杂NLP甚至上模型来清理个人笔记因为个人笔记的格式太天马行空且量级通常在几百到几千条用规则处理完全够用。所以这个项目的核心不是算法而是“给记录定规则”。我给每条记录定义了一个轻量结构[类型标签] 正文内容类型标签采用英文缩写加一个前缀符号例如[T] 待办事项以“记得”“要”“待办”“todo”等开头或有明确时间指向的内容[I] 灵感想法短句、反问、以“要不要”“如果”开头的思考[F] 信息资料包含网址、账号、地址、电话、单号等强特征词的内容[Q] 摘抄引用带引号、书名号、日期署名的句子[M] 临时片段以上都不匹配的默认兜底规则用关键词特征而不是全文语义判断。比如要判断一条记录是不是信息资料就看有没有http、账号、地址、单号这类强标志词判断待办就看有没有记得、明天、周三这类时间或动词提示。这种判断准确率远不到100%但实际使用中能自动归类七成左右的条目剩下没判准的兜底进临时片段我事后扫一眼就能处理完。提示规则设计时不要把特征词定得太死。比如“明天”既可能是待办也可能是某句话里的时间指代。我的策略是“强特征词优先弱特征词兜底”先匹配http、日期式等确定性高的内容再匹配“记得”“要不要”这类提示词最后全部没命中的进默认类。2.3 为什么选Python而非现成整理工具其实市面上很多笔记软件自带整理功能标签系统、全文搜索都有。为什么我非要自己写脚本三个理由。第一我这些数据的源头不是某个笔记软件而是多个渠道汇集的纯文本导出。把它们导入某家在线笔记服务数据要先过一遍平台服务器我不太想为了整理几百行私人文本就承担这个迁移成本。本地脚本处理不产生网络流量天然离线数据始终在自己手里。第二现成工具能做的分类通常是手动建文件夹或标签归类但对我这种时间跨度大、格式乱的文本需求其实是“按内容特征自动分堆”这个功能普通工具做得一般甚至很多工具对txt文件连批量处理都没有。第三写脚本本身是一次低成本的技术练习。文本处理的输入输出很直观调试周期短跑一遍就能看到结果非常适合练正则、练文件IO、练异常处理。我用Python写主要是因为它处理文本和正则表达式的生态最省事随手也能跑起来不需要编译。3. 核心实现一个能跑的清理脚本3.1 第一步读文件前先搞定编码这是整个项目里最“不性感”但最关键的一步。如果不处理编码后面所有正则规则都会在乱码文本上失灵。我最终的读取函数如下import re import chardet from pathlib import Path def read_text_file(file_path: str) - list[str]: raw Path(file_path).read_bytes() # 先用启发式检测猜测编码 guess chardet.detect(raw) enc guess.get(encoding, utf-8) or utf-8 # 常见编码映射修正 if enc.lower() in (gb2312, gbk): enc gb18030 try: text raw.decode(enc) except (UnicodeDecodeError, LookupError): # 兜底用 utf-8 带替换字符解码避免直接抛异常 text raw.decode(utf-8, errorsreplace) # 去掉 BOM if text.startswith(\ufeff): text text[1:] # 统一换行把 \r\n 和 \r 都转成 \n text text.replace(\r\n, \n).replace(\r, \n) lines [line.strip() for line in text.split(\n) if line.strip()] return lines这里有两个细节值得说明。chardet是个第三方库用来检测文本编码。它不是百分百准确但对我们这种场景已经够用。我加了一个小修正检测到gb2312或gbk时统一用gb18030解码因为gb18030是gbk的超集能兼容更多生僻字避免一部分边缘场景解码失败。去BOM和统一换行属于“看起来不起眼但必须做”的脏活。BOM如果不去掉第一行开头会多一个不可见字符正则里的^匹配和关键词判断都可能出问题。统一换行则是为了后面的按行处理。这里我读入原始字节后先整体解码而不是用open(..., encoding...)直接读因为这样才来得及做编码探测。注意解码时设置了errorsreplace兜底这意味着如果真的彻底解不出来会把坏字符替换成替代符号。这个兜底并不安全但可以避免脚本读到一半崩溃。我的习惯是脚本跑完后回头检查报告里的失败条目把那些真正乱掉的少量内容人工核对。自动处理永远做不到完美留一个可控的兜底接口给人工比追求一次成功更实用。3.2 第二步核心的清洗加归类逻辑读入干净文本后下一步是逐行清洗并归类。清洗包括去掉行首行尾的空格和制表符过滤掉空行压缩连续空格为单个空格替换部分中文标点为统一符号。这一段我用正则完成def clean_line(line: str) - str: line line.strip() if not line: return # 把连续空白压缩成一个空格 line re.sub(r\s, , line) # 把常见中文标点统一成英文半角 line line.replace(, ,).replace(。, .).replace(, ;) line line.replace(, ().replace(, )) # 去掉常见分隔线比如 或 ---- if re.fullmatch(r[\-_*]{3,}, line): return return line这一步比较保守我没有做太激进的全角半角转换因为正文内容里偶尔保留原标点是可以接受的过度转换反而可能改变原意。归类函数采用“优先级从上到下”的判断def classify_line(line: str) - str: # 强特征词信息资料优先判断 if re.search(r(http|https|www\.|账号|地址|电话|单号|邮箱), line, re.I): return F # 信息资料 # 待办事项 if re.search(r^(记得|待办|todo|要|应该|务必), line, re.I) or \ re.search(r(明天|下周|今天|晚上|上午|下午)\S{0,4}(去|做|买|办|处理), line): return T # 待办 # 摘抄引用 if in line or “ in line or 》 in line: return Q # 灵感想法 if re.search(r(要不要|如果|我觉得|可能|灵感|突然想到), line): return I # 默认兜底 return M很多人写归类时会想到用机器学习或深度学习我个人觉得没必要。个人笔记的条目通常很短特征词足够明显规则能做到七八成准确率剩下的靠兜底人工处理。规则脚本的好处是透明、可改哪一类识别不准直接调整正则就行不用重新训练。我实际使用中最麻烦的是“待办”和“灵感”之间的区分。一句“明天要不要去超市”明显是待办但规则可能先命中灵感特征词“要不要”。解决办法是调整判断顺序把待办判断放在灵感之前同时把“时间词动作词”作为待办的强特征。这条经验是我的真实心得在这个场景里判断顺序比规则形式本身更重要采用“命中即返回”的设计时一旦顺序错了误判率会大幅上升。3.3 第三步去重与相似度判断清理记事本的一个核心目标是减少冗余。去重我分两级。第一级是精确重复直接对比清洗后的整行文本。这个用集合就能做但要注意保存第一次出现的行否则去重后每一类里的顺序会乱掉。def dedup_exact(lines: list[str]) - list[str]: seen set() unique_lines [] for line in lines: key line.strip().lower() if key not in seen: seen.add(key) unique_lines.append(line) return unique_lines第二级是相似重复比如两条记录内容一样但有一条带了日期前缀。精确去重抓不到因为字符串不完全相同。我采用的方案是提取“指纹”去掉所有数字、日期、标点只保留中文和英文字母统一大小写如果指纹超过10个字且指纹完全相同就判定为相似重复这个方案比较直接效果却意外地好。原因是记事本里的重复往往是同一条内容复制粘贴时带了不同的日期、编号或前后缀文本主体高度重叠。指纹提取把它们归一化后相似度判断就变得很简单。from collections import defaultdict def fingerprint(line: str) - str: # 保留中英文字母其余全部去掉 s re.sub(r[^\u4e00-\u9fffA-Za-z], , line.lower()) return s if len(s) 10 else def merge_similar_duplicates(lines: list[str]) - list[str]: result [] fp_map defaultdict(list) for line in lines: fp fingerprint(line) if fp and fp_map.get(fp): continue # 已存在相近内容 if fp: fp_map[fp].append(1) result.append(line) return result这里有个经验指纹长度阈值不能设得太低。设成8个字的时候会把“明天下午开会”和“明天下午开会的资料”误判为重复后者明显是不同内容设成10个字后短句不参与相似去重只保留精确去重误伤明显减少。还有一个细节待办类内容我默认不做相似去重只做精确去重。因为待办条目看似相近实际可能是两件不同的事误删待办的代价比误删一条摘抄高得多。3.4 第四步输出结构化结果清理完成后我把结果写成五类文件外加一个统计报告。输出采用“原文件夹下新建clean_output目录”的方式不碰原始文件。def write_results(lines: list[str], output_dir: Path): output_dir.mkdir(parentsTrue, exist_okTrue) buckets defaultdict(list) for line in lines: cat classify_line(line) buckets[cat].append(line) for cat, cat_lines in buckets.items(): target output_dir / f{cat}.txt with target.open(w, encodingutf-8) as f: f.write(\n.join(cat_lines)) # 统计报告 report_lines [清理报告, * 30] for cat, cat_lines in sorted(buckets.items()): report_lines.append(f{cat}: {len(cat_lines)} 条) report_lines.append(f总条数: {len(lines)}) (output_dir / report.txt).write_text(\n.join(report_lines), encodingutf-8)这份代码已经可以直接跑通。但直接跑一遍就会发现两个问题一个是分类文件可能为空或过小。比如摘抄类只有两条是不是规则没识别准我会去检查原始文件里那些带引号的句子是编码坏了还是格式怪。另一个是输出文件仍然不够干净因为部分记录是多行复合内容单行拆分后上下文丢了。针对后一个问题我后来加了一个小改进在按\n分割前先把明显是“同一条记录”的多行合并。判断依据很简单一行不以句号、逗号、感叹号等结尾且下一行是缩进或长度较短就认为它们是同一记录的续行。这样做之后导出文件的语义完整性有明显提升。不过这块我做得比较保守只处理了以缩进开头的续行否则容易把不同记录错误拼接。4. 踩坑记录真实使用中遇到的那些问题4.1 编码炸弹UTF-8 BOM引发的首行错乱第一次跑脚本时输出文件里第一行总是莫名少了一个字符。排查了很久最后发现是BOM导致的原始文件以UTF-8 with BOM保存读入后第一行前面带着一个\ufeff字符正则匹配^记得时自然失败归类结果直接掉进默认类。这个问题看似小但它提醒了我一件事不要假设文件“看起来正常”。txt文件可能是任何编码生成的手机导出的、电脑创建的、聊天记录另存的都可能带BOM或使用GBK编码。对任何原始文件第一件事都应该是先做字节层面的探测而不是直接按utf-8读。我排查这类问题时最常用的方法是把读入后的第一行用repr()打印出来。with open(file_path, rb) as f: raw f.read(64) print(repr(raw))如果看到b\xef\xbb\xbf开头基本可以确定是BOM。知道原因后后续处理就很简单解码后第一个字符如果是\ufeff就删掉。4.2 “伪重复”识别误伤人相似度去重上线后我误删过几条内容。最典型的一条是原文“买一个收纳盒桌面书架用”和新记录“买一个收纳盒放卫生间”。指纹都是“买一个收纳盒”长度超过10个字被判定为重复后面那条被删掉。但实际上它们是两个不同的待办事项。这个误伤让我意识到文本指纹适合处理“完全复制后加前后缀”的场景不适合处理“共用前半句但后半句不同”的场景。解决办法有两个一是在指纹之外增加一个尾字符校验要求末尾两个字符也一致才判定重复二是对落入待办类的内容默认不做相似去重只做精确去重。因为待办这种高行动属性的条目内容近似但意图可能是两件事删错了影响很大。最终我选了第二种方案简单有效。更通用的做法是在不同分类上设置不同的去重强度信息类可以生成指纹做相似判断灵感类的容错率更低待办类则尽量保守。规则化处理的优势就在这里——给每个分类一套参数逻辑还是透明可控的。4.3 正则规则处理中文的后遗症中文文本用正则时有个容易翻车的点\w在Python的re模块里默认会匹配中文但\b单词边界对中文的判断并不可靠。我写规则时一开始使用\b做关键词边界结果出现奇怪现象\b灵感\b有时不生效有时又莫名匹配到一行里的词组。排查后我的对策是中文场景里尽量不用\b改用更明确的前后字符判断。比如判断“灵感”这个词是不是独立出现我写成if re.search(r(?![A-Za-z0-9])灵感(?![A-Za-z0-9]), line):这种写法等价于“中文语境下的单词边界”实测下来比\b可靠。另外多个中文特征词同时出现时正则里的交替顺序也会影响结果re.I对内不含拉丁字母的中文没有影响但遇到“Email”这种中英混搭时要注意大小写问题。这些细节都属于“不跑一遍不会意识到”的坑。4.4 误删风险与备份策略清理脚本的杀伤力不在于删除文件本身而在于它会在你忽略细节时悄悄把内容归拢到错误的地方导致你以为内容消失了。有两次我差点把重要的临时备注弄丢一条在信息资料类里因为里面有一个网址另一条在临时片段类里因为特征词全没命中。所以我在脚本里强制设置了两个安全措施。第一个措施是从不原地覆盖。每次清理都新建带时间戳的输出目录。即使某次规则改错了原始文件仍然完好可以重新跑一遍。第二个措施是每次运行前自动生成完整备份from datetime import datetime def backup_original(src: Path, backup_dir: Path): ts datetime.now().strftime(%Y%m%d_%H%M%S) backup_path backup_dir / fbackup_{src.stem}_{ts}{src.suffix} backup_path.write_bytes(src.read_bytes()) return backup_path这个函数看起来简单但它意味着无论脚本怎么改你都能回到清理前的状态。我个人强烈建议任何自动化数据整理脚本都把这份“后悔药”内置进去。5. 一次真实清理过程复盘5.1 准备阶段我用一份模拟数据集演示整体流程。假设有个文件叫notes_raw.txt总共有320条记录其中包含一些故意混入的乱码、重复项和格式不一的条目。我会先手动浏览它的前20行获得一个大致的格式印象然后运行脚本观察报告。命令行使用方式如下python clean_notes.py --input notes_raw.txt --output clean_output脚本启动后会先打印“检测到编码: utf-8”然后逐行处理最后生成报告。由于文件量不大整个处理过程不超过1秒。5.2 运行结果一份代表性的统计报告大致是这样清理报告 F (信息资料): 102 条 T (待办事项): 78 条 I (灵感想法): 56 条 Q (摘抄引用): 35 条 M (临时片段): 49 条 总条数: 320 条清理前我肉眼预览里至少能找到七八条明显重复的内容脚本处理后精确重复和相似重复合计去掉了16条。这个数字说明重复问题比表面看到的更严重。真正让我意外的是临时片段居然还有49条。我逐个扫了一遍其中有一半确实是无关紧要的内容但另一半是信息资料漏网之鱼——它们含地址但格式怪异比如“小区x栋x室”这种没有“地址”关键字的描述。这说明特征词表还需要补充。我额外加了一个特征词小区|栋|室|号楼覆盖了这半部分。这就是规则化处理的迭代快感改一行正则跑一遍看报告马上知道有没有改善。5.3 沉淀出的习惯跑完这次清理后我给自己定了三条使用习惯。第一新记的内容在落笔时就打上标签。以前我随手写现在会尽量用[T]开头。虽然清理脚本能自动判断但人写时如果顺手加标签准确率直接接近100%。第二周期性地清理而不是等文件堆积到上千行再动手。我现在的频率是两个星期一次。两次清理之间积累的内容数量适中分类识别准确率最高人工复核成本也最低。第三确信保留原始文件。即使脚本跑得再顺我也从不让它直接覆盖原始txt。定期归档旧备份后可以删除但原始文件这辈子都不删。6. 后续扩展与我的使用心得6.1 还能加什么功能这个项目目前只是一个单文件脚本扩展空间其实很大。最容易加的是“关键词索引”。把每个分类文件里出现频率较高的词语提取出来生成一份索引找内容时先看索引再进文件定位速度会快很多。对应实现也不复杂用collections.Counter统计切词结果就行。进阶一点可以做成“双格式输出”。现在只输出txt以后可以同时生成Markdown版本把每条记录转成列表项配合清单类应用生成待办导入文件。再进阶就是做成命令行工具加入“交互式确认”模式每处理一条可能被视为重复的记录打印出来问用户是否合并。这种模式牺牲一点速度换来的却是更高的安全感和准确率。我实际使用中批量自动模式处理完后的复核一般要10分钟交互式虽然慢但基本免除了复核环节两者时间成本差不多。6.2 个人使用体会这个项目让我重新理解了“清理”这件事。它本质上不是删除而是重新组织。把散落的记录分类、去重、排序之后那些被淹没的信息重新浮现出来有时候甚至会让人想起自己半年前的一个重要想法有种意外的收获感。技术层面它也用很小的成本验证了一个原则面对格式混乱的文本先统一编码再定义规则再处理重复最后结构化输出这条流水线放在绝大多数个人数据整理场景都通用。规则脚本虽然朴素但它的可解释性和可调整性让它比看上去更“聪明”。最后分享一个小技巧。如果你手头也有一个巨大的无序txt文件先别急着写完整脚本先跑两行代码把文件按行数打印出前100行再统计一下总行数。对数据本身了解得越充分规则设计就越精准。不要拿到文件就开始写正则——先看数据再定规则这永远是文本处理的第一法门。