从Python中的大文件中删除重复的行

时间:2010-08-10 19:50:08

标签: python duplicates

我有一个csv文件,我想删除重复的行,但它太大而无法放入内存中。我找到了一种方法来完成它,但我的猜测是,这不是最好的方法。

每行包含15个字段和数百个字符,并且需要所有字段来确定唯一性。我没有比较整行来找到重复,而是在比较hash(row-as-a-string)以节省内存。我设置了一个过滤器,将数据划分为大致相等数量的行(例如一周中的几天),并且每个分区都足够小,以使该分区的散列值查找表适合内存。我为每个分区传递一次文件,检查唯一的行并将它们写入第二个文件(伪代码):

import csv

headers={'DayOfWeek':None, 'a':None, 'b':None}
outs=csv.DictWriter(open('c:\dedupedFile.csv','wb')
days=['Mon','Tue','Wed','Thu','Fri','Sat','Sun']

outs.writerows(headers)

for day in days:
    htable={}
    ins=csv.DictReader(open('c:\bigfile.csv','rb'),headers)
    for line in ins:
        hvalue=hash(reduce(lambda x,y:x+y,line.itervalues()))
        if line['DayOfWeek']==day:
            if hvalue in htable:
                pass
            else:
                htable[hvalue]=None
                outs.writerow(line)

我想加快速度的一种方法是找到一个更好的过滤器来减少必要的传球次数。假设行的长度均匀分布,可能代替

for day in days: 

if line['DayOfWeek']==day:

我们有

for i in range(n):

if len(reduce(lambda x,y:x+y,line.itervalues())%n)==i:

其中'n'和内存一样小。但这仍然使用相同的方法。

Wayne Werner提供了一个很好的实用解决方案;我很好奇是否有更好/更快/更简单的方法从算法的角度来做这件事。

P.S。我只限于Python 2.5。

6 个答案:

答案 0 :(得分:12)

如果您想要一种非常简单的方法,只需创建一个sqlite数据库:

import sqlite3
conn = sqlite3.connect('single.db')
cur = conn.cursor()
cur.execute("""create table test(
f1 text,
f2 text,
f3 text,
f4 text,
f5 text,
f6 text,
f7 text,
f8 text,
f9 text,
f10 text,
f11 text,
f12 text,
f13 text,
f14 text,
f15 text,
primary key(f1,  f2,  f3,  f4,  f5,  f6,  f7,  
            f8,  f9,  f10,  f11,  f12,  f13,  f14,  f15))
"""
conn.commit()

#simplified/pseudo code
for row in reader:
    #assuming row returns a list-type object
    try:
        cur.execute('''insert into test values(?, ?, ?, ?, ?, ?, ?, 
                       ?, ?, ?, ?, ?, ?, ?, ?)''', row)
        conn.commit()
    except IntegrityError:
        pass

conn.commit()
cur.execute('select * from test')

for row in cur:
    #write row to csv file

然后你不必担心任何比较逻辑 - 只需让sqlite为你处理它。它可能不会比散列字符串快得多,但它可能要容易得多。当然,如果需要,您可以修改存储在数据库中的类型,视情况而定。当然,因为您已经将数据转换为字符串,所以您可以只使用一个字段。这里有很多选择。

答案 1 :(得分:6)

您基本上正在进行合并排序,并删除重复的条目。

将输入分解为内存大小的片段,对每个片段进行排序,然后在删除重复片段的同时合并片段通常是一个合理的想法。

实际上,我会让虚拟内存系统处理它并写下几个演出:

input = open(infilename, 'rb')
output = open(outfile, 'wb')

for key,  group in itertools.groupby(sorted(input)):
    output.write(key)

答案 2 :(得分:2)

无法保证您当前的方法无法正常使用。

首先,实际上不同的两条线可能产生相同的散列值的概率很小。 hash(a) == hash(b)并不总是意味着a == b

其次,你使用“reduce / lambda”caper来提高概率:

>>> reduce(lambda x,y: x+y, ['foo', '1', '23'])
'foo123'
>>> reduce(lambda x,y: x+y, ['foo', '12', '3'])
'foo123'
>>>
顺便说一句,不会“”.join(['foo','1','23'])有点清楚?

BTW2,为什么不为set使用dict代替htable

这是一个实用的解决方案:GnuWin32站点获取“core utils”包并安装它。然后:

  1. 将没有标题的文件副本写入(比如)infile.csv
  2. c:\gnuwin32\bin\sort --unique -ooutfile.csv infile.csv
  3. 读取outfile.csv并编写一份前缀为
  4. 的副本

    对于步骤1和步骤1中的每一步。 3,您可以使用Python脚本或其他一些GnuWin32实用程序(head,tail,tee,cat,...)。

答案 3 :(得分:1)

您的原始解决方案稍有不正确:您可能有不同的行散列到相同的值(哈希冲突),并且您的代码会将其中一个删除。

就算法复杂性而言,如果您期望重复数量相对较少,我认为最快的解决方案是逐行扫描文件,添加每行的哈希值(如您所做),还要存储位置那条线。然后,当您遇到重复哈希时,请寻找原始位置以确保它是重复的而不仅仅是哈希冲突,如果是,请回头并跳过该行。

顺便说一句,如果CSV值被规范化(即,如果相应的CSV行是逐字节的,那么记录被认为是相等的),你根本不需要在这里涉及CSV解析,只需处理纯文本行

答案 4 :(得分:0)

因为我认为你必须经常这样做(或者你已经破解了一次性的脚本),并且你提到你对理论解决方案感兴趣,这是一种可能性。

将输入行读入B-Trees,按每个输入行的散列值排序,在内存填充时将它们写入磁盘。我们注意在B-Trees上存储附加到哈希的原始线(作为一组,因为我们只关心独特的线)。当我们读取一个重复的元素时,我们会检查存储元素上设置的行,如果它是一个碰巧散列到相同值的新行,则添加它。

为什么是B树?当您只能(或想要)将部分内容读取到内存时,它们需要更少的磁盘读取。每个节点上的度数(子节点数)取决于可用内存和行数,但您不希望节点太多。

一旦我们在磁盘上有这些B树,我们就会比较每个B树的最低元素。我们从拥有它的所有B树中删除所有的最低层。我们合并它们的行集,这意味着我们没有为这些行留下重复(并且我们没有更多行散列到该值)。然后,我们将此合并中的行写入输出csv结构。

我们可以分开一半的内存用于读取B-Trees,另一半用于将输出csv保留在内存中一段时间​​。当它的一半已满时,我们将csv刷新到磁盘,附加到已写入的任何内容。我们在每个步骤中读取的每个B树的大小可以通过(available_memory / 2)/ number_of_btrees粗略计算,舍入所以我们读取完整节点。

在伪Python中:

ins = DictReader(...)
i = 0
while ins.still_has_lines_to_be_read():
    tree = BTree(i)
    while fits_into_memory:
        line = ins.readline()
        tree.add(line, key=hash)
    tree.write_to_disc()
    i += 1
n_btrees = i

# At this point, we have several (n_btres) B-Trees on disk
while n_btrees:
    n_bytes = (available_memory / 2) / n_btrees
    btrees = [read_btree_from_disk(i, n_bytes)
              for i in enumerate(range(n_btrees))]
    lowest_candidates = [get_lowest(b) for b in btrees]
    lowest = min(lowest_candidates)
    lines = set()
    for i in range(number_of_btrees):
        tree = btrees[i]
        if lowest == lowest_candidates[i]:
            node = tree.pop_lowest()
            lines.update(node.lines)
        if tree.is_empty():
        n_btrees -= 1

    if output_memory_is_full or n_btrees == 0:
        outs.append_on_disk(lines)

答案 5 :(得分:0)

如何使用heapq模块读取文件到内存限制并将它们写出已排序的部分(heapq始终按排序顺序保存)。

或者你可以抓住第一个单词并将文件分成碎片。然后你可以读取这些行(也可以做'.join(line.split())来统一行间距/标签,如果可以改变间距)按字母顺序设置清除片段之间的集合(设置删除)重复)将事物排序一半(设置不按顺序,如果你想要你可以读入堆并写出来获得排序顺序,最后一次出现在set中替换旧值。)或者你也可以排序并使用Joe Koberg的groupby解决方案删除重复的行。最后,您可以将各个部分重新组合在一起(当然,您可以在整理文件时一步一步地写入最终文件)