ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ubuntu下用sed、awk、find批量删除文档空行的完整指南

Ubuntu下用sed、awk、find批量删除文档空行的完整指南

搞运维和数据处理的人,几乎都遇到过这样一个需求:在 Ubuntu 下批量快速去掉文档中所有空行。清理日志、预处理 CSV、整理从别人手里拿到的 SQL 脚本……只要文件一多,手动一行行删空行就是在给自己找罪受。网上的方案零零散散,不是只讲了单个文件,就是漏掉了批量时的各种边界情况。这篇我来把 sed、awk、find 组合起来一次讲透:先帮你认清“空行”的真面目,再给出一套可以直接抄的批量命令,最后把那些容易踩的坑全部摆出来。

1. 动手前先想清楚:你嘴里的“空行”到底长什么样

1.1 三种最容易被混淆的空行类型

先说一个很多人踩过的大坑:你以为自己删的是“空行”,但实际文件里那些行根本不是空的。

我习惯把空行分成三类,批量操作前必须分清楚。

第一类是真正意义上的空行:行内什么都没有,就是一个换行符。这类空行用sed '/^$/d'是百分百能删干净的。

第二类是伪空行:行内看起来是空的,实际上藏着空格或 Tab。特别是从 Windows 记事本、老旧的办公软件、某些网页编辑器里导出的文本,几乎每行后面都会跟一个\r(回车符),空行也不例外。这种行在^$正则眼里不是空行,因为它“有内容”,只是你看不见。

第三类是“连续空行”和“文件末尾缺换行符”的组合情况。比如文件末尾有十几个空行,或者最后一行数据后面没有换行符,批量处理时如果没提前处理,删完空行后文件可能多出一个空行,甚至最后一行的结尾变得不干净。

我自己经历过一个很典型的案例:某次处理一批从线上导出的日志,每个文件 2 万行左右,用sed -i '/^$/d'一把梭,处理完wc -l一看行数确实少了几千,但用cat -A复查时发现大量“伪空行”还稳稳地躺在文件里,等于白干。从那以后我养成了一个习惯:先弄清楚文件里的空行到底属于哪一种,再决定用哪条命令。

1.2 批量操作前先统计:不要盲目出手

在批量修改任何文件之前,我强烈建议你先做一次“摸底调查”。这一步看着简单,但能省掉后面大量返工时间。

统计单个文件里的空行数量,最直接的是:

grep -c '^$' file.txt

grep -c会输出匹配的行数,也就是空行总数。如果文件里一行空行都没有,输出是 0。

如果想知道空行具体在哪些位置,加上-n:

grep -n '^$' file.txt

输出会带行号,非常直观。比如返回15: 空行,说明第 15 行是空行。

批量摸底的话,可以写个简单的 for 循环,把目录下所有.txt文件的空行数量列出来:

for f in *.txt; do count=$(grep -c '^$' "$f") echo "$f : $count" done

看到每个文件的空行数,你就能判断这次清理的规模,也能在清理后通过再次统计对比确认是否真的删干净。这一步我基本每次都会做,不是因为不信任 sed,而是因为“批量操作”最怕的不是命令写错,而是你根本不知道文件的实际状态。

1.3 用 cat -A 看清文档的真实底细

光看表面不够,得把隐藏字符揪出来。Linux 下查看文件隐藏字符的利器是cat -A:

cat -A file.txt | head -20

输出的关键信息是这样:

  • 行尾会显示为$符号
  • 空行是单独一个$
  • 如果行尾有 Windows 的回车符,会显示成^M$
  • 行内的 Tab 会显示成^I
  • 行尾如果有空格,就能看到空格后跟着$

举个例子,一个文件里如果出现$,说明这行不是空行,它有两个空格再加换行符。如果你用sed '/^$/d'去删,它不会被删除。

看到^M$你就知道文件是 CRLF 换行(Windows 风格),这时候直接删空行往往删不干净。看到^I则说明行里有 Tab。

我通常会在批量操作前对每个可疑文件跑一次cat -A,抽查几个样本就够了,重点是确认两件事:文件是不是 CRLF 换行,空行里有没有空格或 Tab。这两点确认完之后,命令的选择才有依据。

2. 单文件去空行:先掌握核心工具,后面批量才不慌

2.1 sed:批量修改文档的绝对主力

所有批量去空行的方案里,我最推荐的是 sed,不是因为它最强大,而是因为它最适合“原地修改文件”这个场景。先看最常用的命令:

sed '/^$/d' file.txt

这条命令会读取file.txt,把匹配到的空行删掉,然后输出到终端。注意:它不会改动原文件。这其实是个很好的特性,刚开始不熟练时可以先用它来预览结果,确认无误后再决定落地。

如果确认效果没问题,想直接改原文件,就加上-i参数:

sed -i '/^$/d' file.txt

-i是 in-place 的意思,GNU sed 的实现方式是:先在同一个目录下创建一个临时文件,把处理结果写入临时文件,处理完后再把临时文件重命名为原文件。所以处理过程中原文件确实会被替换,操作前要想清楚,特别是数据文件。

解释一下这条命令为什么能删空行。sed 默认逐行读入文本,每次把一行放到“模式空间”里执行脚本。这里的脚本是'/^$/d',它包含两部分:地址匹配^$和动作d。在正则表达式里,^匹配行首,$匹配行尾,^$合起来表示“行首紧接着行尾”,也就是空白的一行。后面的d是 sed 的 delete 命令,意思是“删除当前模式空间里的这一行”并进入下一行。不匹配的行不做操作,直接打印到输出。整个逻辑就是:逐行扫描,是空行就删,不是空行就放过。

看起来平平无奇,但后面所有批量命令的核心都是它。

2.2 grep 和 awk 分别在什么场景更顺手

sed 是批量修改的主力,但 grep 和 awk 也不是没有用,它们在某些场景下比 sed 更顺手。

grep 擅长“过滤”。要删除空行,用grep -v '^$'可以把非空行保留下来:

grep -v '^$' file.txt > clean.txt

-v是反向匹配,也就是把匹配^$的行排除掉,剩下的写出去。grep 的好处是它天生就是干“筛选”的,命令一目了然;缺点是它不能像 sed 那样直接-i原地修改,必须重定向到新文件再手动替换,批量操作时就多了一步。所以我的习惯是:单独处理一两个文件、或者想把过滤结果拿去继续管道操作时用 grep;真正要批量修改文件内容时用 sed。

awk 的出场方式更特别:

awk 'NF' file.txt > clean.txt

awk 默认按空白(空格、Tab 等)把每一行切分成字段,NF是当前行的字段数量。空行没有任何字段,所以NF等于 0;非空行至少有一个字段,NF大于 0。awk 'NF'这个写法很简洁,等价于“只打印字段数不为 0 的行”。

这里有个容易被忽略的优点:awk 'NF'不仅能删纯空行,连只有空格或 Tab 的行也会一并删掉。因为 awk 把空白字符当作分隔符,一行即使全是空格,切分后也不会有任何字段,NF依然为 0。如果你遇到一堆“看着是空行”的文件,用 awk 比 sed 的^$更稳。

2.3 工具选型对照和几条关键命令的取舍

把这几个经典命令放在一起对比,方便你以后按场景挑:

工具命令示例是否支持原地修改能否处理全空白行适用场景
sedsed -i '/^$/d' file.txt是(-i)不可以(需扩展正则)批量修改文件首选
grepgrep -v '^$' file.txt > new否不可以查看、管道过滤、单文件输出
awkawk 'NF' file.txt > new否可以(自动)需同时处理空格/Tab 时
perlperl -ni -e 'print if /\S/' file.txt是(-i)可以跨平台兼容、极端场景兜底

如果只想删纯空行,sed 足够;如果文件里有大量“肉眼空行”,直接上awk 'NF'或sed '/^[[:space:]]*$/d'。perl 的命令看着别扭,但它最大优势是跨平台:在 Linux、macOS 上行为一致,很多和我一样经常在两种系统间切换的人喜欢用 perl 兜底。

这里补充一个 sed 处理全空白行的高频命令:

sed -i '/^[[:space:]]*$/d' file.txt

[[:space:]]是 POSIX 字符类,代表空白字符(空格、Tab、换页等);*表示前面字符出现 0 次或多次;^[[:space:]]*$意思就是“整行只有空白字符或什么都没有的行”,所以它既包含纯空行,也包含全空白行。注意不要写成[ \t],虽然也能匹配空格和 Tab,但少匹配了其他空白字符,不够通用。

3. 批量快速去空行的实战命令

3.1 最直接的 for 循环方案:适合目录下的一批文件

如果你要处理的文件都在同一个目录里,文件名也有规律,用 for 循环最简单。比如当前目录下有一堆.txt文件,要去掉所有空行:

for f in *.txt; do sed -i '/^$/d' "$f" done

每轮循环把文件名放进变量f,然后对$f执行 sed 原地删除。这里有两个细节必须注意。

第一,"$f"一定要加双引号。文件名里万一有空格,不加引号会被 shell 拆成多个参数,sed 会误认为你提供了多个文件名,轻则处理错文件,重则报错。第二,如果目录里没有匹配*.txt的文件,bash 默认会把星号当成字面量传给命令,导致 sed 去处理一个名为*.txt的文件,然后报错。为了避免这种尴尬情况,可以在循环前面加一句:

shopt -s nullglob

这样在没有匹配文件时,循环体根本不会执行。

如果你不想用 nullglob,也可以在循环体里加一个文件存在性判断:

for f in *.txt; do [ -f "$f" ] || continue sed -i '/^$/d' "$f" done

这套写法看起来朴素,但胜在直观可控,适合几十个文件以内的场景。

3.2 递归处理文件用 find 更稳:一步到位涵盖子目录

文件分在多个子目录里,或者文件名后缀五花八门,for 循环就有点力不从心了。这时候用 find 才是正解,它能把“找到符合条件的文件”和“对文件执行命令”两步合成一步。

最简单的递归版本:

find . -type f -name "*.txt" -exec sed -i '/^$/d' {} \;

这条命令的意思是:从当前目录开始,查找所有后缀为.txt的普通文件,每找到一个就执行一次sed -i '/^$/d' {}。这里的{}是 find 的占位符,表示当前找到的文件名;结尾的\;是-exec参数的结束标志,注意反斜杠和分号都不能少。

如果要处理多个后缀,可以加扩展名分组:

find . -type f \( -name "*.txt" -o -name "*.csv" \) -exec sed -i '/^$/d' {} \;

-o是逻辑“或”,括号需要转义,否则 shell 会把它当成子 shell 语法,导致命令报错。

还有一类场景是“只处理部分目录,跳过另一部分”。比如项目里有一个vendor或backup目录,里面也有同后缀文件,但不想动它们:

find . -type f -name "*.txt" \ -not -path "./backup/*" \ -exec sed -i '/^$/d' {} \;

这种排除能力是 for 循环很难优雅实现的。如果你的文件不只在当前目录一层,递归处理几乎是唯一靠谱的批量方案。

这里再分享一个性能细节:-exec后面的结尾符号,\;每找到一个文件就启动一个 sed 进程,文件数量多时进程创建开销不小。GNU find 支持把\;换成+:

find . -type f -name "*.txt" -exec sed -i '/^$/d' {} +

+会收集尽可能多的文件名,一次性传给 sed 处理。sed 本身就支持多个文件参数,所以效果一样,但进程数大幅减少。文件数量超过几千个时,用+能明显感受到速度差异。

3.3 文件数量特别大时,用 xargs 并行加速

find 的-exec +已经比\;高效了,但它是单进程串行处理所有文件。如果你的文件数量真的很大,几万个文件起步,想进一步压榨时间,就用 xargs 配合多进程并行。

最稳妥的 xargs 写法:

find . -type f -name "*.log" -print0 | xargs -0 -n1 sed -i '/^$/d'

-print0让 find 用空字符而不是换行分隔文件名,-0让 xargs 也按空字符拆分。这一对必须配合使用,否则遇到含空格、换行的文件名会把命令搞乱。-n1表示每次只向 sed 传递一个文件,等价于一个文件启动一个 sed 进程。

想并行处理,加-P参数:

find . -type f -name "*.log" -print0 | xargs -0 -P 4 -n1 sed -i '/^$/d'

-P 4表示最多同时运行 4 个进程。四路并行时处理速度接近四倍,但要注意 CPU、磁盘 I/O 是否扛得住。我的经验是:普通机械硬盘并发过高反而会卡在 I/O 上,4 到 8 是个比较舒服的区间;SSD 上可以大胆一点。

并行有个前提:处理的是不同文件,且不会互相依赖。sed 原地修改各管各的文件,天然满足这个条件。如果你处理的是共享某个索引的数据库文件之类的场景,就不能轻易并行,否则可能出问题。

3.4 批量操作前的一套安全流程

批量场景下,命令一旦跑起来影响范围是几十上百个文件,所以我总结了一套“先观察、再备份、后执行、终对比”的流程,强烈建议你照做。

第一步:找出所有有空行的文件。

grep -rl '^$' --include='*.txt' .

-r递归,-l只输出文件名。这一步能让你知道哪些文件会被改动,不会傻乎乎地对所有文件执行操作。

第二步:备份。两个选择,一是整个目录复制一份:

cp -a dir dir_bak

二是用 sed 的备份参数,在原文件旁边生成.bak:

find . -type f -name "*.txt" -exec sed -i.bak '/^$/d' {} +

-i.bak会在每个文件旁边生成一个xxx.txt.bak,内容为处理前的原始文件。生成一堆备份文件确实碍眼,但批量操作时这点代价很值得。

第三步:先抽样预览再落地。随机挑一两个文件,用不带-i的命令看效果:

sed '/^$/d' sample.txt | head -30

确认删除的位置和数量符合预期,没有误伤需要保留的内容,再执行真正的-i版本。

第四步:处理后对比。每批文件处理完,立刻用wc -l统计行数,和处理前对比,看实际删掉了多少行,心里有数:

wc -l *.txt

如果删掉的行数与之前grep -c '^$'统计的空行数对不上,一定有哪个环节出了问题,趁早排查。

这套流程看着繁琐,但真正批量操作时全程不到两分钟,却能避免因为一条命令导致几百个文件被改坏。我自己已经养成肌肉记忆,处理任何批量任务都走这套流程。

4. 常见问题与排查实录

4.1 处理完还剩“空行”,多半是空格或 Tab 在捣乱

最经典的翻车现场:跑完sed -i '/^$/d',结果cat -A一看,文件里依然有大量形如$的行。原因就是这些行里包含空格或 Tab,不是真正的空行。

解决办法一是改用能匹配全空白行的正则:

sed -i '/^[[:space:]]*$/d' file.txt

解决办法二是直接上 awk:

awk 'NF' file.txt > clean.txt && mv clean.txt file.txt

如果你在批量场景里,find 加上全空白正则一样好用:

find . -type f -name "*.txt" -exec sed -i '/^[[:space:]]*$/d' {} +

这里特别提醒:处理完一定要用cat -A复查,不要只看less或编辑器里“看起来像空行”就以为成功了。

4.2 文件末尾缺换行符,最后一行处理结果异常

很多编辑器(特别是版本管理工具生成的文本文件)保存时会在文件末尾补一个换行符,但也有些程序生成的文件末尾没有换行。缺换行符的文件在批量处理时可能出现两个怪现象:一是文件最后一行内容被误判;二是处理完后莫名其妙多出一个空行。

先用一条命令检查文件最后一个字节是不是换行符:

tail -c 1 file.txt | od -An -tx1

如果输出是0a,说明最后有换行符;如果输出是其他十六进制数字,说明文件末尾没有换行。遇到这种情况,批量处理前先给所有文件统一补换行:

find . -type f -name "*.txt" -exec sed -i -e '$a\' {} +

$a\的意思是在最后一行之后追加一行,但内容为空,相当于补一个换行符。我见过不少项目里生成的 CSV 末尾少一个换行,批量去空行之后最后一行数据直接被拼接到前一行上,排查了很久才发现是这个原因。

4.3 Windows 换行符导致删不干净

从 Windows 环境拿来的文件,换行符是\r\n(CRLF),而不是 Linux 的\n(LF)。在这种文件里,“空行”实际内容是\r加\n,在 cat -A 下显示为^M$。^$正则匹配的是“行首和行尾之间没有内容”的行,但这里行里有一个\r,所以匹配不到,sed 自然删不掉。

处理顺序必须微调:先统一换行符,再删空行。

单文件操作:

sed -i 's/\r$//' file.txt sed -i '/^$/d' file.txt

批量操作:

find . -type f -name "*.txt" -exec sed -i 's/\r$//' {} + find . -type f -name "*.txt" -exec sed -i '/^$/d' {} +

如果你系统里有dos2unix,也可以先批量转换再删空行,效果一样:

find . -type f -name "*.txt" -exec dos2unix {} + find . -type f -name "*.txt" -exec sed -i '/^$/d' {} +

这里要特别说一下顺序:如果先删空行再转换行符,空行里的\r会阻碍^$匹配,结果就是空行删不干净;反过来,先把\r统一干掉,空行就变成真正的空行,后续正则才能正常匹配。

4.4 误删了需要保留的空行怎么办

有时候并不是要“删除所有空行”,而是觉得连续空行太多,希望压缩成保留一个空行。这种情况下直接全删会破坏排版,比如 SQL 脚本里空行起分组作用,Markdown 里空行分隔段落。

如果你想做“压缩连续空行为一个”,不要用sed '/^$/d',应该用这个逻辑清晰的 awk 命令:

awk 'NF { print; blank=0; next } !blank { print; blank=1 }' file.txt > new.txt

翻译成人话是:遇到非空行,打印并标记“上一个不是空行”;遇到空行,只有在“上一个不是空行”时才打印这个空行,然后标记“上一个就是空行”。这样连续十个空行只会保留第一个,其他全被跳过。

很多发行版自带cat -s也能压缩空行,但它不支持原地修改,需要重定向到临时文件再替换:

cat -s file.txt > new.txt && mv new.txt file.txt

批量时需要循环配合,没有 awk 版本灵活。

如果已经执行了全删命令,才发现需要保留一部分空行,那基本只能从备份里恢复。这就是我反复强调备份的原因:空行删除是不可逆操作,编辑器里 Ctrl+Z 还能撤销,sed 一旦写回原文件,后悔药只有备份能给你。

4.5 跨平台环境下的 sed -i 兼容性问题

如果你不只是用 Ubuntu,偶尔也会在 macOS 的终端里跑同样的命令,会撞上一个非常经典的问题:macOS 自带的 BSD sed 和 Linux 的 GNU sed 对-i参数的要求不一样。

在 Ubuntu 上,sed -i 's///' file完全没问题。在 macOS 上,同样的写法会直接报错,因为 BSD sed 要求-i后面必须带一个备份后缀参数,即使你不需要备份也要写一个空字符串:

# macOS 上这样写 sed -i '' '/^$/d' file.txt

这就导致同一个脚本在两种系统上不能直接通用。如果你是跨平台开发,又不想记住两套语法,我推荐直接用 perl 的一行式,它两个系统上行为一致:

perl -ni -e 'print if /\S/' file.txt

-n表示逐行读取但不自动打印,-i表示原地修改,print if /\S/表示“这行里有非空白字符就打印出来”。由于\S匹配任何非空白字符,这个命令能一并删掉纯空行和只有空格 Tab 的行,实际用起来比 sed 更省心。缺点是需要记得 perl 的语法结构,但花两分钟记住这条命令,以后在各种系统里都能直接用。

5. 写在最后:批量操作前的几个习惯

聊到这儿,批量去空行的核心命令基本都过了一遍,但我还是想多啰嗦几句经验层面的东西。

我自己踩过几次坑之后,现在做文件批量清理时的固定习惯是:第一,永远先grep -rl摸底,宁可多花一分钟看清单,也不盲目跑全量命令;第二,永远用sed -i.bak或目录备份兜底,尤其处理那些源文件不在版本管理里的数据时;第三,删除空行前先用cat -A确认有没有 CRLF 和全空白行,不同的隐藏字符要用不同的正则;第四,批量操作结束后立刻wc -l对比行数,一旦数字对不上,马上用备份恢复并排查原因。

最后再分享一个小技巧:如果你只是偶尔处理一两个文件,完全没必要搬出 find 和 xargs,直接一条sed -i '/^$/d' file.txt就完事了。但只要你面对的是批量场景,就一定要先想清楚“哪些文件会被动、有没有备份、误伤了怎么恢复”。批量命令越强大,越要冷静地控制它。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进