手上有一堆从系统导出、从数据库拷出来、从聊天记录粘贴下来的纯文本,打开一看:满屏的空行、行首一串空格、行尾几个制表符、文件末尾还挂着三五行的空白——这种文件拿去给下游程序解析,轻则报错,重则静默解析出空记录。更麻烦的是,这种文件动辄几万行、几十万行,用记事本手删到天亮也删不完,用某些编辑器自带的"删除空行"功能又只能去空行,行内的空格和制表符纹丝不动。所以用 Windows 自带的 bat 批处理写个小工具,把空行、空格、制表符、末尾空行一次性清干净,是最省事也最可控的做法,不装任何软件,双击或者拖拽就能用,适合运维、数据整理、日志分析、文本预处理这类场景。下面我把自己反复改过好几版的实现思路、脚本全文、踩坑记录一次讲透,新手照着抄就能跑,有一定基础的可以直接跳到参数设计和排查表那几节。
1. 先把需求拆明白:四个动作不是一回事
1.1 空行、空格、制表符、末尾空行各自的语义
很多人一上来就写findstr /v "^$",结果发现文件里"看起来是空行"的行根本没被删掉,原因就是没搞清这四个词在文本层面各自指什么。空行指的是行内一个字符都没有、只有一个行结束符(CRLF)的行;纯空白行指的是行内只有空格或制表符、视觉上跟空行一样、但字节上长度不为零的行,这两者在正则里是完全不同的东西,只匹配^$是抓不到后者的。空格通常指的是 ASCII 0x20,而制表符是 0x09,它们在 CMD 里可以被当作分隔符,但在文本里就是普通字符,如果不显式处理,删除空行的时候它们会原封不动留下来。
末尾空行又是另一种情况。一个正常的文本文件最后一行结尾会有一个换行符,这是文件格式本身要求的,不算"多余空行";真正需要清理的是文件尾部连续多个空行,比如导出工具在数据后面又多敲了几个回车。如果不删,某些解析程序会把它当成一条空记录,或者在 CSV 里多出一行空字段。所以严格讲,末尾空行不是单独的一类字符,而是"连续空行出现在文件尾部"这个位置特征,处理方式是先删所有空行,再单独判断文件尾部是否还要额外收紧。
1.2 "删除空格"到底删哪里的空格
这是整个需求里最容易理解错的一处。标题里说"删除空格、制表符",字面上有两种解释,做出来的结果天差地别。第一种是去掉每行首尾的空白字符,也就是俗称的 trim,行内部的空格保持原样,这样英文句子、带空格的路径、SQL 语句都不会被破坏;第二种是删掉行内所有空格和制表符,也就是 strip,这种操作会把hello world变成helloworld,把select * from t变成select*fromt,对自然语言文本几乎是灾难性的。
我的做法是把这两种语义都做成开关,默认走 trim 模式,因为绝大多数场景下人们真正想干的就是"去掉行首缩进和行尾的制表符对齐",而不是把整行揉成一团。真正需要 strip 的场景一般是处理机器生成的定长文本、去掉从网页复制来的多余空格、或者给某些不接受空格的字段做预处理。这个判断必须在写代码之前完成,否则后面无论怎么写都是错的。
1.3 为什么不直接用编辑器,而要用 bat
可视化编辑器确实能删空行,但有几个绕不过去的坎:一是大多数编辑器的"删除空行"是一次性动作,处理十万行以上的文件会卡死甚至崩溃;二是编辑器通常只处理空行,不处理纯空白行和行首尾空白;三是这类清洗往往不是一次性的,比如每天从系统导出的日志都要先洗一遍,这时候你就需要一个可以放到计划任务里、可以批处理整个目录的自动化脚本。
bat 的价值就在这里:它是 Windows 上零依赖的原生方案,不需要装 Python、不需要装任何运行时,拷贝一个.bat文件到任何一台 Windows 机器上都能跑,文件大小只有几 KB。而且它可以直接读取系统里已经存在的编码和环境,把清洗逻辑固化成文件,比"每次打开编辑器点几下菜单"可靠得多。我自己的经验是,凡是重复超过三次的文本处理,就值得写成脚本固化下来。
2. 三条技术路线怎么选:findstr、for、PowerShell
2.1 findstr 路线:大文件秒级出结果
findstr是 Windows 自带的字符串查找工具,它最被低估的能力是"反向匹配",也就是-v参数配合正则,把匹配到的行直接扔掉。删空行本质上就是"扔掉所有空行",所以一行命令就够:findstr /v /r "^$" input.txt > output.txt。这个写法的可怕之处在于速度,它不做逐行字符串拼接、不开变量环境,直接流式扫描,我实测一个 4 MB、八万多行的文件,处理耗时在零点几秒,跟 for 循环完全不是一个量级。
但它有两个硬伤必须知道。第一,它只能"整行匹配后丢弃",没法做 trim,也没法逐行改写,所以它只能解决空行和纯空白行的问题,删不掉行中间的空格。第二,findstr的正则能力非常有限,不支持\t这种转义,你要匹配制表符,脚本里就必须放一个真实的制表符字符,这在不同编辑器之间复制代码时极易丢失。所以 findstr 适合做"清理的第一道工序",或者你只关心空行的场景。
2.2 for /f 路线:可控性最强,代价是慢
for /f是批处理里唯一能逐行读取文件并逐行改写的结构,它给你完全的控制权:每一行读进来,你能判断、能替换、能拼接、能计数、能决定写不写。我最后的主方案就是它,因为它能在一个脚本里同时实现删空行、删纯空白行、trim、strip、统计行数、自动备份这几件事,而且纯 bat 无外部依赖。
代价也很明显。每次循环都要做一次set变量赋值和延迟扩展,处理速度跟 findstr 差两个数量级,十万行级别的文件可能要跑几十秒。这不是脚本写得不好,而是 CMD 解释器本身的机制决定的上限,所以我的建议是:五万行以内用 for 方案,五万行以上先考虑 findstr 或者 PowerShell。另外for /f有几个祖传的坑,我在第 3 节单独展开,那部分才是真正值钱的东西。
2.3 PowerShell 混合路线:编码和特殊字符最稳
如果你不排斥在 bat 里调一行 PowerShell,这条路线的性价比其实是最高的。Windows 7 之后系统自带 PowerShell,用powershell -NoProfile -Command直接执行一串管道,Get-Content读、Trim()去首尾空白、Where-Object过滤空行、Set-Content写回,一行搞定所有需求,而且-Encoding参数能显式指定输入输出编码,中文完全可控。
它的短板是启动开销,每次调用大概要几百毫秒到一秒,处理单个小文件感觉不明显,但如果你要循环处理一万个文件,累加起来的启动时间就很可观了。另外命令里嵌套引号比较绕,路径里带单引号会直接出问题。我一般把 PowerShell 当成"兜底方案":纯 bat 搞不定的编码问题、含大量特殊字符的文本、需要严格 trim 的场景,都交给它。
| 路线 | 处理速度(4MB 文件实测) | 能否 trim | 编码控制 | 特殊字符安全 | 适用场景 |
|---|---|---|---|---|---|
| findstr | 0.2 至 0.5 秒 | 否 | 字节级转发,最稳 | 安全 | 大文件去空行、第一道工序 |
| for /f 纯 bat | 30 至 60 秒 | 可以 | 依赖当前代码页 | 需小心处理 | 中小文件、需要完整清洗 |
| PowerShell 混合 | 1.5 至 3 秒 | 可以 | 显式指定,最灵活 | 安全 | 编码复杂、含特殊符号 |
3. for /f 的五个隐藏坑,不知道必然翻车
3.1 默认 eol=; 会悄悄吃掉所有分号开头的行
这是最阴险的一个坑。for /f的默认行为里有一项eol=;,意思是"以分号开头的行视为注释,直接跳过"。你以为自己在读文件,其实你读到的只是文件的一部分。如果被处理的是代码文件、配置文件、CSV(有些导出的 CSV 第一列恰好是分号),那丢的数据悄无声息,你还以为清洗成功了。
解决办法是在选项里显式写一个空的eol=。注意写法:for /f "eol= delims=" %%L in ("%SRC%"),eol=后面紧跟一个空格再接下一个选项,表示"不指定任何行尾注释字符"。这个写法看起来很别扭,但确实是正确形式。我见过太多脚本因为这个丢行,尤其处理 SQL 脚本和日志的时候,因为分号太常见了。
3.2 for /f 跳不过"纯空格行",但会跳过真空行
for /f默认的行处理机制里有一步"分词",它会先去掉行首的分隔符,然后把结果给到变量。对于真正的空行,它默认直接跳过,一个 token 都没有;但对于"由若干空格组成、没有其他内容"的行,它的处理结果也是一个空 token,行为上同样被跳过;可是当delims=被设成空(即不指定分隔符)时,整行会被完整保留,包括那些前导空格,此时纯空白行不再被自动跳过。
这个行为差异导致脚本的健壮性完全取决于你怎么写。我的做法是不依赖这个隐式行为,而是在循环体里显式判断:把行内的空格和制表符全部替换掉,看剩下的内容是否为空,为空就丢弃。这样无论delims怎么设、行内是空格还是制表符还是两者混排,判断结果都一致,逻辑上也没有歧义。多写两行set,换来的可预期性非常值。
3.3 延迟扩展与感叹号丢失的关系
逐行改写必须用延迟扩展enabledelayedexpansion,因为你在同一个括号块里既set变量又读变量,用%var%读到的是块开始解析时的旧值。开启延迟扩展后变量用!var!读,逻辑就正确了。但它带来一个副作用:行内的感叹号会被解释器吃掉。如果你的文本里有!,比如感叹句、某些配置文件、LaTeX 片段,输出就会缺字符。
权衡的结论是这样:如果文本里几乎没有!,直接开延迟扩展最省事;如果文本里有大量!,要么改用setlocal disabledelayedexpansion配合call技巧(写法绕但保真),要么干脆走 PowerShell 路线。我的脚本把这一点写进了注释里,因为我自己就因为这个问题丢过几百行感叹句,排查了半天才反应过来是解释器干的。
3.4 制表符在脚本里必须是真字符
处理制表符时最典型的翻车现场是:代码是从网页或者聊天记录里复制过来的,里面那个"制表符"其实是几个空格,脚本跑起来看着正常,但文件里的制表符一个都没删掉,你还以为是逻辑写错了。制表符在纯文本里无法用可读的方式表达,bat 又不认识\t这种转义,唯一的办法就是在脚本里真的敲一个 Tab 字符进去。
我的做法是专门定义一个变量set "TAB=<真实制表符>",在用过的地方统一引用%TAB%,这样脚本里只有一个位置需要小心,改起来也方便。用 Notepad++ 或者 VS Code 打开脚本时,开启"显示空白字符"就能看到那个箭头符号,确认它是真的 Tab 而不是空格。这一步一定要检查,我周围至少三个人栽在这上面。
3.5 编码和 BOM:中文最容易出问题的地方
for /f读文件时是按照当前控制台代码页去解码字节流的。中文 Windows 默认代码页是 936(GBK),如果被处理的是 UTF-8 编码的文件,中文部分就会被错误解码,写回去的时候再按 GBK 编码,结果就是乱码或者某些字被吞掉。这一点很多人没意识到,因为纯 ASCII 文件不受影响,一处理中文就出问题。
处理办法是两条:一是在脚本开头按需切换代码页,处理 UTF-8 文件时先chcp 65001,处理完之后可以再切回 936,避免影响终端里的中文显示;二是尽量用 findstr 或者 PowerShell 路线,findstr 是字节级的流式转发,不做字符解码,所以不会破坏原有编码,PowerShell 则可以用-Encoding参数精确指定。另外 UTF-8 BOM 会被当成第一行的内容读进来,它不是空格,trim 不掉,如果下游程序对 BOM 敏感,得单独处理。
4. 完整脚本实操:从空文件到能用的清洗工具
4.1 脚本全文
下面这个脚本我用了很久,支持三种模式、自动备份、输出统计,还能直接把文件拖到脚本图标上运行。保存成CleanText.bat,注意保存时编码选 ANSI 或者带说明的 UTF-8,否则脚本里的中文提示会乱码。
@echo off setlocal enabledelayedexpansion title 文本清洗工具 set "SRC=%~f1" set "MODE=%~2" set "CP=%~3" if "%SRC%"=="" goto :Usage if not exist "%SRC%" (echo [错误] 找不到文件: %SRC% & goto :Usage) if "%MODE%"=="" set "MODE=trim" if not "%CP%"=="" chcp %CP% >nul rem --- 等号右边是一个真实的制表符,复制代码时务必确认别变成空格 --- set "TAB= " set "DST=%~dpn1_clean%~x1" if /i "%SRC%"=="%DST%" set "DST=%~dpn1_cleanout%~x1" copy /y "%SRC%" "%SRC%.bak" >nul 2>&1 set /a N_ALL=0 set /a N_KEEP=0 set /a N_DROP=0 > "%DST%" ( for /f "eol= delims=" %%L in ("%SRC%") do ( set /a N_ALL+=1 set "LN=%%L" rem 把行内空格和制表符全部抹掉,只用来判断这行是不是纯空白 set "PROBE=!LN: =!" set "PROBE=!PROBE:%TAB%=!" if "!PROBE!"=="" ( set /a N_DROP+=1 ) else ( if /i "%MODE%"=="strip" ( set "OUT=!PROBE!" ) else ( call :DoTrim "LN" set "OUT=!LN!" ) echo(!OUT! set /a N_KEEP+=1 ) ) ) echo. echo 源文件 : %SRC% echo 输出文件 : %DST% echo 处理模式 : %MODE% echo 总行数 : %N_ALL% echo 保留行数 : %N_KEEP% echo 丢弃空白行 : %N_DROP% goto :eof :DoTrim setlocal enabledelayedexpansion set "S=!%~1!" rem 去掉行首的空格和制表符 for /f "eol= tokens=* delims=%TAB% " %%A in ("!S!") do set "S=%%A" rem 去掉行尾的空格和制表符 :TrimTail if "!S!"=="" goto :TrimEnd if "!S:~-1!"==" " goto :TrimOne if "!S:~-1!"=="%TAB%" goto :TrimOne goto :TrimEnd :TrimOne set "S=!S:~0,-1!" goto :TrimTail :TrimEnd endlocal & set "%~1=%S%" exit /b :Usage echo 用法: %~nx0 "文件路径" [trim^|strip^|blank] [代码页] echo trim : 删除空行和纯空白行,并去掉每行首尾的空格与制表符(默认) echo strip : 删除空行,并把行内所有空格与制表符一并删除 echo blank : 只删除空行和纯空白行,行内字符保持原样 echo. echo 示例: %~nx0 "D:\data\list.txt" trim 65001 goto :eof4.2 逐段拆解:每一行为什么这么写
开头的参数解析用%~f1而不是%1,是为了拿到绝对路径,这样后面拼输出文件名的时候不用担心相对路径和当前工作目录的影响。第二个参数是模式,第三个参数是代码页,都做了默认值兜底,所以最简用法就是只给一个文件名,直接跑默认的 trim 模式。
输出文件名我用%~dpn1_clean%~x1拼出来,也就是在原名后面加_clean再带原扩展名,好处是永远不覆盖源文件,符合"清洗工具不应该直接改原文件"的原则。我还加了一层保险:如果算出来的输出路径和源文件路径完全相同,就换成_cleanout后缀,避免出现自己读自己写的死循环。备份那行copy /y是习惯动作,处理几万行的数据时,心里有个兜底版本会踏实很多。
主体循环外面套的> "%DST%" ( ... )是个关键优化。如果把重定向写在循环里的每一行 echo 上,CMD 会为每一行重新打开、写入、关闭文件,几万行下来光是文件句柄操作就能把时间翻好几倍。把重定向提到循环外面,整个块共享一个文件句柄,写入是顺序追加的,速度差别很直观。这一点在脚本里看不出门道,但实际性能差距非常明显。
循环体里的顺序是有讲究的:先把行读进LN,再用PROBE做"空白判定"。PROBE只服务于一件事,就是回答"这行除了空格和制表符还有没有别的字符",所以它把空格和制表符全删掉,剩下的内容是否为空就是判定结果。这个判定和后面实际怎么输出是完全解耦的,所以三种模式共用同一套判断逻辑,只有输出那一行不同。这种写法比在三种模式里各写一遍判断要可靠得多。
echo(!OUT!这个写法可能有人没见过。普通的echo %var%在变量为空时会输出当前 echo 的开关状态(比如ECHO is off.),而echo(这个左括号形式可以安全地输出空内容或者以特殊字符开头的内容,是批处理里公认的稳妥写法。虽然我们的逻辑已经保证OUT非空,但用更稳的写法没有坏处。
4.3 trim 子过程为什么要单独拎出来
行首的空白比较好办,for /f "tokens=*"会自动把前导分隔符吃掉,所以一句for /f "eol= tokens=* delims=%TAB% " %%A in ("!S!") do set "S=%%A"就能解决。注意这里delims里同时放了制表符和空格,两个都会被当作前导分隔符处理;eol=也必须显式置空,否则以分号开头的行会被整行丢掉。
麻烦的是行尾。CMD 没有内置的RTrim,字符串操作只有:~这一套切片。所以我的做法是从末尾一个字符一个字符往回砍,遇到空格或者制表符就砍掉一个,再判断新的末尾,直到末尾既不是空格也不是制表符为止。这段逻辑放在循环里会反复解析标签,所以我把它抽成一个独立的子过程:DoTrim,用call调用,通过传参把变量名带进去,返回值用endlocal & set "%~1=%S%"这个经典技巧把局部作用域里的结果带出来。
这里要说清一个局限:因为开了延迟扩展,!字符在这一段里会被吃掉。如果你的文本里有感叹号,strip模式反而是安全的(因为它不调用子过程),但trim模式会丢字符。这是纯 bat 方案的天花板,不是写法问题,是解释器机制造成的。真要严格保真,第 2.3 节的 PowerShell 路线一行就解决了,没必要跟 CMD 死磕。
4.4 三种运行方式和实测数据
最简单的运行方式是命令行:CleanText.bat "D:\data\list.txt",后面可以跟模式和代码页。第二种是把文件直接拖到脚本图标上,Windows 会自动把文件路径作为第一个参数传进来,我用%~f1接住的正是它。第三种是放进一个 for 循环里批量处理整个目录,这个在第 6 节展开。
实测数据我拿两份文件跑过:一份是 8.7 万行、4.2 MB 的 CSV 导出,一份是 1.2 万行、600 KB 的日志片段。纯 for 方案在处理大文件时大约 40 秒到 1 分钟,小文件 3 到 5 秒;findstr 方案大文件不到 1 秒;PowerShell 方案大文件大约 2 秒出头。这个量级差异是稳定的,不是你机器快就能弥补的,所以选型真的要看文件规模。
验证结果我一般做三步:先用find /c /v "" 输出文件数一下行数,看是否和脚本统计的保留行数一致;再用fc对比源文件和备份文件,确认除了空白之外没有别的内容被改动;最后用记事本打开输出文件,把光标移到文件末尾,看最后一行是不是有内容。三步都过,这次清洗才算放心。
5. 常见问题与排查速查表
5.1 结果文件乱码或者中文丢字
最常见的表现是输出文件里的中文变成问号、方块,或者部分汉字消失。原因基本就一个:源文件的编码和当前代码页不匹配。解决办法是先确认源文件编码(Notepad++ 右下角能看到),然后处理时显式指定代码页,UTF-8 文件用CleanText.bat "文件.txt" trim 65001,GBK 文件保持默认或者显式写 936。如果换代码页之后仍有问题,直接用 PowerShell 路线并加上-Encoding UTF8,这条路基本不会翻车。
还有一种容易被忽略的情况:源文件头部带 UTF-8 BOM,第一行前面会多出三个不可见字节,trim 处理不掉,下游程序可能因此报格式错误。处理办法是用支持去 BOM 的编辑器另存一次,或者用 PowerShell 的-Encoding utf8NoBOM参数写回。
5.2 行数变少但"不该少的也少了"
结果统计显示丢弃了很多行,但你明明记得文件里没那么多空行,这时候先检查三件事。第一,eol=有没有写成eol=;或者干脆没写,导致分号开头的行被当作注释吃掉;第二,delims有没有被误设成某个字符,导致以该字符开头的行被拆分;第三,文本里是不是有超长行,for /f对单行长度有上限,超过之后可能被截断甚至整行丢弃,这在处理不含换行的 JSON 或者某些导出数据时会出现。
排查方法很朴素:先拿文件的前 100 行单独跑一遍,用fc跟源文件对比,看差异出现在哪一行,问题立刻聚焦。我一般会先切一个样本文件出来验证脚本逻辑,确认无误再跑全量,这个习惯帮我省了很多时间。
5.3 大文件跑到一半卡住或者内存飙升
for /f逐行处理本身不需要把整个文件载入内存,所以正常不会内存飙升。真正容易出问题的是把每行内容往变量里累积(比如先收集所有行再一次性输出),那种写法在几十万行时会直接把环境变量空间撑爆。我的脚本是边读边写的流式结构,不存在这个问题,但如果你基于它改出了"先攒后写"的版本,要特别小心。
另外call子过程在循环里被高频调用也会拖慢速度,trim模式下每行都要进一次子过程,这是纯 bat 方案慢的主要原因之一。如果你的文件很大又必须 trim,直接跳到 PowerShell 路线,别硬扛。
| 现象 | 可能原因 | 快速验证 | 处理方式 |
|---|---|---|---|
| 中文乱码 | 代码页与文件编码不匹配 | 用编辑器看文件编码 | 加代码页参数或改用 PowerShell |
| 行数莫名减少 | eol 默认值吃掉分号开头行 | 检查 for 选项是否写了 eol= | 显式写 eol= |
| 制表符没删掉 | 脚本里的 Tab 被存成了空格 | 开启显示空白字符查看 | 重新敲入真实 Tab |
| 感叹号消失 | 延迟扩展吞字符 | 找一行含感叹号的文本测 | 改用 strip 或 PowerShell |
| 结尾多一行空行 | 尾部连续 CRLF | 用十六进制查看器看末尾 | 删空行后可选去掉末尾换行 |
| 输出文件为空 | 源文件被同时读写或路径拼错 | 看备份文件是否存在 | 检查输出路径拼装逻辑 |
6. 把清洗工具接进日常流程
6.1 一条命令批量处理整个目录
单文件处理只是起点,实际工作里更常见的是整个目录几十个文件一起洗。做法是在外面再套一层循环,遍历目标目录下的所有文本文件,逐个调脚本。这里有两个细节要注意:一是要在循环里对每个文件分别记录处理前后的行数,方便事后核对;二是输出目录最好独立,别和源目录混在一起,否则下次再跑会把自己的输出当成输入再洗一遍,白白耗时。
@echo off setlocal enabledelayedexpansion set "SRCDIR=%~1" set "DSTDIR=%~2" if not exist "%DSTDIR%" md "%DSTDIR%" for %%F in ("%SRCDIR%\*.txt") do ( echo 正在处理: %%~nxF call "%~dp0CleanText.bat" "%%~fF" trim 65001 move /y "%%~dpnF_clean%%~xF" "%DSTDIR%\" >nul ) echo 全部处理完成写这段的时候有个坑值得一提:move的目标目录如果不存在,移动会失败,所以先md建目录。另外路径末尾千万别手滑多敲一个反斜杠,"D:\out\"这种写法在引号里会让 CMD 把反斜杠当作转义引号,命令直接报错,这是很常见的低级问题。
6.2 和导出数据、日志清洗配合的注意点
如果你的文本是从 Excel 或者数据库导出的 CSV,清洗时要格外小心,因为 CSV 里的字段分隔符本身就是逗号,而字段内容里的空格是有意义的。这种情况下千万不要用 strip 模式,只能用 trim,而且不能把行内的制表符当成噪声删掉,因为有些导出工具正好用制表符做分隔。此时trim模式只会去掉行首行尾的空白,字段内部完好,是安全的选择。
日志文件的处理思路又不一样。日志里常有对齐用的连续空格,trim 之后可读性可能会下降,所以删空行通常就够用了,用blank模式跑一遍即可,好处是完全不碰行内字符,零风险。我在处理服务器日志的时候基本都用blank,只有在往数据库里灌数据时才用trim。
6.3 放进计划任务定时执行
这类清洗脚本很适合做成每日任务,让它在凌晨自动把当天导出的文件洗一遍。用schtasks命令创建任务时要注意两点:一是运行的用户账户要对该目录有读写权限,否则会静默失败;二是脚本里的相对路径要全部换成绝对路径,因为计划任务的工作目录不是你期望的那个目录,相对路径会解析到系统目录去,导致找不到文件。
另外建议在脚本末尾加一句把处理结果追加写入日志文件的逻辑,每天一行,记录日期、文件名、处理前后行数。这样跑了几个月之后,你还能回查某一天的数据是不是被正确处理过,出了问题也有据可依。这个习惯看似多余,但在真正需要排查的时候能救命。
我个人在实际使用中的体会是,这类小工具真正的价值不在于代码有多精妙,而在于它把"重复的、容易出错的、需要人盯着"的操作变成了"双击一下、看一眼统计、关掉"的动作。至于脚本本身,从最简单的findstr /v /r "^$"一行开始,遇到编码问题就加代码页,遇到需要 trim 就换 for 方案,遇到特殊字符就上 PowerShell,按需演进,别一上来就想着写一个万能工具。最后再分享一个小技巧:如果你不确定一份文件到底该用哪种模式,先复制前 200 行出来试跑,用fc对比源文件和输出文件,看清楚到底哪些字符被动了,再决定要不要跑全量。