1. 为什么我写了这个TXT批量文本处理工具
先说个真实的场景。有段时间我手上有一份从老系统导出的客户数据,整整八十万行,格式乱得让人头大——有的行是电话号码,有的行是备注,有的行是空行,还有大量重复记录混在里面。当时我第一个念头是用记事本打开,结果等了快半分钟才显示出来,想做个去重,记事本根本干不了这活。换Notepad++?加个插件勉强能去重,但八十万行的量级一跑就卡。打开Excel?数据量已经超过单表上限,连导入都费劲。
后来我陆陆续续遇到类似的需求:把两个TXT文件的共找出差异、把通讯录里的重复号码清掉、给诗句词典txt按行增删改查、把从网页复制下来的小说正文整理成干净的章节目录。这些活儿说大不大,说小不小,每一件用通用编辑器和SQL都能做,但每次都靠临时写脚本、靠各种在线工具倒腾,效率实在低到让人抓狂。
于是我开始动手写一个TXT批量文本处理工具,核心就围绕五个能力:数据比对、去重、增删改查、号码筛选、记事本净化。这五个词看起来简单,可真正把“批量”“文本”“处理”三个词落到工程实现上,里面埋的坑比想象中多得多。这篇文章我会把设计和实现思路拆开讲,也会把实际使用过程中踩过的坑和解决经验完整交代一遍。
需要说明的是,下面所有代码示例和算法思路,是基于我在开发这款工具时的真实背景补充的。如果你的需求类似,可以直接参考;如果需求有差异,也可以根据这些思路做减法或加法。
这款工具适合谁来用?在我看来,主要三类人:一类是经常处理导出数据、日志、通信录、网址列表的运营和测试人员;一类是维护词典、编码表、小说TXT、歌词字幕等文本资源的内容整理者;还有一类是程序员——写SQL之前想快速把数据清洗一遍,或者懒得为一次性任务开IDE,直接用这个工具搞定。
2. 五项核心功能的设计思路与实现要点
2.1 数据比对:不只是findstr能搞定的事
数据比对是这个工具最常被用到的功能。很多人第一时间想到的是IDE里自带的比较插件或者Beyond Compare,但那些工具更适合文件级别的可视化对比。我面对的需求往往是“同一个文件里有哪些行是重复出现的”“两个文件里哪些行只在A中出现”“哪些行两边都有”,也就是集合层面的差集、交集、并集运算。
这里有个关键设计:所有比对结果必须可导出、可预览,而不是只在界面上把两堆文字并排给你看。
举个例子,我有A和B两个txt文件,A是今天从系统导出的手机号码列表,B是昨天导出的历史号码列表。我要知道今天新增了多少号码,那就是B-A的差集;我要知道两天都在的号码,那就是交集。用编程方式写,核心逻辑非常直白:
def compare_files(file_a, file_b): set_a = set(读取并清洗(file_a)) set_b = set(读取并清洗(file_b)) only_a = list(set_a - set_b) only_b = list(set_b - set_a) both = list(set_a & set_b) return only_a, only_b, both但工程上的坑在于“清洗”这一步。是不是要去掉行首行尾空格?要不要忽略空行?要不要区分大小写?要不要把全角数字转成半角?这些选项多了,工具反而难用。我的处理方式是:默认不改变原始内容,但提供“比对前规范化”的可选项,比如“忽略首尾空白”“忽略空行”“忽略大小写”“全角转半角”。这样既保留了文本原貌,又能照顾到实际业务中那些“表面不一样、实际应该一样”的数据。
还有一个细节容易被忽视——比对结果的排序。集合运算在数学上不关心顺序,可人在看结果时,顺序乱了根本看不下去。我的做法是保持结果按照源文件中的原始出现顺序输出,而不是用Python的set直接输出(那会导致内容顺序随机)。这样用户在比对完导出结果后,能清楚地回看“这一条原来在文件的什么位置附近”。
2.2 去重:从内存哈希到磁盘分桶
去重是TXT批量处理里最常见的操作,也是网上讨论最多的话题。热搜词里有“数组去重”“对象数组去重”“sql语句去重”,可见这个问题在编程世界里也是高频需求。但TXT文件的去重和内存数组去重有一个本质区别:数据规模太大了,大到内存可能装不下。
如果文件只有几万行,直接用hashset就可以了:
seen = set() with open(source, 'r', encoding='utf-8') as f: for line in f: line = line.rstrip('\n').rstrip('\r') if line not in seen: seen.add(line) out.write(line + '\n')这种办法简洁高效,几万行的文件秒级搞定。但当我第一次处理一个两百多万行的日志文件时,内存瞬间飙升到快2GB,程序差一点被系统杀掉。问题就出在set里存的是完整字符串对象,每行一百多个字符,两百万行就是两百多MB的原始数据,再加上set的哈希表结构和Python字符串对象的额外开销,内存直接爆炸。
这时候就要换思路。我采用了最朴素也最稳的分桶去重方案:先对每一行计算哈希值,比如MD5或者更快的xxhash,然后根据哈希值的前两位把行分流到256个临时文件中。同一个桶内的行,哈希值前缀一致,并不代表内容一样,但至少内容一样的行一定在同一桶内。然后再对每个桶单独做内存去重,最后按桶顺序合并输出。
行内容 → 哈希值(如 1a2b3c...) → 取前缀"1a" → 写入 temp_1a.txt 每个桶内部 → set去重 → 合并好处是内存占用被控制在一个桶的规模以内,即使面对上千万行的文件也能扛住。坏处是需要临时磁盘空间,并且要多读多写一遍。实际测试中,一个500万行、每行80字节的文件,分桶去重大约需要多花30%的时间,但内存占用始终在200MB以内,这个代价完全值得。
去重还有一个衍生需求:统计每条数据出现的次数。这个功能在筛选重复号码、整理“哪个词在词典里出现最频繁”这类场景下非常好用。实现上只需要把set换成dict,key是行内容,value是出现次数,最后按次数排序输出即可。
2.3 增删改查:行级编辑与批量正则替换
说起“增删改查”,大家第一反应是数据库。但TXT文件里的增删改查有自己的特殊性:没有主键、没有事务、改坏了没人帮你回滚。所以我在设计这个模块时,把核心放在了“操作的可预期性”和“执行前的预览”上。
- 增:在文件头部、尾部、或者匹配到的某一行前后插入内容。
- 删:删除空行、删除重复行、删除包含指定关键词的行、删除正则匹配的行、删除第N行到第M行。
- 改:全局替换、正则替换、行首行尾加内容、去掉行首行尾空白、大小写转换、全角半角转换。
- 查:按关键词筛选、按正则筛选、按行号定位、按内容长度筛选、统计行数。
这里我想重点讲一讲改的预览机制。很多编辑器做全局替换都是一次性改完,改完才发现坏了,可不可逆非常看运气。我在工具里做了一个“替换预览面板”:执行替换之前,先模拟所有替换,把受影响的行列出来,显示替换前和替换后的差异,确认无误后再写回文件。
比如我想把一个SQL导出文件里所有VARCHAR(255)批量改成VARCHAR(100),或者把小说文本里所有“章节”后面的编号统一格式,这种正则可以帮你省掉大量手工劳动:
匹配规则:^第([一二三四五六七八九十百千零]+)章 替换模板:第\1章 | 新前缀还有一个容易踩的坑:行尾换行符不统一。Unix系统是\n,Windows是\r\n,老Mac是\r。如果你把这三种换行混在一个文件里,很多文本处理工具都会出现怪现象。工具里必须有一个“统一换行符”的功能,我一般默认统一成\r\n,毕竟Windows下记事本打开最正常。
2.4 号码筛选:不只是正则那么简单
号码筛选是这个工具里最贴合“中文互联网日常需求”的功能。热搜词里有“wifi密码本txt”“号码筛选”,可见很多人手里有一堆纯文本的号码簿、通讯录备份、刷卡记录、短信导出文件。做号码筛选,核心是号码格式的归一化。
同一个手机号,可能出现的形式有:
- 13800138000
- 138 0013 8000
- 138-0013-8000
- +86 13800138000
- 13800138000,
筛选的第一步是清洗:把非数字字符去掉,再做区号处理。这里要注意,不能用简单的正则一配就走,因为不同来源的数据格式差异巨大。我的实现逻辑是:
import re def normalize_phone(raw): digits = re.sub(r'[^\d]', '', raw) if digits.startswith('86') and len(digits) == 13: digits = digits[2:] if len(digits) == 11 and digits.startswith('1'): return digits return None这样就能把各种“花式写法”统一成11位手机号,然后再提供两个分支:筛出符合手机号规则的行、筛出不符合规则的行。对于固话号码、400电话、特殊服务号码,可以单独配置规则集。
在实际操作中,我发现这个功能最实用的场景是两个号码文件做差集。比如运营商给了一份今天所有通话记录号码的txt,自己手里有一份业务名单的txt,比对后能快速得到“哪些号码今天联系了我们但不在名单里”“哪些名单里的号码今天没有出现在记录里”,在运营分析里这就是一次很基础的客户触达排查。
2.5 记事本净化:编码、乱码、空行和隐藏符号
“记事本净化”这个名字是我起的,含义是把原本用记事本打开乱成一团、看也看不懂、改也无从下手的TXT,处理成干净、整齐、可读的纯文本文档。它包含四个子任务:
- 编码自动识别与统一:把文件转成UTF-8无BOM,避免乱码。
- 去除空行和多余空格:压缩连续空行,去掉行尾空格。
- 去除不可见控制字符:清掉
\x00、\t、\r等混入的杂散字符。 - 整理段落与格式:对于小说、歌词、字幕类文本,把强制换行合并为自然段落。
编码问题一定要单独拿出来说。Windows记事本的默认编码在不同系统版本、不同区域设置下不一样,可能是ANSI(本地代码页,比如GBK)、UTF-8、UTF-16。很多老系统的导出文件用的是GBK,你用Python直接按UTF-8读取就会报错或者产生乱码。我的工具做了编码探测,参考了类似chardet的做法——如果探测不到就显示二进制内容摘要,绝不闷头“猜”着打开然后让你拿到一屏幕乱码。
净化之后的文本,应该能做到:用记事本打开不乱码、用Excel导入不乱列、用数据库导入不报错、用搜索引擎检索能准确命中关键词。这就是“净化”的价值。
3. 工程细节与性能优化
3.1 大文件如何做到秒级打开
TXT文件一旦超过几百MB,几乎所有文本编辑器都会卡成狗。原因在于很多编辑器把整个文件读入内存并在界面上注册UI状态,行数越多,界面开销越大。而我的工具做的是“流式处理”,也就是说,打开文件时不把全部内容一次性读进内存,而是只读取尾部少量数据用于判断行数和编码,内容按需分页读取。
这里有个小技巧能极大提升体验:索引行偏移。也就是第一次扫描文件时,记录下每一行的起始位置偏移量,存到内存中的偏移表里。之后需要查看第N行时,直接根据偏移跳到那个位置读一行,不用从文件头重新读。
一个500万行、800MB的文件,建立行偏移索引大约需要3-5秒,之后翻页和定位都是瞬间完成。这个看起来不起眼的“打开快”功能,在实际使用中几乎决定了一个工具能不能用——用户没有耐心等10秒只为看个开头。
3.2 编码自动识别与BOM处理
UTF-8 BOM是个让人又爱又恨的东西。Windows记事本保存UTF-8文件时,默认带BOM(即文件开头三个字节EF BB BF)。很多程序读这种文件没问题,但另一些程序(比如很早的PHP解析器、某些Java的BufferedReader、一些数据库导入工具)会把这个BOM当成文本内容一起读进去,导致第一行第一个字段莫名其妙多了一个看不见的字符。
我的工具处理原则是:打开时保留探测到的编码信息,保存时由用户决定带不带BOM。如果用户选择UTF-8无BOM,我就删掉开头三个字节;选择UTF-8带BOM,我就加回去。这个功能我几乎天天用,因为很多线上系统导入TXT时对BOM极其敏感。
编码识别还有一个冷门坑:GBK和BIG5的区别。简体中文文本用GBK编码,繁体中文文本可能用BIG5编码。两者在某些字节区间会重叠,如果只靠简单的字节统计,很容易把繁体文件误判成GBK,导致出现“锟斤拷”那种经典乱码。我的经验是:不能只看编码名,要把内容里的常用字频统计进去,比如“的”“了”“是”这类字在GBK下的字节模式出现的频率,再和BIG5下的常见模式做对比,准确率会高很多。
3.3 去重算法的选择与适用边界
去重算法我实测下来可以分成四档,每一档适用于不同的数据规模:
| 数据规模 | 推荐方案 | 说明 |
|---|---|---|
| 1万行以内 | Python set / JS Set 直接去重 | 一行代码搞定,快且简单 |
| 1万-100万行 | set去重,但注意按行清洗后再入set | 内存可控,性能足够 |
| 100万-1000万行 | 哈希分桶+桶内set去重 | 内存占用稳定在几百MB |
| 超大文件(>1000万行) | 外部排序思想:按行哈希分桶后,桶内再分桶,或者直接借助数据库唯一索引 | 时间换空间,或者用SQLite临时表 |
这里我要特别强调一下大数据量下的去重误区。很多人一提到大数据就去用数据库,比如把TXT导入SQLite或MySQL,再对字段加唯一索引,通过INSERT IGNORE或者GROUP BY去重。这思路没错,但忽略了导入本身的成本。一个五百万行的TXT,即使数据库导入也要几十秒,而分桶去重方案的时间里大部分也是花在I/O上,两者差距不大。如果只是去重这一个需求,没必要引入数据库这么重的依赖,除非你接下来还要做多表关联、聚合统计等更复杂的操作。
另外一个很实际的问题是:去重要不要保留最后一条重复记录?默认情况下,大多数工具保留第一次出现的记录。但有些业务场景要求保留最后一次出现的记录,比如某种配置文件里后面出现的配置覆盖前面的。我的工具支持二选一,实现上就是遍历时持续更新位置映射,最后按位置映射输出。
4. 从热搜词看到的高频使用场景
我写这篇文章之前,顺手整理了一批和TXT批量文本处理相关的热搜词,结果发现用户需求比我预想的要丰富得多。这些关键词本身就是一份“这个工具该往哪里使劲”的需求清单。
4.1 SQL去重与文本清洗的配合
“sql语句去重”“mssql 去重 多表查询”“sql语句去重查询”这几个词的搜索量说明一个问题:很多人手里攒着大量数据,第一反应不是用文本工具处理,而是想直接用SQL。但实际上SQL去重有个前提,你得先把数据导入数据库。而导入这一步往往卡在数据格式上——行尾有分号、字段里有引号、空行太多、编码不对,随便一个都能让批量导入失败。
我自己常用的路径是:先用TXT工具做数据清洗(去空行、去首尾空格、统一编码、去除重复行),再把清洗后的文件导入数据库,最后写SQL做复杂去重和查询。这样分工的好处是各干各的专长:文本工具擅长廉价地处理“脏格式”,数据库擅长高效率地处理“复杂关系”。
4.2 小说TXT转换与章节整理
“蕃茄小说txt转换器”“番茄小说导出txt工具”“网页小说链接复制提取txt文档”这一类热搜词背后,是大量喜欢把网页小说整理成离线TXT的人。这个需求虽然简单,但讲究也真不少。
从网页复制下来的小说内容,通常有大量站点广告、片头片尾、分页导航文字(比如“上一章”“下一章”“返回目录”)。整理成干净的txt,需要三步:第一步用正则把广告行和导航行删掉;第二步把章节标题统一成“第X章 XXX”的格式;第三步合并零散段落,让正文不乱。
举个例子,网页复制的文本经常长这样:
上一章 第3章 夜色下的约定 内容段落... (本章未完,请点击下一页继续阅读) 下一章我用正则批量清理:
删除:^\s*[((]本章未完[^)]*[))]\s*$ 删除:^(上一章|下一章|返回目录)\s*$清理完之后再用空行压缩和段落合并,一部几百万字的小说,从源网页到干净TXT,十分钟之内就能搞定。这就是“记事本净化”功能在小说场景下的实际应用。
4.3 词典、五笔编码和号码簿的处理
热搜词里有两类特别有意思:一类是“词典txt”“mp3词典文件免费txt”“86五笔三级简码完整txt下载”,另一类是“wifi密码本txt”。这些本质上都是编码与密钥类纯文本资源。处理这类资源时,关键不是清洗,而是精确的增删改查和去重。五笔编码表可能有上万行,每行格式是“汉字+编码”,如果我想给某个字追加新的编码,或者把某个编码统一修改,手工操作完全不可行,必须用批量替换。
我还见过有人拿这个工具处理wifi密码本txt,那是从某个设备导出的热点名称和密码列表,格式是SSID:密码。用户需要验证哪些密码是重复的、哪些SSID缺失。这种场景用“按冒号分割取key去重”的功能就很方便:我提供一个自定义分隔符去重的选项,用户指定一行中哪个字段作为去重依据,而不是整行内容作为依据。这是一个非常重要但很多工具都不提供的功能。
4.4 数据导出文件与程序日志的清洗
“pcap流量数据包中有txt文件”“c# listview项保存到txt文件”“c#中读写.txt文件”“bin文件怎么转换成txt”这些热搜词说明,很多开发者会跟TXT文件打各种奇怪的交道。数据包导出、程序配置导出、二进制文件转换,最终落地往往都是一堆TXT。这些文件通常含有大量看不出来但打印得出来的字符,比如空字节、制表符、换页符。
我用工具清洗这类文件时,最常用的是“按可见性过滤”功能——把不可见字符单独高亮或剔除。配合十六进制预览模式,能看清文件底层到底是什么内容。这个功能在排查程序生成的文件格式问题、或者手动修一个损坏的配置文件时,能省去大量折腾时间。
5. 实测踩坑记录与经验总结
工具写出来是一回事,用起来不崩是另一回事。下面这几个坑,是我在实际操作中一遍一遍踩出来的,写出来给各位提个醒。
5.1 去重后行序漂移
先说最隐蔽的一个坑:集合去重后顺序全乱的问题。如果直接用Python的set把行加进去再遍历输出,得到的结果顺序是随机的,因为set不保证插入顺序。这个问题在数据量小时不明显,可一旦数据量大,用户发现去重后的文件顺序和原文件完全对不上,基本就会直接判定工具不可用。
解决办法是维护一个顺序列表和集合同步:列表记录“哪些行被留下了”,集合负责快速判断“这个行是否出现过”。
order = [] seen = set() for line in lines: if line not in seen: seen.add(line) order.append(line)这其实是一个极其简单的改动,但它决定了工具是否专业。
5.2 全角半角符号导致比对失败
第二个坑来自字符集的“傲慢与偏见”。两份来源不同的号码文件,一份全是半角数字“13800138000”,另一份可能是某个老系统导出的全角数字“13800138000”。肉眼看一模一样,程序比对出来却完全不同。这个坑在文本处理里比乱码还要隐蔽,因为界面上看起来完全正常。
我的工具专门加了一个“一键全角转半角”选项,并且把它集成到了去重、比对、号码筛选三个功能的前置操作里。处理任何不确定来源的数据时,我都会先做一步全角转半角,宁可多转换一次,也不要最后比对结果和预期差十万八千里。
5.3 误操作、大文件崩溃与恢复机制
还有一个我必须承认的教训:工具再顺手,也挡不住用户手滑。我自己就干过一件事——想删除某文件中包含“测试”两字的行,结果正则写错,删掉了三千多行有效数据,然后直接点了保存。那时候我的工具还没有恢复机制,文件就彻底没了。
从那以后我加了两个机制,一个是自动生成备份文件,格式为原文件名.bak_时间戳.txt,默认在每次写操作前自动创建;一个是支持“回滚本次操作”,在内存中保留上一版的关键元数据。现在我用这个工具处理任何重要数据的铁律是:
批量操作前,永远先复制一份原始文件到同目录的_备份文件夹里。宁可多此一举,不要追悔莫及。
5.4 不同来源的txt内容行尾隐藏差异
最后一个容易让人懵圈的坑是行尾的隐藏差异。很多时候你用编辑器的十六进制模式看一眼,才发现某一行末尾除了\n之外还有\r,或者某两行之间有肉眼看不见的\x00空字节。这些字符不影响阅读,但会影响比对和去重的结果——因为“看起来一样的两行”,在程序眼里内容并不一样。
我的建议是:在批量处理前先执行一次“净化-移除无关字符”操作,把行尾统一成\n、移除NUL字节、压缩连续空格。这不会伤害数据可读性,但会极大减少后续逻辑判断出错的概率。
6. 工具的使用流程与实际边界
最后总结一下我平时用这个工具处理一次完整任务的流程,给各位一个参考:
- 确认文件来源,先做编码探测,确保打开无乱码。
- 做基础清洗:统一换行符、全角转半角、去BOM、去不可见字符。
- 根据任务目标选择主操作:去重、比对、筛选、替换。
- 执行前使用预览功能,确认结果符合预期。
- 确认无误后写回,写回前保留备份。
这个工具的边界,我也要说清楚。它不适合做复杂的跨文件关联逻辑,比如要根据另一个文件的内容来修改当前文件,这种需要写真正的脚本;它也不适合做需要高度视觉化对比的场景,那种还是用专业的文件对比软件更合适。它的定位从来都不是替代程序员写脚本,而是让90%的“数据整理零碎活”可以不用打开IDE就轻松完成。
我在开发这个工具的过程中,最大的感受就是:TXT看似简单,实际上承载的数据形态千奇百怪。真正做文本工具时,考验你的不是多么高深的算法,而是能否把每一类“脏数据”的常见特征都提前想到。把上面这些逻辑理清楚之后,你也能做出一个属于自己的批量文本处理工具。