从文件中删除多余的标题行,但第一行除外


18

我有一个看起来像这个玩具示例的文件。我的实际文件有400万行,其中大约10行需要删除。

ID  Data1  Data2
1    100    100
2    100    200
3    200    100
ID  Data1  Data2
4    100    100
ID  Data1  Data2
5    200    200

我想删除看起来像标题的行,但第一行除外。

最终文件:

ID  Data1  Data2
1    100    100
2    100    200
3    200    100
4    100    100
5    200    200

我怎样才能做到这一点?

Answers:


26
header=$(head -n 1 input)
(printf "%s\n" "$header";
 grep -vFxe "$header" input
) > output
  1. 将输入文件的标题行抓取到变量中
  2. 打印标题
  3. 处理文件grep以省略与标题匹配的行
  4. 将以上两个步骤的输出捕获到输出文件中

2
也许{ IFS= read -r head; printf '%s\n' "$head"; grep -vF "$head" ; } <file
iruvar

两者都是很好的补充。由于don_crissti间接指出从头POSIX最近去除-1语法,有利于-N 1
杰夫·夏勒

3
@JeffSchaller,最近在12年前。并且head -1在此之前已经过时了数十年。
斯特凡Chazelas

36

您可以使用

sed '2,${/ID/d;}'

这将从第2行开始删除ID为ID的行。


3
很好 或者更具体地讲模式匹配sed '2,${/^ID Data1 Data2$/d;}' file((当然要在列之间使用正确数量的空格))
杰夫·谢勒

嗯,我以为您可以只为1个命令省略分号,但是还可以。
bkmoney 2016年

不是理智sed的,不。
mikeserv '16

aaaand -i用于就地编辑获胜。
user2066657 '16

4
或者sed '1!{/ID/d;}'
斯特凡Chazelas

10

对于那些不喜欢大括号的人

sed -e '1n' -e '/^ID/d'
  • n表示pass行号1
  • d 删除所有以开头的匹配行 ^ID

5
也可以缩短为sed '1n;/^ID/d'文件名。只是一个建议
Valentin Bajrami,2016年

请注意,这还将打印IDfoo与标题不同的类似行(在这种情况下不太可能有所作为,但您永远不会知道)。
terdon

6

这是一个有趣的。您可以sed直接用于剥离第一行的所有副本,并将其他所有内容保留在原处(包括第一行本身)。

sed '1{h;n;};G;/^\(.*\)\n\1$/d;s/\n.*$//' input

1{h;n;}将第一行放入保留空间,进行打印,然后读入下一行-跳过sed第一行的其余命令。(它也跳过1了第二行的第一个测试,但这无关紧要,因为该测试不会应用到第二行。)

G 将换行符后跟保留空间的内容追加到模式空间。

/^\(.*\)\n\1$/d如果在换行符之后的部分(即从保留空间追加的内容)与换行符之前的部分完全匹配,则删除模式空间的内容(因此跳至下一行)。这是重复标题的行将被删除的地方。

s/\n.*$//删除G命令添加的文本部分,以便打印的只是文件中的文本行。

但是,由于正则表达式很昂贵,P因此如果换行符之后的部分(即从保留空间追加的部分)与该部分完全匹配,则使用相同的条件(取反)并提高到换行符的速度会稍微快一些在换行符之前,然后无条件删除模式空间:

sed '1{h;n;};G;/^\(.*\)\n\1$/!P;d' input

输入时的输出为:

ID  Data1  Data2
1    100    100
2    100    200
3    200    100
4    100    100
5    200    200


@don_crissti,有趣的补充;谢谢!我可能会选择更长但相当的东西sed '1{h;n;};G;/^\(.*\)\n\1$/d;P;d' input;我以某种方式更容易阅读。:)
通配符


5

这里有一些其他选项,不需要您提前知道第一行:

perl -ne 'print unless $_ eq $k; $k=$_ if $.==1; 

-n标志告诉perl在其输入文件上循环,将每行保存为$_。该$k=$_ if $.==1;节省的第一行($.是行号,所以$.==1只能是1号线真)作为$k。该print unless $k eq $_打印如果它不是作为一个保存在同一当前行$k

另外,在awk

awk '$0!=x;(NR==1){x=$0}' file 

在这里,我们测试当前行是否与变量中保存的行相同x。如果测试的结果$0!=x为true(如果当前行$0x)不同,则将打印该行,因为awk对true表达式的默认操作是打印。第一行(NR==1)另存为x。由于这是在检查当前行是否匹配之后完成的x,因此可以确保第一行也将被打印。


我不想知道第一行的想法,因为它使它成为工具箱的通用脚本。
马克·斯图尔特

1
awk方法在每行中创建一个空/假数组项;对于4M线来说,如果所有不同的线(从Q不清楚)和相当短的线(看来是这样)都可以,但是,如果有更多或更长的线,则可能会崩溃或死亡。!($0 in a)测试而不创建并避免这种情况,或者awk可以执行与perl相同的逻辑:'$0!=x; NR==1{x=$0}'或者标题行可以为空'NR==1{x=$0;print} $0!=x'
dave_thompson_085

1
@ dave_thompson_085每行在哪里创建一个数组?你的意思是!a[$0]?为什么要在中创建一个条目a
terdon

1
因为那是awk的工作方式;请参阅gnu.org/software/gawk/manual/html_node/…,尤其是“注意”。
dave_thompson_085 '16

1
@ dave_thompson_085好,我该死!谢谢,我没有意识到这一点。立即修复。
terdon

4

AWK也是用于此目的的相当不错的工具。这是代码示例运行:

$ awk 'NR == 1 {print} NR != 1 && $0!~/ID  Data1  Data2/' rmLines.txt | head -n 10                                
ID  Data1  Data2
1    100    100
     100    200
3    200    100
1    100    100
     100    200
3    200    100
1    100    100
     100    200
3    200    100

分解

  • NR == 1 {print} 告诉我们打印文本文件的第一行
  • NR != 1 && $0!~/ID Data1 Data2/ 逻辑运算符&&告诉AWK打印不等于1且不包含的行ID Data1 Data2。注意缺少{print}部分;awk中,如果测试条件评估为true,则假定要打印行。
  • | head -n 10仅是一个很小的添加,将输出限制为仅前10行。与AWK零件本身无关,仅用于演示目的。

如果要在文件中添加> newFile.txt,请通过在命令末尾追加来重定向命令的输出,如下所示:

awk 'NR == 1 {print} NR != 1 && $0!~/ID  Data1  Data2/' rmLines.txt > newFile.txt

它如何支撑?实际上还不错:

$ time awk 'NR == 1 {print} NR != 1 && $0!~/ID  Data1  Data2/' rmLines.txt > /dev/null                            
    0m3.60s real     0m3.53s user     0m0.06s system

边注

生成的示例文件用于从1循环到1百万,然后打印文件的前4行(所以4行乘以百万等于4百万行),顺便说一下,这花了0.09秒。

awk 'BEGIN{ for(i=1;i<=1000000;i++) printf("ID  Data1  Data2\n1    100    100\n     100    200\n3    200    100\n");  }' > rmLines.txt

请注意,这还将打印ID Data1 Data2 foo与标题不同的类似行(在这种情况下不太可能有所作为,但您永远不会知道)。
terdon

@terdon是的,完全正确。但是,OP仅指定了他们想要删除的一种模式,他的示例似乎支持这一点
Sergiy Kolodyazhnyy,2016年

3

Awk,自动适应任何标题:

awk '( FNR == 1) {header=$0;print $0;}
     ( FNR > 1) && ($0 != header) { print $0;}'  file1  file2 ....

即,在第一行上,获取标题并进行打印,然后打印与该标题不同的后续行。

FNR =当前文件中的记录数,因此您可以拥有多个文件,并且每个文件都将执行相同的操作。


2

为了完整起见,Perl解决方案IMO比@terdon给出的更为优雅:

perl -i -p -e 's/^ID.*$//s if $. > 1' file

1
嗯,但是我的重点是避免指定模式,而是从第一行开始读取它。您的方法只会删除以开头的任何行ID。您无法保证这不会删除应保留的行。因为您带来了优雅,g所以使用^和毫无意义$。实际上,m///除了s; 之外,所有您选择的选项都没有用。它们会激活您不使用的功能。因此$s/^ID.*//s将执行相同的操作。
terdon

@terdon,很公平。您的更具通用性!
KWubbufetowicz

2

只是将问题推后一点……看起来您的输入本身就是将多个TSV文件组合在一起的结果。如果您可以备份处理流程中的某个步骤(如果您拥有该步骤或可以与进行此操作的人员进行对话),则可以使用标头感知工具将数据连接在一起,从而消除了必须处理的问题。删除多余的标题行。

例如,使用Miller

$ cat f1.tsv
ID  Data1 Data2
1 100 100
2 100 200
3 200 100
$ cat f2.tsv
ID  Data1 Data2
4 100 100
$ cat f3.tsv
ID  Data1 Data2
5 200 200

$ cat f1.tsv f2.tsv  f3.tsv
ID  Data1 Data2
1 100 100
2 100 200
3 200 100
ID  Data1 Data2
4 100 100
ID  Data1 Data2
5 200 200

$ mlr --tsvlite cat f1.tsv f2.tsv  f3.tsv
ID  Data1 Data2
1 100 100
2 100 200
3 200 100
4 100 100
5 200 200

1
感谢您添加此花絮。将来这将非常有用,因为我的大部分管道都需要加入和合并来自单个样本的文件。
盖乌斯·奥古斯都
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.