在Python中从大文件中删除重复行
我有一个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 在下面提供了一个很好的实际解决方案;我很好奇从算法的角度来看,是否还有更好、更快、更简单的方法。
附言:我只能使用Python 2.5。
6 个回答
你现在的方法不一定能正常工作。
首先,有一种小概率情况是,两个实际上不同的内容可能会产生相同的哈希值。也就是说,hash(a) == hash(b)
并不总是意味着 a == b
。
其次,你的“reduce/lambda”做法会增加这种概率:
>>> reduce(lambda x,y: x+y, ['foo', '1', '23'])
'foo123'
>>> reduce(lambda x,y: x+y, ['foo', '12', '3'])
'foo123'
>>>
顺便问一下,使用 ''.join(['foo', '1', '23'])
不是更清晰吗?
再问一下,为什么不使用 set
而不是 dict
来做 htable
呢?
这里有一个实用的解决方案:从 GnuWin32 网站获取“核心工具”包并安装。然后:
- 将你的文件复制一份,去掉标题,命名为 (比如) infile.csv。
- 运行
c:\gnuwin32\bin\sort --unique -o outfile.csv infile.csv
。 - 读取 outfile.csv,并写一份带有标题的副本。
在步骤 1 和 3 中,你可以使用 Python 脚本,或者其他一些 GnuWin32 工具(如 head、tail、tee、cat 等)。
你基本上是在做一种叫归并排序的操作,同时还在去掉重复的条目。
把输入的数据分成适合内存大小的小块,先对每一块进行排序,然后再把这些块合并在一起,同时去掉重复的内容,这个方法总体来说是个不错的主意。
实际上,如果数据量在几GB以内,我会让虚拟内存系统来处理这些事情,直接写:
input = open(infilename, 'rb')
output = open(outfile, 'wb')
for key, group in itertools.groupby(sorted(input)):
output.write(key)
如果你想要一个非常简单的方法来实现这个,可以创建一个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来处理这些事情。虽然它的速度可能不会比对字符串进行哈希处理快多少,但这样做可能会简单很多。当然,如果你想的话,可以修改存储在数据库中的数据类型,或者不修改也可以。其实因为你已经把数据转换成字符串了,所以也可以只用一个字段。这里有很多选择。