Python在文件中搜索百万字符串并计算每个字符串的出现次数
这段内容主要是关于寻找最快的方法来解决一个问题。我有一个文件file1,里面大约有一百万个字符串,每个字符串的长度在6到40个字符之间,都是单独一行。我想在另一个文件file2中查找这些字符串,file2里面大约有80,000个字符串,并且统计每个字符串出现的次数(如果一个小字符串在一个大字符串中出现多次,计数仍然算作1)。如果有人想比较性能,这里有下载file1和file2的链接:dropbox.com/sh/oj62918p83h8kus/sY2WejWmhu?m
我现在做的是为file2构建一个字典,用字符串的ID作为键,字符串本身作为值。(因为file2中的字符串有重复值,只有字符串ID是唯一的)我的代码是:
for line in file1:
substring=line[:-1].split("\t")
for ID in dictionary.keys():
bigstring=dictionary[ID]
IDlist=[]
if bigstring.find(substring)!=-1:
IDlist.append(ID)
output.write("%s\t%s\n" % (substring,str(len(IDlist))))
我的代码需要几个小时才能完成。有没有人能建议一个更快的方法?file1和file2的大小都在50M左右,我的电脑有8G内存,你可以使用尽可能多的内存来加快速度。任何能在一个小时内完成的方法都是可以接受的:)
在这里,我尝试了一些评论中的建议,看看性能比较,先是代码,然后是运行时间。
一些人,比如Mark Amery,建议了一些改进:
import sys
from Bio import SeqIO
#first I load strings in file2 to a dictionary called var_seq,
var_seq={}
handle=SeqIO.parse(file2,'fasta')
for record in handle:
var_seq[record.id]=str(record.seq)
print len(var_seq) #Here print out 76827, which is the right number. loading file2 to var_seq doesn't take long, about 1 second, you shall not focus here to improve performance
output=open(outputfilename,'w')
icount=0
input1=open(file1,'r')
for line in input1:
icount+=1
row=line[:-1].split("\t")
ensp=row[0] #ensp is just peptides iD
peptide=row[1] #peptides is the substrings i want to search in file2
num=0
for ID,bigstring in var_seq.iteritems():
if peptide in bigstring:
num+=1
newline="%s\t%s\t%s\n" % (ensp,peptide,str(num))
output.write(newline)
if icount%1000==0:
break
input1.close()
handle.close()
output.close()
这个方法花了1分4秒完成。比我之前的代码快了20秒。
#######接下来是entropy建议的方法from collections import defaultdict
var_seq=defaultdict(int)
handle=SeqIO.parse(file2,'fasta')
for record in handle:
var_seq[str(record.seq)]+=1
print len(var_seq) # here print out 59502, duplicates are removed, but occurances of duplicates are stored as value
handle.close()
output=open(outputfilename,'w')
icount=0
with open(file1) as fd:
for line in fd:
icount+=1
row=line[:-1].split("\t")
ensp=row[0]
peptide=row[1]
num=0
for varseq,num_occurrences in var_seq.items():
if peptide in varseq:
num+=num_occurrences
newline="%s\t%s\t%s\n" % (ensp,peptide,str(num))
output.write(newline)
if icount%1000==0:
break
output.close()
这个方法花了1分10秒,速度没有预期的快,因为它避免了搜索重复项,我不太明白为什么。
Mark Amery建议的“干草堆和针”方法,结果证明是最快的。不过这个方法的问题是所有子字符串的计数结果都是0,我还不太明白这个原因。
这是我实现他的方法的代码:
class Node(object):
def __init__(self):
self.words = set()
self.links = {}
base = Node()
def search_haystack_tree(needle):
current_node = base
for char in needle:
try:
current_node = current_node.links[char]
except KeyError:
return 0
return len(current_node.words)
input1=open(file1,'r')
needles={}
for line in input1:
row=line[:-1].split("\t")
needles[row[1]]=row[0]
print len(needles)
handle=SeqIO.parse(file2,'fasta')
haystacks={}
for record in handle:
haystacks[record.id]=str(record.seq)
print len(haystacks)
for haystack_id, haystack in haystacks.iteritems(): #should be the same as enumerate(list)
for i in xrange(len(haystack)):
current_node = base
for char in haystack[i:]:
current_node = current_node.links.setdefault(char, Node())
current_node.words.add(haystack_id)
icount=0
output=open(outputfilename,'w')
for needle in needles:
icount+=1
count = search_haystack_tree(needle)
newline="%s\t%s\t%s\n" % (needles[needle],needle,str(count))
output.write(newline)
if icount%1000==0:
break
input1.close()
handle.close()
output.close()
这个方法只花了11秒就完成,比其他方法快多了。不过,我不知道是我代码的问题导致所有计数结果都是0,还是Mark的方法本身有缺陷。
2 个回答
我不是算法高手,但我觉得这样做应该能让你的程序跑得更快。你需要把“haystacks”设置成一个包含你想查找的大词的列表,把“needles”设置成一个包含你要找的子字符串的列表(这两个列表都可以有重复的词),具体怎么实现就靠你自己了。如果你能把你的“needles”和“haystacks”列表发出来,那样我们就能更方便地比较不同方案的性能了。
haystacks = <some list of words>
needles = <some list of words>
class Node(object):
def __init__(self):
self.words = set()
self.links = {}
base = Node()
for haystack_id, haystack in enumerate(haystacks):
for i in xrange(len(haystack)):
current_node = base
for char in haystack[i:]:
current_node = current_node.links.setdefault(char, Node())
current_node.words.add(haystack_id)
def search_haystack_tree(needle):
current_node = base
for char in needle:
try:
current_node = current_node.links[char]
except KeyError:
return 0
return len(current_node.words)
for needle in needles:
count = search_haystack_tree(needle)
print "Count for %s: %i" % (needle, count)
你可能通过看代码就能明白发生了什么,但我还是用简单的话说一下:我构建了一个巨大的树形结构,里面包含了“haystack”词的所有子字符串。这样一来,给定任何一个“needle”,你就可以一个字符一个字符地在树上导航,最终找到一个节点,这个节点上会附带所有包含这个子字符串的“haystack”的ID。然后对于每个“needle”,我们只需要在树上走一遍,最后数一下集合的大小就行了。
你的代码看起来好像不太对(你确定不是记错了而是直接粘贴的代码吗?)
比如,这一行:
substring=line[:-1].split("\t")
会导致 substring
变成一个列表。但后来你又做了:
if bigstring.find(substring)!=-1:
如果你调用 str.find(list)
,就会出错。
无论如何,你在最里面的循环中无意义地创建了列表。这一行:
IDlist=[]
if bigstring.find(substring)!=-1:
IDlist.append(ID)
#later
len(IDlist)
会无意义地分配和释放列表,这样会导致内存频繁使用,进而拖慢程序的运行速度。
下面是应该能正常工作的代码,它用更有效的方法来进行计数:
from collections import defaultdict
dictionary = defaultdict(int)
with open(file2) as fd:
for line in fd:
for s in line.split("\t"):
dictionary[s.strip()] += 1
with open(file1) as fd:
for line in fd:
for substring in line.split('\t'):
count = 0
for bigstring,num_occurrences in dictionary.items():
if substring in bigstring:
count += num_occurrences
print substring, count
顺便说一下,我假设你每行有多个用制表符分隔的单词,因为你在某个地方使用了 line.split("\t")
。如果这个假设不对,修改代码应该很简单。
另外,如果这个代码运行得太慢(你得试试看,但我猜大概应该在10分钟左右完成,考虑到你说的字符串数量)。你可能需要使用后缀树,正如评论中提到的。
编辑:我修改了代码,使它能够处理 file2
中相同字符串的多次出现,而不会影响性能。
编辑2:在时间和空间之间做了权衡。
下面的代码会消耗不少内存,并且构建字典需要一些时间。不过,一旦完成,从一百万个字符串中搜索时,每次查找的时间应该和单次哈希表查找一样快,也就是 O(1)
。
注意,我添加了一些语句来记录每个步骤所花的时间。你应该保留这些,这样你就能知道搜索时哪个部分耗时最多。因为你只用1000个字符串进行测试,这一点非常重要。如果90%的时间花在构建上,而不是搜索上,那么当你用100万个字符串测试时,构建只需做一次,所以就没关系了。
另外,我已经修改了我的代码,以便像你一样解析 file1
和 file2
,所以你应该可以直接插入并测试:
from Bio import SeqIO
from collections import defaultdict
from datetime import datetime
def all_substrings(s):
result = set()
for length in range(1,len(s)+1):
for offset in range(len(s)-length+1):
result.add(s[offset:offset+length])
return result
print "Building dictionary...."
build_start = datetime.now()
dictionary = defaultdict(int)
handle = SeqIO.parse(file2, 'fasta')
for record in handle:
for sub in all_substrings(str(record.seq).strip()):
dictionary[sub] += 1
build_end = datetime.now()
print "Dictionary built in: %gs" % (build-end-build_start).total_seconds()
print "Searching...\n"
search_start = datetime.now()
with open(file1) as fd:
for line in fd:
substring = line.strip().split("\t")[1]
count = dictionary[substring]
print substring, count
search_end = datetime.now()
print "Search done in: %gs" % (search_end-search_start).total_seconds()