有没有一种按列进行“去重”的方法?

242

我有一个像这样的 .csv 文件:

stack2@domain.example,2009-11-27 01:05:47.893000000,domain.example,127.0.0.1
overflow@domain2.example,2009-11-27 00:58:29.793000000,domain2.example,255.255.255.0
overflow@domain2.example,2009-11-27 00:58:29.646465785,domain2.example,256.255.255.0
...

我需要从文件中删除重复的电子邮件(整行),即从上面示例中包含 overflow@domain2.example 的一行。如何仅在第1列(由逗号分隔)上使用 uniq?根据 manuniq 没有列选项。

我尝试了一些带有 sort | uniq 的东西,但它不起作用。

9个回答

409
sort -u -t, -k1,1 file
  • -u 用于去重
  • -t, 以逗号为分隔符
  • -k1,1 用于指定第一个关键字段

测试结果:

overflow@domain2.example,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

23
为什么需要在“-k1,1”中使用“,1”?为什么不只用“-k1”? - hello_there_andy
27
这在手册(man sort)中有解释。它代表排序的起始和结束位置。 - Serrano
它是如何决定输出具有重复字段的哪一行的?是在排序之前的第一次出现的重复吗? - Geremia
6
我进行了测试,并确认了sort的手册所说的:“-u--unique-c一起使用时,检查严格排序;没有-c仅输出相等运行的第一个。”因此,确实是“在排序之前重复的第一个出现”。 - Geremia
2
这会改变行的顺序,不是吗? - rkachach
3
它确实回答了具体的问题,但标题并没有反映出来 - 即有其他选项可用于“uniq”,而“sort -u”不适用于这些选项 - 例如仅报告哪些行是重复的(并且不为唯一的行产生输出)。 我想知道为什么“uniq”有一个“--skip-fields = N”的选项,但没有选择要比较哪个字段的选项...这似乎是一个显而易见的事情。 - Max Waterman

135
awk -F"," '!_[$1]++' file
  • -F设置字段分隔符。
  • $1是第一个字段。
  • _[val]在哈希_(一个普通变量)中查找val
  • ++递增并返回旧值。
  • !返回逻辑非。
  • 最后会有一个隐式的打印操作。

6
这种方法比排序快两倍。 - Alex Bitek
12
这还有额外的好处,可以保持原始顺序不变! - AffluentOwl
9
如果你需要的是最后一个唯一值而不是第一个,那么这个awk脚本可以帮助你:awk -F',' '{ x[$1]=$0 } END { for (i in x) print x[i] }' file - Sukima
4
@eshwar 只需将更多字段添加到字典索引中即可!例如,!_[$1][$2]++ 可用于按前两个字段排序。但是,我的 awk 技能不足以在一系列字段上进行唯一性处理。 :( - Soham Chowdhury
1
太棒了!这个选项比答案更好,因为它保持了行的顺序。 - rkachach
显示剩余4条评论

23
考虑多列。
基于第一列和第三列进行排序并给出唯一列表:
sort -u -t : -k 1,1 -k 3,3 test.txt
  • -t :冒号是分隔符
  • -k 1,1 -k 3,3基于第一列和第三列

8
如果您想使用uniq命令: <mycvs.cvs tr -s ',' ' ' | awk '{print $3" "$2" "$1}' | uniq -c -f2 输出如下:
1 01:05:47.893000000 2009-11-27 tack2@domain.example
2 00:58:29.793000000 2009-11-27 overflow@domain2.example
1

5
我想指出一种可能的简化方法:你可以抛弃cat!不要使用管道传输给tr,直接让tr使用<读取文件即可。对于新手来说,通过cat进行管道传输是常见但不必要的复杂操作。对于大量数据,这样做会有性能影响。 - Carl Smotricz
使用 rev 可以简化字段的反转。 - Hielke Walinga
@HielkeWalinga 我认为 rev 是将每行中的 字符 反转,而不是字段! - Fixee
1
@Fixee 是的,但是这样也会改变字段的顺序,并且对于唯一性来说,字符被反转的字段并不重要。所以像这样:<mycsv.cvs tr -s , ' ' | rev | uniq -f 4 | rev - Hielke Walinga

7
如果你想保留重复项中的最后一个,你可以使用下面的方法:
 tac a.csv | sort -u -t, -r -k1,1 |tac

这是我的要求

这里

tac 命令会按行反转文件内容


2
这里有一个非常巧妙的方法。
首先,格式化内容,使要比较唯一性的列成为固定宽度。一种方法是使用awk printf和字段/列宽度指示符(“%15s”)。
现在可以使用uniq的-f和-w选项跳过前面的字段/列并指定比较宽度(列宽)。
以下是三个示例。
在第一个示例中...
1)暂时将感兴趣的列设置为大于或等于字段最大宽度的固定宽度。
2)使用-f uniq选项跳过之前的列,并使用-w uniq选项将宽度限制为tmp_fixed_width。
3)从该列中删除尾随空格,以“恢复”其宽度(假设之前没有尾随空格)。
printf "%s" "$str" \
| awk '{ tmp_fixed_width=15; uniq_col=8; w=tmp_fixed_width-length($uniq_col); for (i=0;i<w;i++) { $uniq_col=$uniq_col" "}; printf "%s\n", $0 }' \
| uniq -f 7 -w 15 \
| awk '{ uniq_col=8; gsub(/ */, "", $uniq_col); printf "%s\n", $0 }'

在第二个例子中...
创建一个新的唯一列1。然后在应用uniq过滤器后删除它。
printf "%s" "$str" \
| awk '{ uniq_col_1=4; printf "%15s %s\n", uniq_col_1, $0 }' \
| uniq -f 0 -w 15 \
| awk '{ $1=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

第三个示例与第二个示例相同,但适用于多列。
printf "%s" "$str" \
| awk '{ uniq_col_1=4; uniq_col_2=8; printf "%5s %15s %s\n", uniq_col_1, uniq_col_2, $0 }' \
| uniq -f 0 -w 5 \
| uniq -f 1 -w 15 \
| awk '{ $1=$2=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

1

awk命令行界面,行为类似于uniq但不需要sort,只捕获连续的重复项

迄今为止,大多数其他答案都提供了即使不是连续的重复项也会删除的方法。

这种方法的问题在于它要求首先进行排序或在内存中存储可能巨大的映射,这对于大型输入文件来说可能会很慢/不可行。

因此,对于这些情况,这里有一个awk解决方案,就像uniq一样,只有在连续的行上出现重复项时才会捕获。例如,要删除第一列上的所有连续重复项,我们可以使用$1

awk '$1 != last { print $0; last = $1; }' infile.txt

例如,考虑输入文件:
a 0
a 1
b 0
a 0
a 1

输出将是:
a 0
b 0
a 0

这里:
- 第一个 `a 1` 列被移除,因为前一个 `a 0` 行有一个重复的第一列 `a` - 但是我们得到了第二个 `a 0` 列,因为 `b 0` 行打破了连续性
`awk` 脚本的工作原理很简单,只需将上一行的列值存储在 `last` 变量中,并将当前值与其进行比较,如果不同则跳过。
如果你知道输入数据中有很多无用的连续重复项,并且想在进行任何更昂贵的类似排序的处理之前清理一下,这种仅限连续的方法可能会很有用。
如果你真的需要删除非连续重复项,更健壮的解决方案通常是使用关系型数据库,如 SQLite,例如:how can I delete duplicates in SQLite? 快速的 Python 脚本可以删除最后 N 行中的重复项。
如果你需要更灵活一点但仍不想支付完整排序的费用: uniqn
#!/usr/bin/env python

import argparse
from argparse import RawTextHelpFormatter
import fileinput
import sys

parser = argparse.ArgumentParser(
    description='uniq but with a memory of the n previous distinct lines rather than just one',
    epilog="""Useful if you know that duplicate lines in an input file are nearby to one another, but not necessarily immediately one afte the other.

This command was about 3x slower than uniq, and becomes highly CPU (?) bound even on rotating disks. We need to make a C++ version one day, or try PyPy/Cython""",
    formatter_class=RawTextHelpFormatter,
)
parser.add_argument("-k", default=None, type=int)
parser.add_argument("-n", default=10, type=int)
parser.add_argument("file", nargs='?', default=[])
args = parser.parse_args()
k = args.k

lastlines = {}
for line in fileinput.input(args.file):
    line = line.rstrip('\r\n')
    if k is not None:
        orig = line
        line = line.split()[k]
    else:
        orig = line
    if not line in lastlines:
        print(orig)
    lastlines.pop(line, None)
    lastlines[line] = True
    if len(lastlines) == args.n + 1:
        del lastlines[next(iter(lastlines))]

这个脚本在前面的-n行中查找重复项,并且可以用来清理具有某种周期模式的数据,以防止uniq对其产生太大影响。-k选择列。例如,考虑输入文件:uniqn-test
1 a
2 a
3 a
1 a
2 a
2 b
3 a

然后:

./uniqn -k0 -n3 uniqn-test

給:

1 a
2 a
3 a

例如,第二个1 a看到前面三行的第一个1 a并跳过它,这是由于-n3的结果。
一些内置的uniq选项需要考虑
尽管uniq没有一个很好的“仅考虑第N列”的选项,但它确实有一些标志可以解决某些更受限制的情况,来自man uniq
- -f,--skip-fields=N:避免比较前N个字段 - -s,--skip-chars=N:避免比较前N个字符 - -w,--check-chars=N:在行中最多比较N个字符 - 字段是一系列空白(通常是空格和/或制表符),然后是非空白字符。在比较字符之前会跳过字段。
如果有人能够将类似于--check-chars--check-fields补丁到其中,那么我们就可以使用--skip-fields N-1 --check-fields 1了。对于第一个字段的特定情况,它已经起作用了。
在Ubuntu 23.04上进行了测试。

你需要什么性能?out9: 1.85GiB 0:00:15 [125MiB/s] [125MiB/s] [<=] (pvE 0.1 in0 <“$m3t” | mawk2'!__[$_]++' FS='\n';)11.70秒用户 1.53秒系统 87% CPU 15.158总 行数= 12494275。| UTF8字符= 1285316715。| 字节= 1983544693。awk完整行去重,使用了12.5百万行,跨越1.85 GiB,在15.2秒内完成。附注:结果证明我的自定义wc减慢了速度。没有它,去重近1.9 GiB的多字节Unicode文本只需12.35秒。 - RARE Kpop Manifesto
尝试对一个包含148,156,631行7.59 GiB大小的ASCII文本文件进行去重操作,最终发现使用sort | uniq比使用awk快了4.5%,分别耗时148秒155秒 - RARE Kpop Manifesto
@RAREKpopManifesto,超过7 GiB的容量很容易实现。对于小文件来说,使用awk映射是可以的。但是当我们处理数百GB的文件时,它会消耗大量内存,并且排序(使用临时文件以避免内存溢出)可能比不使用映射的awk更慢。 - Ciro Santilli OurBigBook.com
这是7 GB的完整行去重。通过适当大小的键进行去重,awk可以轻松处理100 GiB而不浪费时间进行排序。您始终可以通过python预先管道化它,并通过行的SHA256哈希使awk仅执行完整行去重。最大的排序仍然是n log n,所以我总是在过滤之后而不是之前进行排序,只要适用。 - RARE Kpop Manifesto
最令人惊讶的是,在我的7 GiB文件中,预先将其传送到python3进行逐行MD5处理后,速度竟然变慢了9秒 - RARE Kpop Manifesto
显示剩余2条评论

-2
通过使用sort对文件进行排序,然后再应用uniq,可以去重。
看起来文件已经被很好地排序了:
$ cat test.csv
overflow@domain2.example,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.example,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv
overflow@domain2.example,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0
overflow@domain2.example,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv | uniq
overflow@domain2.example,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0
overflow@domain2.example,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

你也可以使用一些AWK的魔法:

$ awk -F, '{ lines[$1] = $0 } END { for (l in lines) print lines[l] }' test.csv
stack2@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.example,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.example,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0

这并不是问题中要求的“按列”唯一。这只是整行唯一。此外,您不必进行排序即可执行uniq。这两者是互斥的。 - Javid Jamae
2
是的,你说得对。最后一个示例确实做到了问题所要求的,尽管被接受的答案更加简洁。关于 sort,然后是 uniq,需要在执行 uniq 之前进行 sort,否则它不起作用(但你可以跳过第二个命令,直接使用 sort -u)。来自 uniq(1):"从输入(或标准输入)中过滤相邻匹配的行,并写入输出(或标准输出)"。 - Mikael S

-3

如果你需要删除给定文件中某个特定值的所有内容,为什么不直接使用grep -v呢?这比使用awk隔离列要简单得多:

例如,要删除第二行中值为"col2"的所有内容:col1,col2,col3,col4

grep -v ',col2,' file > file_minus_offending_lines

如果这不够好,因为某些行可能会被错误地剥离,可能会在不同的列中显示匹配值,您可以像这样做:
使用awk来隔离有问题的列: 例如:
awk -F, '{print $2 "|" $line}'

-F参数将字段分隔符设置为“,”,$2表示第二列,然后是一些自定义的分隔符,最后是整行内容。您可以通过删除以有问题的值开头的行来进行过滤:

 awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE

然后剥离分隔符之前的内容:

awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE | sed 's/.*|//g'

(注意 - sed 命令很粗糙,因为它不包括转义值。此外,sed 模式应该真正是类似于 "[^|]+"(即除了分隔符之外的任何内容)。但希望这足够清楚了。)


3
他不想清除行,而是想保留带有特定字符串的单个行的副本。uniq 是正确的用法。 - ingyhere

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接