Python在文件中搜索一百万个字符串并计算每个字符串的出现次数

时间:2013-03-03 20:39:10

标签: python performance string-search

这更像是找到最快的方法。 我有一个file1,其中包含大约一百万个字符串(长度为6-40)。我想在另一个文件中搜索每个文件,其中包含大约80,000个字符串并计算出现次数(如果在一个字符串中多次找到小字符串,则该字符串的出现仍为1)。如果有人有兴趣比较性能,则有下载file1和file2的链接。 dropbox.com/sh/oj62918p83h8kus/sY2WejWmhu?m

我现在正在做的是为文件2构造一个字典,使用字符串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秒

####### NEXT方法由熵建议
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提出的Haystack和Needle方法,结果证明是最快的。这个方法的问题是所有子串的计数结果都是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()

完成只需0分11秒,这比其他方法快得多。但是,我不知道将所有计数结果都设为0是错误的,或者Mark的方法存在缺陷。

2 个答案:

答案 0 :(得分:3)

您的代码似乎不起作用(您确定您不是仅仅从内存中引用它而不是粘贴实际代码吗?)

例如,这一行:

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

PS:我假设每行有多个单词是制表符分割的,因为你在某个时候会line.split("\t")。如果这是错误的,那么修改代码应该很容易。

PPS:如果这对你的使用来说太慢了(你必须尝试一下,但是我的猜测是这应该在大约10分钟内运行,因为你说的字符串数量)。您必须使用后缀树作为建议的注释之一。

编辑:修改了代码,以便它处理file2中同一字符串的多次出现,而不会对性能产生负面影响

编辑2:交易最长时间空间。

下面的代码会消耗相当多的内存并需要一段时间来构建字典。但是,一旦完成,要搜索的百万个字符串中的每个搜索都应该在单个哈希表查找所需的时间内完成,即O(1)

注意,我添加了一些语句来记录流程每个步骤所需的时间。您应该保留这些内容,以便了解搜索时间的哪一部分。由于您只使用1000个字符串进行测试,这一点非常重要,因为如果90%的成本是构建时间而不是搜索时间,那么当您使用1M字符串进行测试时,您仍然只会执行一次,因此它不会物质

另请注意,我已经修改了我的代码来解析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()

答案 1 :(得分:0)

我不是算法专家,但我认为这应该会给你带来健康的性能提升。您需要将'haystacks'设置为要查看的大字的列表,并将'needle'设置为您要查找的子字符串列表(可以包含重复项),我会告诉您实施你的目的。如果您可以发布您的针头列表和干草堆列表,那就太棒了,这样我们就可以轻松地比较建议解决方案的性能。

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)

你可以通过查看代码来弄清楚正在发生什么,但只是用文字来表达:我构造了一个巨大的干草堆字的子串树,这样给定任何针,你可以通过字符并最终在一个节点上,该节点附加了包含该子字符串的干草堆的所有干草堆id的集合。然后,对于每个针,我们只需穿过树并计算最后一组的大小。