Python:在字符串列表中优化搜索子字符串

时间:2016-01-15 17:40:25

标签: python substring

我有一个特殊的问题,我想在许多字符串列表中搜索许多子字符串。以下是我要做的事情的要点:

listStrings = [ACDE, CDDE, BPLL, ... ]

listSubstrings = [ACD, BPI, KLJ, ...]

以上条目只是示例。 len(listStrings)是~60,000,len(listSubstrings)是~50,000-300,000,len(listStrings [i])是10到30,000。

我目前的Python尝试是:

for i in listSubstrings:
   for j in listStrings:
       if i in j:
          w.write(i+j)

或者沿着这些方向的东西。虽然这适用于我的任务,但它非常慢,使用一个核心并按照40分钟的顺序完成任务。有没有办法加快速度呢?

我不相信我可以用listStrings制作一个字典:listSubstrings,因为有可能需要在两端存储重复的条目(尽管我可以试试这个,如果我能找到一种方法为每个标签附加一个唯一的标签,因为dicts的速度要快得多。同样,我也不认为我可以预先计算可能的子串。我甚至不知道搜索dict键是否比搜索列表更快(因为dict.get()将提供特定输入而不寻找子输入)。在内存中搜索列表是否相对较慢?

5 个答案:

答案 0 :(得分:11)

对于你正在尝试的那种东西(在一大堆其他字符串中搜索一大堆字符串的固定集合),并行化和小调整将无济于事。您需要进行算法改进。

首先,我建议使用Aho-Corasick string matching algorithm。基本上,为了换取一些预先计算的工作来从你的固定字符串集合中构建一个匹配器对象,你可以一次扫描另一个字符串,一次性扫描那些固定字符串的所有

因此,不是每次扫描60K字符串50K +(三百亿次扫描?!?),您可以每次扫描一次,成本仅比普通单次扫描略高,并获得所有命中。

最重要的是,你不是自己写的。 PyPI(Python包索引)已经为您编写了pyahocorasick包。试试吧。

使用示例:

import ahocorasick

listStrings = [ACDE, CDDE, BPLL, ...]
listSubstrings = [ACD, BPI, KLJ, ...]

auto = ahocorasick.Automaton()
for substr in listSubstrings:
    auto.add_word(substr, substr)
auto.make_automaton()

...

for astr in listStrings:
    for end_ind, found in auto.iter(astr):
        w.write(found+astr)

如果在被搜索的字符串(“haystack”)中找到一个子串(“needle”)不止一次,这将write多次。您可以通过使用write重复数据删除来更改循环,使其在给定大海捞针的第一次击中时仅set

for astr in listStrings:
    seen = set()
    for end_ind, found in auto.iter(astr):
        if found not in seen:
            seen.add(found)
            w.write(found+astr)

你可以进一步调整这个以输出针对给定草垛的针头,它们以listSubstrings中出现的顺序输出(并且在你使用它时无需通知),方法是将单词的索引存储为或者使用它们的值所以你可以对命中数进行排序(可能是小数字,因此排序开销很小):

from future_builtins import map  # Only on Py2, for more efficient generator based map
from itertools import groupby
from operator import itemgetter

auto = ahocorasick.Automaton()
for i, substr in enumerate(listSubstrings):
    # Store index and substr so we can recover original ordering
    auto.add_word(substr, (i, substr))
auto.make_automaton()

...

for astr in listStrings:
    # Gets all hits, sorting by the index in listSubstrings, so we output hits
    # in the same order we theoretically searched for them
    allfound = sorted(map(itemgetter(1), auto.iter(astr)))
    # Using groupby dedups already sorted inputs cheaply; the map throws away
    # the index since we don't need it
    for found, _ in groupby(map(itemgetter(1), allfound)):
        w.write(found+astr)

对于性能比较,我在mgc的答案中使用了一个变体,它更可能包含匹配,以及扩大干草堆。首先,设置代码:

>>> from random import choice, randint
>>> from string import ascii_uppercase as uppercase
>>> # 5000 haystacks, each 1000-5000 characters long
>>> listStrings = [''.join([choice(uppercase) for i in range(randint(1000, 5000))]) for j in range(5000)]
>>> # ~1000 needles (might be slightly less for dups), each 3-12 characters long
>>> listSubstrings = tuple({''.join([choice(uppercase) for i in range(randint(3, 12))]) for j in range(1000)})
>>> auto = ahocorasick.Automaton()
>>> for needle in listSubstrings:
...     auto.add_word(needle, needle)
...
>>> auto.make_automaton()

现在要进行实际测试(使用ipython %timeit magic for microbenchmarks):

>>> sum(needle in haystack for haystack in listStrings for needle in listSubstrings)
80279  # Will differ depending on random seed
>>> sum(len(set(map(itemgetter(1), auto.iter(haystack)))) for haystack in listStrings)
80279  # Same behavior after uniquifying results
>>> %timeit -r5 sum(needle in haystack for haystack in listStrings for needle in listSubstrings)
1 loops, best of 5: 9.79 s per loop
>>> %timeit -r5 sum(len(set(map(itemgetter(1), auto.iter(haystack)))) for haystack in listStrings)
1 loops, best of 5: 460 ms per loop

因此,为了检查5000个中等大小字符串中的〜1000个小字符串,pyahocorasick在我的机器上击败个别成员资格测试的系数约为21倍。随着listSubstrings的大小增加,它也可以很好地扩展;当我以相同的方式对其进行初始化时,但是使用10,000个小字符串而不是1000个字符串,所需的总时间从~460 ms增加到〜852 ms,这是1.85倍时间乘数,执行10倍的逻辑搜索。

对于记录,在这种情况下构建自动机的时间微不足道。你需要在每个干草堆前面支付一次,并且测试显示~1000字符串自动机需要大约1.4毫秒才能构建并占用~277 KB的内存(超出字符串本身); ~10000字符串自动机需要~21 ms才能构建,占用~2.45 MB的内存。

答案 1 :(得分:1)

也许你可以尝试将两个列表中的一个(最大的?虽然直观地我会剪切listStrings)中的一个列表然后使用线程并行运行这些搜索(Pool class of multiprocessing提供了方便的方法这样做)?我使用类似的东西进行了一些显着的加速:

from multiprocessing import Pool
from itertools import chain, islice

# The function to be run in parallel :
def my_func(strings):
    return [j+i for i in strings for j in listSubstrings if i.find(j)>-1]

# A small recipe from itertools to chunk an iterable :
def chunk(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

# Generating some fake & random value :
from random import randint
listStrings = \
    [''.join([chr(randint(65, 90)) for i in range(randint(1, 500))]) for j in range(10000)]
listSubstrings = \
    [''.join([chr(randint(65, 90)) for i in range(randint(1, 100))]) for j in range(1000)]

# You have to prepare the searches to be performed:
prep = [strings for strings in chunk(listStrings, round(len(listStrings) / 8))]
with Pool(4) as mp_pool:
    # multiprocessing.map is a parallel version of map()
    res = mp_pool.map(my_func, prep)
# The `res` variable is a list of list, so now you concatenate them
# in order to have a flat result list
result = list(chain.from_iterable(res))

然后你可以编写整个result变量(而不是逐行编写):

with open('result_file', 'w') as f:
    f.write('\n'.join(result))

编辑01/05/18:按照ShadowRanger的建议,使用itertools.chain.from_iterable代替使用map副作用的丑陋变通方法展平结果。 < / p>

答案 2 :(得分:0)

你的子串长度是否相同?您的示例使用3个字母的子字符串。在这种情况下,您可以创建一个带有3个字母子串的dict作为字符串列表的键:

index = {}
for string in listStrings:
    for i in range(len(string)-2):
        substring = string[i:i+3]
        index_strings = index.get(substring, [])
        index_strings.append(string)
        index[substring] = index_strings

for substring in listSubstrings:
    index_strings = index.get(substring, [])
    for string in index_strings:
        w.write(substring+string)

答案 3 :(得分:0)

通过将listString连接成一个长字符串(或者从文件中读取字符串而不将其拆分为换行符),可以显着加快内部循环。

with open('./testStrings.txt') as f:
    longString = f.read()               # string with seqs separated by \n

with open('./testSubstrings.txt') as f:
    listSubstrings = list(f)

def search(longString, listSubstrings):
    for n, substring in enumerate(listSubstrings):
        offset = longString.find(substring)
        while offset >= 0:
            yield (substring, offset)
            offset = longString.find(substring, offset + 1)

matches = list(search(longString, listSubstrings))

可以将偏移量映射到字符串索引。

from bisect import bisect_left
breaks = [n for n,c in enumerate(longString) if c=='\n']

for substring, offset in matches:
    stringindex = bisect_left(breaks, offset)

我的测试表明,与嵌套for循环相比,速度提高了7倍(11秒对77秒)。

答案 4 :(得分:-1)

使用内置列表功能可以加快速度。

for i in listSubstrings:
   w.write(list(map(lambda j: i + j, list(lambda j: i in j,listStrings))))

从运行时间复杂度分析来看,最坏的情况似乎是n ^ 2比较,因为您需要在给定当前问题结构的情况下浏览每个列表。你需要担心的另一个问题是内存消耗,因为更大的规模,更多的内存通常是瓶颈。

正如您所说,您可能希望索引字符串列表。我们可以知道的子串列表或字符串列表是否有任何模式?例如,在您的示例中,我们可以索引哪些字符串具有字母表中的哪些字符{&#34; A&#34;:[&#34; ABC&#34;,&#34; BAW&#34;,&#34 ; CMAI&#34;] ...}因此我们不需要每次为每个子串元素列表遍历字符串列表。