从文本文件中删除重复的行

5
我正在处理包含以行为分隔符的数据的大型文本文件(约20MB)。大多数数据条目都是重复的,我想删除这些重复项,只保留一份副本。
此外,为了使问题稍微复杂化,有些条目会重复附加额外的信息。在这种情况下,我需要保留包含额外信息的条目并删除旧版本。
例如, 我需要从这个:
BOB 123 1DB
JIM 456 3DB AX
DAVE 789 1DB
BOB 123 1DB
JIM 456 3DB AX
DAVE 789 1DB
BOB 123 1DB EXTRA BITS
到这个:
JIM 456 3DB AX
DAVE 789 1DB
BOB 123 1DB EXTRA BITS
注意:最终顺序无关紧要。
有什么有效的方法可以做到这一点?
我可以使用awk、python或任何标准的Linux命令行工具。
谢谢。

3
一个快速而简单的版本是 cat file|sort|uniq,但这无法处理带有“额外部分”的行。 - Marc B
2
消除重复行很容易。"sort -u" 就可以自动完成。但是你必须更好地定义部分匹配/子字符串。它总是匹配前两个字段,然后输出中会有更多的字段吗? - dj_segfault
2
同一个键可以有多个额外位的值吗? - Spaceghost
@dj_segfault: 好观点。是的,前两列将始终匹配。 - Pete W
@Spaceghost:我认为每个键只会有一个“额外位”值。 - Pete W
显示剩余2条评论
8个回答

12
以下是Python中的代码示例:

如下代码怎么样:

prev = None
for line in sorted(open('file')):
  line = line.strip()
  if prev is not None and not line.startswith(prev):
    print prev
  prev = line
if prev is not None:
  print prev

如果你发现内存使用是一个问题,你可以将排序作为预处理步骤使用Unix的sort(基于磁盘)并更改脚本,以便它不会将整个文件读入内存。

我稍微修改了你的代码 - 我使用了 str.startswith 而不是用 len 切片。 - Paul Fisher
太好了 - 我现在正在测试这个,从我所看到的来看它正在工作... 我会继续测试所有答案并很快选择最佳解决方案。谢谢! - Pete W
@Pete W:出于好奇,你是否对不同的解决方案进行了任何基准测试?了解在处理20MB数据时使用sorted的成本会很有趣。 - shang
@shang:看起来没有引起任何问题。这个解决方案在几分之一秒内返回了结果,在这种情况下已经足够了。我没有进行正式的基准测试,但它们似乎都在一秒左右返回结果。 - Pete W

3

awk '{x[$1 " " $2 " " $3] = $0} END {for (y in x) print x[y]}'

如果您需要为不同的文件指定列数:

awk -v ncols=3 '
  {
    key = "";
    for (i=1; i<=ncols; i++) {key = key FS $i}
    if (length($0) > length(x[key])) {x[key] = $0}
  }
  END {for (y in x) print y "\t" x[y]}
'

这样做的缺点是,除非“额外位”是文件中匹配前3列的最后一行的一部分,否则它将不会被保留。 - Joe Kington
这太棒了 -非常整洁!我现在正在测试它,看看它是否完全符合我的需求,但第一批结果看起来非常不错。我很快就会回来给出更明确的答复。 - Pete W
不幸的是,正如Joe Kington所提到的那样,当“额外位”不是最近的条目时(我刚刚发现在某些情况下我的txt文件中确实会出现这种情况),它并不能完全解决我的问题。 - Pete W
@Pete,如果你有兴趣的话,我添加了一个测试,以便保留具有最长额外位的行。 - glenn jackman
我相信你那里缺少了一个括号。希望我加上去没问题。(虽然我认为 (?) OP 可能只是想要 for (y in x) print x[y]?不过这不是重点...) 尽管我很喜欢 Python,但很遗憾 awk 在现今经常被忽视了! :) - Joe Kington

2
这个或者稍作修改的方案应该可以胜任:
finalData = {}
for line in input:
    parts = line.split()
    key,extra = tuple(parts[0:3]),parts[3:]
    if key not in finalData or extra:
        finalData[key] = extra

pprint(finalData)

输出:

{('BOB', '123', '1DB'): ['EXTRA', 'BITS'],
 ('DAVE', '789', '1DB'): [],
 ('JIM', '456', '3DB'): ['AX']}

变量可能应该先对输入进行排序(这样就不会在重复项之前出现带有额外位的项而丢失额外位),或者使用collections.defaultdict来收集任何额外位的出现,例如finalData = collections.defaultdict(set),然后finalData[key].add(extra)(您仍然必须决定如何处理一个键的不同“额外位”)。 - Steven
那是一个故意的决定,因为不清楚是否应该在那种情况下删除“额外位”...很容易添加。 - MikeyB

2
这是对Glenn Jackman答案的改进,应该可以处理带有额外部分的任何位置的行:
awk '{idx = $1 " " $2 " " $3; if (length($0) > length(x[idx])) x[idx] = $0} END {for (idx in x) print x[idx]}' inputfile

或者

awk -v ncols=3 '
  {
    key = "";
    for (i=1; i<=ncols; i++) {key = key FS $i}
    if (length($0) > length(x[key])) x[key] = $0
  }
  END {for (y in x) print x[y]}
' inputfile

1

你需要定义一个函数来将你的行分成重要部分和额外部分,然后你可以这样做:

def split_extra(s):
    """Return a pair, the important bits and the extra bits."""
    return blah blah blah

data = {}
for line in open('file'):
    impt, extra = split_extra(line)
    existing = data.setdefault(impt, extra)
    if len(extra) > len(existing):
        data[impt] = extra

out = open('newfile', 'w')
for impt, extra in data.iteritems():
    out.write(impt + extra)

1

find_unique_lines 函数适用于文件对象或字符串列表。

import itertools

def split_line(s):
    parts = s.strip().split(' ')
    return " ".join(parts[:3]), parts[3:], s

def find_unique_lines(f):
    result = {}
    for key, data, line in itertools.imap(split_line, f):
        if data or key not in result:
            result[key] = line
    return result.itervalues()

test = """BOB 123 1DB
JIM 456 3DB AX
DAVE 789 1DB
BOB 123 1DB
JIM 456 3DB AX
DAVE 789 1DB
BOB 123 1DB EXTRA BITS""".split('\n')

for line in find_unique_lines(test):
        print line

BOB 123 1DB EXTRA BITS JIM 456 3DB AX DAVE 789 1DB

谢谢!不幸的是,我认为这对于额外位不是最后一个条目的情况不起作用(尽管我承认这从我的问题中并不明显)。 - Pete W
@Pete WпјҡжҳҜзҡ„гҖӮжқЎд»¶if data or key not in resultдјҡеңЁд»ҘдёӢжғ…еҶөдёӢеӯҳеӮЁдёҖиЎҢж•°жҚ®пјҡ1пјүе®ғеҢ…еҗ«ж•°жҚ®пјӣ2пјүеҰӮжһңеүҚзјҖеңЁеӯ—е…ёдёӯдёҚеӯҳеңЁгҖӮе®ғе·Із»ҸйҖҡиҝҮдәҶеҢ…еҗ«йўқеӨ–дҪҚзҡ„ж•°жҚ®зҡ„жөӢиҜ•гҖӮ - shang
对不起,您所提供的字符串示例是可行的。我一直在使用文件进行测试(即将test替换为file = open('data.txt')),它只返回最近的一行(没有额外的部分)。我正在尝试使用我提供的样本数据文件再次复现此操作,但是使用样本数据时,我得到了Bob的2行记录-一行包含额外的部分,一行不包含... 我很困惑! - Pete W
@Pete W: 可能有额外的空格吗?我添加了一个额外的 split() 调用,应该可以解决这个问题。虽然你已经从另一个发布者那里得到了一个可用的解决方案,但我很感兴趣知道错误出在哪里,以便我可以从中学习。 :) - shang
我认为这一定是空格的问题。您最新的解决方案在文件中的示例数据上对我有效。出于某种原因,当我在真实数据上使用它时,如果“EXTRA BITS”不是最近的数据,它仍然会返回数据。我猜想我提供的示例数据和我正在使用的真实数据之间可能存在一些微妙的差异,但这是我的问题,而不是您的问题!您的解决方案正确地回答了这个问题! - Pete W

1

Since you need the extra bits the fastest way is to create a set of unique entries (sort -u will do) and then you must compare each entry against each other, e.g.

if x.startswith(y) and not y.startswith(x)
and just leave x and discard y.


1

如果您有perl,并且只想保留最后一个条目:

cat file.txt | perl -ne 'BEGIN{%k={}} @_ = split(/ /);$kw = shift(@_); $kws{$kw} = "@_"; END{ foreach(sort keys %kws){ print "$_ $kws{$_}";} }' > file.new.txt

1
UUOC。将file.txt作为perl的第一个参数传递。 - William Pursell
我想应该没什么关系吧。 - OneOfOne

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