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

2 投票
2 回答
3211 浏览
提问于 2025-04-17 17:47

这段内容主要是关于寻找最快的方法来解决一个问题。我有一个文件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 个回答

0

我不是算法高手,但我觉得这样做应该能让你的程序跑得更快。你需要把“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”,我们只需要在树上走一遍,最后数一下集合的大小就行了。

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

顺便说一下,我假设你每行有多个用制表符分隔的单词,因为你在某个地方使用了 line.split("\t")。如果这个假设不对,修改代码应该很简单。

另外,如果这个代码运行得太慢(你得试试看,但我猜大概应该在10分钟左右完成,考虑到你说的字符串数量)。你可能需要使用后缀树,正如评论中提到的。

编辑:我修改了代码,使它能够处理 file2 中相同字符串的多次出现,而不会影响性能。

编辑2:在时间和空间之间做了权衡。

下面的代码会消耗不少内存,并且构建字典需要一些时间。不过,一旦完成,从一百万个字符串中搜索时,每次查找的时间应该和单次哈希表查找一样快,也就是 O(1)

注意,我添加了一些语句来记录每个步骤所花的时间。你应该保留这些,这样你就能知道搜索时哪个部分耗时最多。因为你只用1000个字符串进行测试,这一点非常重要。如果90%的时间花在构建上,而不是搜索上,那么当你用100万个字符串测试时,构建只需做一次,所以就没关系了。

另外,我已经修改了我的代码,以便像你一样解析 file1file2,所以你应该可以直接插入并测试:

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()

撰写回答