公众号旧文
Linux 中如何从 N 行文件里删除指定的 M 行
如果是在 Windows 上,从 N 行文件里删除指定的 M 行,你是不是需要复制第一行,然后去文件中查找,然后再删除整行,然后再继续删除第二行,循环往复 M 次?
直到我遇见了 Linux
在处理文本文件时,经常会遇到这样一个问题:
有一个包含N 行数据的原文件:
original.txt
还有一个包含M 行待删除内容的名单文件:
delete.txt
目标是:
从
original.txt中删除所有出现在delete.txt里的整行内容,并生成一个新的文件。
一个非常简洁、可靠的做法是使用 grep。
一、先查看哪些行会被删除
执行:
grep -xFf delete.txt original.txt
这条命令不会修改任何文件,它只是从 original.txt 中找出所有命中删除名单的行。
假设:
original.txt
内容是:
apple
banana
orange
grape
watermelon
pear
而:
delete.txt
内容是:
banana
grape
执行:
grep -xFf delete.txt original.txt
输出:
banana
grape
这说明:
original.txt
↓
逐行检查
↓
是否存在于 delete.txt
↓
存在 → 输出
不存在 → 忽略
如果想知道一共命中了多少行,可以接 wc -l:
grep -xFf delete.txt original.txt | wc -l
输出:
2
也就是说,删除名单中的 2 行确实存在于原文件中。
二、真正删除这些行
核心命令是:
grep -vxFf delete.txt original.txt > new.txt
执行后:
new.txt
内容会变成:
apple
orange
watermelon
pear
原文件有 6 行,删除 2 行以后:
6 - 2 = 4
新文件正好剩下 4 行。
三、-xFf 到底是什么意思
这条命令的关键在于:
grep -xFf delete.txt original.txt
可以拆成:
grep
-x
-F
-f delete.txt
original.txt
-f delete.txt
-f 表示:
从一个文件中读取匹配规则。
也就是说:
-f delete.txt
告诉 grep:
不要只匹配一个字符串,而是把
delete.txt中的每一行都当成一个待匹配内容。
例如:
delete.txt
里面有:
banana
grape
pear
实际上等价于告诉 grep:
匹配 banana
或者 grape
或者 pear
因此删除名单可以有几十行、几百行甚至更多,不需要手工写一长串条件。
-F:按普通字符串匹配
grep 默认把匹配内容当作正则表达式。
例如:
.
*
[
]
^
$
这些字符在正则表达式中都有特殊含义。
但很多时候,我们只是想进行:
字符串完全一致比较。
这时使用:
-F
也就是 fixed strings。
它告诉 grep:
不要解析正则表达式,把每一行原封不动地当成普通字符串。
例如删除名单中有:
file.1
使用 -F 后,. 就只是一个普通的点,而不会代表“任意字符”。
因此对于“名单匹配”这种场景,通常使用 -F 更安全。
-x:必须整行完全匹配
-x 表示:
只有整行内容完全一样,才算匹配。
例如原文件中有:
apple
apple pie
pineapple
删除名单中只有:
apple
如果使用:
grep -xFf delete.txt original.txt
只会匹配:
apple
不会匹配:
apple pie
pineapple
这对于“删除指定行”非常重要。
因为通常我们想表达的是:
删除这一整行
而不是:
删除所有包含这个字符串的行
四、-v 为什么能把“查找”变成“删除”
前面的命令:
grep -xFf delete.txt original.txt
含义是:
输出所有匹配删除名单的行。
加入:
-v
以后:
grep -vxFf delete.txt original.txt
意思变成:
输出所有“不匹配删除名单”的行。
-v 就是 invert match,也就是反选。
因此:
没有 -v
逻辑是:
匹配 → 保留
不匹配 → 丢弃
而加上:
-v
以后变成:
匹配 → 丢弃
不匹配 → 保留
这正好实现了“删除指定行”。
五、整个过程的逻辑
可以把它理解为一个集合操作。
假设原文件:
original.txt
包含 N 行。
删除文件:
delete.txt
包含 M 行。
我们真正想得到的是:
new = original - delete
也就是:
原集合减去删除集合。
grep 的处理过程相当于:
读取 original.txt 第一行
↓
检查这一行是否存在于 delete.txt
↓
┌─────────┐
│ │
是 否
│ │
↓ ↓
丢弃 输出
然后继续处理下一行,直到原文件结束。
所以:
grep -vxFf delete.txt original.txt > new.txt
本质上就是:
遍历 N 行
+
使用 M 行作为排除名单
+
输出剩余内容
六、为什么先输出到 new.txt
可能有人会想直接写:
grep -vxFf delete.txt original.txt > original.txt
这是错误的。
原因不是 grep 本身,而是 Shell 的输出重定向机制。
当 Shell 看到:
> original.txt
时,会在启动 grep 之前先把:
original.txt
清空。
因此实际顺序可能变成:
Shell 清空 original.txt
↓
grep 开始读取 original.txt
↓
发现文件已经是空的
↓
最终什么都没有
所以千万不要让输入文件和 > 后面的输出文件是同一个文件。
正确做法是:
grep -vxFf delete.txt original.txt > new.txt
确认结果正确以后,再替换原文件:
mv new.txt original.txt
完整流程:
original.txt
↓
grep 读取
↓
过滤删除名单
↓
new.txt
↓
检查无误
↓
mv
↓
original.txt
七、更加安全的实际操作流程
如果原文件比较重要,可以先备份:
cp original.txt original.txt.bak
然后查看命中内容:
grep -xFf delete.txt original.txt
统计命中数量:
grep -xFf delete.txt original.txt | wc -l
执行删除:
grep -vxFf delete.txt original.txt > new.txt
查看新文件行数:
wc -l original.txt new.txt
最后确认删除名单已经没有任何命中:
grep -xFf delete.txt new.txt
如果没有输出,就表示指定内容已经全部被过滤掉。
确认无误以后:
mv new.txt original.txt
八、一个完整示例
原文件:
original.txt
有 8 行:
A
B
C
D
E
F
G
H
删除名单:
delete.txt
有 3 行:
B
E
G
先检查:
grep -xFf delete.txt original.txt
得到:
B
E
G
统计:
grep -xFf delete.txt original.txt | wc -l
得到:
3
执行删除:
grep -vxFf delete.txt original.txt > new.txt
新文件:
A
C
D
F
H
检查行数:
wc -l original.txt new.txt
可能得到:
8 original.txt
5 new.txt
正好满足:
8 - 3 = 5
九、两条命令可以这样记
如果只想记住最核心的内容,其实就是两条。
查看哪些行命中删除名单
grep -xFf delete.txt original.txt
可以理解为:
精确找出 delete.txt 中存在于 original.txt 的行
删除这些行
grep -vxFf delete.txt original.txt > new.txt
可以理解为:
保留所有不在 delete.txt 中的行
两者唯一关键的区别就是:
-v
没有 -v:
找出黑名单。
有 -v:
排除黑名单。
十、一句话理解
从一个 N 行文件中删除指定的 M 行,本质上不一定需要真的去“编辑并删除”原文件。
更简单的思路是:
重新生成一份文件,只把不在删除名单里的行写进去。
也就是:
原文件 N 行
↓
删除名单 M 行
↓
逐行比较
↓
命中名单 → 不输出
未命中 → 输出
↓
得到过滤后的新文件
而:
grep -vxFf delete.txt original.txt > new.txt
就是这个逻辑非常简洁的 Linux 实现。