【发布时间】:2009-09-20 02:01:45
【问题描述】:
我有一个包含一堆行的文件。 这些行中的每一行都有 8 个分号分隔的列。
我如何(在 Linux 中)返回重复的行但仅基于第 2 列?
我应该使用grep 还是别的什么?
【问题讨论】:
标签: linux
我有一个包含一堆行的文件。 这些行中的每一行都有 8 个分号分隔的列。
我如何(在 Linux 中)返回重复的行但仅基于第 2 列?
我应该使用grep 还是别的什么?
【问题讨论】:
标签: linux
在 awk 脚本中查看我的 cmets
$ cat data.txt
John Thomas;jd;301
Julie Andrews;jand;109
Alex Tremble;atrem;415
John Tomas;jd;302
Alex Trebe;atrem;416
$ cat dup.awk
BEGIN { FS = ";" }
{
# Keep count of the fields in second column
count[$2]++;
# Save the line the first time we encounter a unique field
if (count[$2] == 1)
first[$2] = $0;
# If we encounter the field for the second time, print the
# previously saved line
if (count[$2] == 2)
print first[$2];
# From the second time onward. always print because the field is
# duplicated
if (count[$2] > 1)
print
}
示例输出:
$ sort -t ';' -k 2 data.txt | awk -f dup.awk
John Thomas;jd;301
John Tomas;jd;302
Alex Tremble;atrem;415
Alex Trebe;atrem;416
这是我的解决方案 #2:
awk -F';' '{print $2}' data.txt |sort|uniq -d|grep -F -f - data.txt
此解决方案的美妙之处在于它保留了行顺序,但代价是同时使用了许多工具(awk、sort、uniq 和 fgrep)。
awk 命令打印出第二个字段,然后对其输出进行排序。接下来,uniq -d 命令挑选出重复的字符串。此时,标准输出包含重复的第二个字段列表,每行一个。然后我们将该列表通过管道传输到 fgrep。 '-f -' 标志告诉 fgrep 从标准输入中查找这些字符串。
是的,你可以用命令行全力以赴。我更喜欢第二种解决方案,可以更好地使用许多工具和更清晰的逻辑(至少对我而言)。缺点是工具的数量和可能使用的内存。此外,第二种解决方案效率低下,因为它扫描数据文件两次:第一次使用 awk 命令,第二次使用 fgrep 命令。仅当输入文件很大时,这种考虑才有意义。
【讨论】:
fgrep 命令将拾取每一行在它的任何地方都有一个'e'。您可以通过让awk 执行{print ";$2;"} 来降低出现问题的可能性;现在,如果重复的 $2 中的值也显示为 $1 或 $3..$8 中的值,您将只打印杂散线。对于'e',这可能不太可能;对于'1',也许不是那么不可能。要使用grep,您必须能够使用正则表达式,例如:'^[^;]*;X;' (以隔离出现在第二个字段中的 X 到 X)。
grep -w 'e'(也不要使用单引号),但这不如将其锚定到字段分隔符可靠。另一个字段可能包含“e pluribus unum”(“out of many, one”),它将在;e; 的位置被拾取,因为搜索词不会。你必须决定这是否重要。
怎么样:
sort -t ';' -k 2 test.txt | awk -F';' 'BEGIN{curr="";prev="";flag=0} \
NF==8{ prev=curr;
curr=$2;
if(prev!=curr){flag=1}
if(flag!=0 && prev==curr)flag++ ;
if(flag==2)print $0}'
我还尝试了uniq 命令,该命令具有显示重复行“-d”的选项,但无法确定是否可以与字段一起使用。
【讨论】:
一个不分拣的班轮:
awk '{d[$2][a[$2]++]=$0} END{for (i in a) {if (a[i] > 1) for (j in d[i]) {print d[i][j]}}}'
【讨论】:
有一个复杂的awk 脚本。
awk 'BEGIN { FS=";" } { c[$2]++; l[$2,c[$2]]=$0 } END { for (i in c) { if (c[i] > 1) for (j = 1; j <= c[i]; j++) print l[i,j] } }' file.txt
它的工作原理是为第二个字段中每个值的所有出现次数以及具有该值的行保留一个计数器,然后打印出计数器大于 1 的行。
将$2 的所有实例替换为您需要的任何字段编号,并将末尾的file.txt 替换为您的文件名。
【讨论】:
借用 Hai Vu:
% cat data.txt
John Thomas;jd;301
Julie Andrews;jand;109
Alex Tremble;atrem;415
John Tomas;jd;302
Alex Trebe;atrem;416
有一个非常简单的方法(使用 gnu-sort 和 gawk):
(虽然这会重新排序输出!)
(警告:如果没有 --stable,排序会重新排序行,因此第二次出现在第一次之前。请注意!)
cat data.txt | sort -k2,2 -t';' --stable | gawk -F';' '{if ( $2==old ) { print $0 }; old=$2; }'
还有perl方式...
cat data.txt | perl -e 'while(<>) { @data = split(/;/); if ( defined( $test{$data[1]} ) ) { print $_; } $test{$data[1]} = $_; }'
.
【讨论】:
简单的awk 唯一的方法是根据第 2 列删除唯一行(或根据第 2 列返回重复的行);您可能需要更改为您预期的目标列或多个列的组合$X$Y。
awk -F\; 'NR==FNR{s[$2]++;next} (s[$2]>1)' infile infile
【讨论】:
正如@mjv 推测的那样 - awk(或 Perl,或 Python)是更好的选择:
awk -F';' ' {
if (assoc[$2]) { # This field 2 has been seen before
if (assoc[$2] != 1) { # The first occurrence has not been printed
print assoc[$2]; # Print first line with given $2
assoc[$2] = 1; # Reset array entry so we know we've printed it;
# a full line has 8 fields with semi-colons and
# cannot be confused with 1.
}
print $0; # Print this duplicate entry
}
else {
assoc[$2] = $0; # Record line in associative array, indexed by
# second field.
}
}' <<!
a;b;c;d;e;f;g;h
a;c;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;2;c;d;e;f;g;h
a;z;c;d;e;f;g;h
a;q;c;d;e;f;g;h
a;4;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;x;c;d;e;f;g;h
a;c;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;q;c;d;e;f;g;h
a;4;c;d;e;f;g;h
!
这可行,但它可以稍微重新排序数据 - 因为它会在第二次出现时打印重复行的第一次出现。示例输出为:
a;1;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;c;c;d;e;f;g;h
a;c;c;d;e;f;g;h
a;1;c;d;e;f;g;h
a;q;c;d;e;f;g;h
a;q;c;d;e;f;g;h
a;4;c;d;e;f;g;h
a;4;c;d;e;f;g;h
awk 脚本的这个变体对测试进行了重新排序,从而使符号更加紧凑。它还明确忽略不包含用分号分隔的 8 个字段的格式错误的数据行。它被打包为一个 shell 脚本,但没有任何选项处理,因此您只能提供要扫描的文件列表(如果没有列出文件,它会读取标准输入)。我删除了脚本中的 Perl-ish 分号; awk 不需要它们。
#!/bin/sh
awk -F';' '
NF == 8 {
if (!assoc[$2]) assoc[$2] = $0
else if (assoc[$2] != 1)
{
print assoc[$2]
assoc[$2] = 1
print $0
}
else print $0
}' "$@"
此外,@mjv 评论说,如果输入很大,则可能存在内存问题,因为它会记录关联数组“assoc”中每个不同的字段 2 值。如果输入awk 的数据已排序,我们可以消除这种情况,当然,我们可以使用sort 来确保这一点。这是一个变体脚本,它确实处理巨大的输入(因为sort 会在必要时将数据溢出到磁盘以保存中间结果):
sort -t';' -k 2,2 "$@" |
awk -F';' '
BEGIN { last = ";"; line = "" }
NF == 8 {
if ($2 != last)
{
last = $2
line = $0
}
else if (line != "")
{
print line
line = ""
print $0
}
else print $0;
}'
这只会保留一行输入的副本。当然,样本数据的输出是按排序顺序给出的。
【讨论】:
{END if (line != "") print line} 但我的 AWK 很烂。
grep 可能会这样做,但我猜您使用 awk(在某些系统上也称为 gawk)会更轻松。
用于您需要的有效链/脚本取决于一些额外的信息。例如,输入文件是否易于排序,输入有多大(或者说它是巨大的还是流)......
假设输入是排序的(无论是最初的还是通过排序的管道),awk 脚本看起来像这样:(注意未经测试)
查看 Jonathan Leffler 或 Hai Vu 提供的解决方案,以了解实现相同预排序要求的方法。
#!/usr/bin/awk
# *** Simple AWK script to output duplicate lines found in input ***
# Assume input is sorted on fields
BEGIN {
FS = ";"; #delimiter
dupCtr = 0; # number of duplicate _instances_
dupLinesCtr = 0; # total number of duplicate lines
firstInSeries = 1; #used to detect if this is first in series
prevLine = "";
prevCol2 = ""; # use another string in case empty field is valid
}
{
if ($2 == prevCol2) {
if (firstInSeries == 1) {
firstInSeries = 0;
dupCtr++;
dupLinesCtr++;
print prevLine
}
dupLinesCtr++;
print $0
}
else
firstInSeries = 1
prevCol2 = $2
prevLine = $0
}
END { #optional display of counts etc.
print "*********"
print "Total duplicate instances = " iHits " Total lines = " NR;
}
【讨论】:
--help。您确实有某个版本的 awk - 如果完全丢失,您将收到一条未找到命令的消息,而不是一条无法识别的选项消息。如果需要,您可以随时安装 awk。
我假设您不依赖输入的任何特定顺序(它可能没有在键(第二)字段上预先排序)并且您希望保留输入的顺序输出中的输入行...打印第一行和所有后续行的副本,这些行在第二个字段中包含重复值。
这是我能用 Python 写出的最快的 sn-p 代码:
import fileinput
seen = dict()
for line in fileinput.input():
fields = line.split(';')
key = fields[1]
if key in seen:
if not seen[key][0]:
print seen[key][1],
seen[key] = (True, seen[key][1])
print line,
else:
seen[key] = (False, line)
fileinput 模块让我们以类似于默认的awk 文件/输入处理的方式处理我们的输入行...或类似于 Perl 的-n 命令行开关的语义。
从那里我们只需跟踪我们看到的第一行,在第二个字段中使用一个唯一值,以及一个指示我们之前是否打印过这一行的标志。当我们第一次找到一个副本时,我们打印具有该键的第一行,并将其标记为已打印,然后我们打印当前行。对于所有后续重复,我们只打印当前行。显然,对于任何非受骗者,我们只需将其作为条目发布到我们的字典中即可。
可能有一种更优雅的方式来处理“第一次欺骗”布尔值......但这对我来说是最明显的,不应该带来任何额外的撤销开销。创建一个非常简单的具有自己状态的对象/类(我已被打印)将是一种选择。但我认为这会使代码的整体要点更难理解。
很明显,这可以在任何支持关联数组(散列、字典、表,无论您喜欢的语言如何称呼它们)的任何脚本或编程语言中完成。此代码与我在此线程中看到的大多数其他示例之间的唯一区别在于我对您的要求所做的假设(您希望保留输入和输出行的相对顺序)。
【讨论】: