删除文件的最后N行
1. 概述
当我们在 Linux 下使用命令行工作时,我们经常需要处理文本文件。在本教程中,我们将介绍从输入文件中删除最后n行的不同方法。
此外,我们还将讨论这些方法的性能。
2. 实例介绍
首先,让我们创建一个输入文件来理解问题:
$ cat input.txt
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
05 is my line number. Keep me please!
06 is my line number. Keep me please!
07 is my line number. Keep me please!
08 is my line number. Delete me please!
09 is my line number. Delete me please!
10 is my line number. Delete me please!
如上面的输出所示,我们的input.txt包含十行。
现在,假设我们要从input.txt文件中删除最后三行 ( n=3 ) 。
在本教程中,我们将使用四种技术解决问题:
之后,我们将讨论解决方案的性能并找出解决问题的最有效方法。
3. 使用head命令
使用head命令,我们可以通过在连字符 (-) 后面传递一个数字以及-n选项来打印除文件最后x行之外的所有行,例如-n -x。**
因此,我们可以使用此选项以直接的方式解决我们的问题:
$ head -n -3 input.txt
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
05 is my line number. Keep me please!
06 is my line number. Keep me please!
07 is my line number. Keep me please!
但是 head命令在 stdin 中打印结果。我们可以通过临时文件将结果保存回input.txt :
$ head -n -3 input.txt > tmp.txt && mv tmp.txt input.txt
4. 使用wc和sed命令
使用 sed命令及其地址范围,我们可以快速从文件中删除从给定行号开始到最后一行的行:
sed 'GIVEN_LINE_NO, $ d' input_file
例如,让我们从第 5 行删除直到input.txt的末尾:
$ sed '5,$ d' input.txt
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
然而,我们的问题是从输入文件中删除最后三行。由于我们的输入文件有十行,sed命令:sed ‘8,$ d’ input.txt将是问题的解决方案。
于是,问题就变成了如何计算第一个被删除的行号“ 8 ”。
现在,是时候介绍wc命令了。使用 带有*-l选项的wc*命令,我们可以轻松获取文件中的总行数 ( TOTAL ):
$ wc -l input.txt
10 input.txt
此外,我们可以通过计算TOTAL – n + 1得到要删除的第一行号。在我们的示例中,我们有n=3:
$ echo $(( $(wc -l <input.txt)-3+1 ))
8
让我们仔细看看上面的命令:
- wc -l < input.txt:这里我们将 input.txt 文件重定向到标准输入以跳过输出中的文件名
- $(wc -l < input.txt):我们使用命令替换 来捕获TOTAL结果
- $(( TOTAL – 3+1 )):算术展开将计算数学表达式
现在,让我们将这两部分组装在一起并尝试解决我们的问题:
$ sed '$(( $(wc -l <input.txt)-3+1 )),$ d' input.txt
sed: -e expression #1, char 2: unknown command: `('
哎呀!为什么sed命令会抱怨“ ( ”?
这是因为 bash 扩展和命令替换不会在单引号之间扩展。
让我们将sed命令中的单引号更改为双引号并再次测试:
$ sed "$(( $(wc -l <input.txt)-3+1 )),$ d" input.txt
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
05 is my line number. Keep me please!
06 is my line number. Keep me please!
07 is my line number. Keep me please!
伟大的!现在问题解决了。
如果我们使用流行的GNU sed,我们可以使用*-i*选项将更改写回输入文件:
$ sed -i "$(( $(wc -l <input.txt)-3+1 )),$ d" input.txt
5. 使用tac和sed命令
在本节中,我们仍然会使用sed命令来解决问题,但是从不同的角度来看。
我们了解到,使用sed解决问题的难点 在于计算要删除的第一行号。
但是,如果我们可以颠倒输入文件中行的顺序,问题就会变成“从文件中删除前 n 行”。一个简单的 sed 单行sed ‘1,n d’可以删除前n行。之后,如果我们再次反转线路,我们的问题就解决了。
tac命令 可以颠倒文件中行的顺序 。也就是说,我们可以尝试通过命令“ tac input.txt | sed ‘1,3 d’ | tac ”解决我们的问题。
最后,让我们测试它是否适用于我们的示例:
$ tac input.txt | sed '1,3 d' | tac
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
05 is my line number. Keep me please!
06 is my line number. Keep me please!
07 is my line number. Keep me please!
是的!有用。我们得到了预期的结果。
6. 使用awk命令
awk命令是一个强大的文本处理实用程序。我们可以让 awk 遍历输入文件两次 来解决问题。
在第一遍中,它会找出文件中的总行数,在第二遍中,我们打印那些我们想要保留的行:
$ awk -v n=3 'NR==FNR{total=NR;next} FNR==total-n+1{exit} 1' input.txt input.txt
01 is my line number. Keep me please!
02 is my line number. Keep me please!
03 is my line number. Keep me please!
04 is my line number. Keep me please!
05 is my line number. Keep me please!
06 is my line number. Keep me please!
07 is my line number. Keep me please!
如上面的输出所示, awk命令解决了我们的问题。
最后,让我们了解一下 one-liner 的工作原理:
- -vn=3:我们声明了一个awk变量 n=3
- NR==FNR{total=NR;next}:这是第一遍。在此过程中, awk命令将当前行号保存到一个名为total的变量中。第一次通过后,total变量保存输入文件中的总行数
- FNR==total-n+1{exit} 1:这是第二遍。如果FNR==total-n+1,这意味着我们已经到达了需要删除的第一行,所以我们exit。否则,我们只打印该行。在这里,非零数字1将被评估为true 并触发awk的默认操作 : print
7.性能
到目前为止,我们已经了解了解决问题的不同方法。现在,让我们讨论一下他们的表现。
我们将创建一个包含 1 亿行的大输入文件,并在其上测试每个解决方案以删除最后 100 万行:
$ wc -l big.txt
100000000 big.txt
为了对它们的性能进行基准测试,我们将使用*time *命令:
- head 解决方案: time head -n -1000000 big.txt > /dev/null
- wc 和 sed解决方案: time sed “$(( $(wc -l < big.txt )-1000000+1 )),$ d” big.txt > /dev/null
- tac 和 sed解决方案:time tac big.txt | sed ‘1,1000000 d’ | tac > /dev/null
- awk 解决方案:time awk -vn=1000000 ‘NR==FNR{total=NR;next} FNR==total-n+1{exit} 1’ big.txt big.txt > /dev/ null
现在,让我们来看看测试结果: top解决方案
real 0m0.238s
user 0m0.087s
sys 0m0.150s
wc和sed解决方案
real 0m6.328s
user 0m6.062s
sys 0m0.254s
tac和sed解决方案
real 0m7.780s
user 0m8.284s
sys 0m2.239s
awk解决方案
real 0m36.553s
user 0m36.234s
sys 0m0.297s
如表所示,head解决 方案是最快的。它比sed解决方案快 30 倍,比awk命令快 150 倍。
这是因为 head 命令只读取换行符,没有做任何预处理或保留一行的文本。它寻找直到找到目标行号并将内容转储到输出中。
另一方面, sed和 awk命令将读取输入文件的每一行并进行一些预处理。例如, ** awk命令 根据给定的 FS 和 RS初始化一些内部属性,例如字段、NF、记录等。**因此,它增加了我们的问题所不需要的大量开销。
尽管sed和 awk解决方案比解决这个问题的head解决方案慢得多 ,但仍然值得学习它们并了解它们的工作原理。那是因为它们比head命令更具可扩展性。
例如,假设我们面临一个新问题,将输入文件的最后n行中的所有“ foo ”更改为“ bar ”。现在, head命令无法解决问题。但是,我们可以扩展 sed或 awk命令来解决它。