【问题标题】:Find all Occurences of Every Substring in String查找字符串中每个子字符串的所有出现
【发布时间】:2013-04-03 13:54:55
【问题描述】:

我正在尝试在主字符串(所有长度)中查找所有出现的子字符串。我的函数接受一个字符串,然后返回每个子字符串的字典(当然,它出现多次)以及它出现的次数(字典格式:{substring: # of occurrences, ...})。我正在使用collections.Counter(s) 来帮助我。

这是我的功能:

from collections import Counter

def patternFind(s):
    patterns = {}
    for index in range(1, len(s)+1)[::-1]:
        d = nChunks(s, step=index)
        parts = dict(Counter(d))
        patterns.update({elem: parts[elem] for elem in parts.keys() if parts[elem] > 1})
    return patterns

def nChunks(iterable, start=0, step=1):
    return [iterable[i:i+step] for i in range(start, len(iterable), step)]

我有一个字符串data,其中包含大约 2500 个随机字母(以随机顺序)。但是,其中插入了 2 个字符串(随机点)。假设这个字符串是'TEST'。 data.count('TEST') 返回 2。但是,patternFind(data)['TEST'] 给了我一个 KeyError。因此,我的程序没有检测到其中的两个字符串。

我做错了什么?谢谢!

编辑:我创建测试实例的方法:

def createNewTest():
    n = randint(500, 2500)
    x, y = randint(500, n), randint(500, n)
    s = ''
    for i in range(n):
        s += choice(uppercase)
        if i == x or i == y: s += "TEST"
    return s

【问题讨论】:

  • 你能说明你是如何在你的字符串中插入“TEST”的吗?你的代码似乎对我来说很完美。我用s = [random.choice(string.letters+string.digits) for i in range(2500)] 创建了一个测试字符串,然后使用s[ind:ind+4] = "TEST" 在几个地方插入了“TEST”,并用s = ''.join(s) 将列表加入到一个字符串中。打电话给out = patternFind(s) 给了我一个out['TEST'] == 2 的字典。
  • @Vorticity 用我创建字符串的方式编辑了我的代码。我用 100 根弦试了一下,只有 29 根是正确的。有时有效,有时无效。
  • 我想我的第一个回复是错误的。代码不能按原样工作。我现在没有时间了,但至少我可以给你一个提示。开始使用更短的字符串(比如n = randint(20, 40))并在每次循环中打印parts。你会发现你并没有考虑所有的可能性。您的查找程序将变得更加复杂,我认为您最好尝试回到最初的问题并找出您真正在寻找的是什么。
  • 或者,看起来 mezzomondo 可能会给你一个很好的答案。
  • +1 激发了这场“竞赛”:)

标签: python regex string


【解决方案1】:

使用正则表达式

除了您描述的count() 方法之外,正则表达式是一个明显的替代方法

import re

needle = r'TEST'

haystack = 'khjkzahklahjTESTkahklaghTESTjklajhkhzkhjkzahklahjTESTkahklagh'
pattern = re.compile(needle)

print len(re.findall(pattern, haystack))

捷径

如果您需要构建子字符串字典,您可以只使用这些字符串的子集。假设您知道您在data 中寻找的needle,那么您只需要与needle 长度相同的data 子字符串字典。这非常快。

from collections import Counter

needle = "TEST"

def gen_sub(s, len_chunk):
    for start in range(0, len(s)-len_chunk+1):
        yield s[start:start+len_chunk]

data = 'khjkzahklahjTESTkahklaghTESTjklajhkhzkhjkzahklahjTESTkahklaghTESz'
parts = Counter([sub for sub in gen_sub(data, len(needle))])

print parts[needle]

蛮力:构建所有子字符串的字典

如果您需要计算所有可能的子字符串,这可行,但速度很慢:

from collections import Counter

def gen_sub(s):
    for start in range(0, len(s)):
        for end in range(start+1, len(s)+1):
            yield s[start:end]

data = 'khjkzahklahjTESTkahklaghTESTjklajhkhz'
parts = Counter([sub for sub in gen_sub(data)])

print parts['TEST']

由此改编的子串生成器:https://stackoverflow.com/a/8305463/1290420

【讨论】:

  • 不过,2500 个字母的字符串需要很长时间。
  • 我想找到主字符串中每个子字符串的模式(仅给定主字符串)。我不一定需要蛮力,但这就是我最初想到的方式。如果有其他方法,请告诉我。
  • 添加了两个比蛮力方法更快的变体。希望这会有所帮助。
  • 问题是,我不知道“针”。我尝试过d = {},然后是for i in range(1, len(data)+1): parts.update(dict(Counter([sub for sub in gen_sub(data, i)]))),但这需要很长时间并且实际上冻结了我的电脑。
  • 在 2500 个字符的字符串中查找所有可能的子字符串,所有可能的长度会花费过多的时间。您可能需要重新分析问题并选择不同的解决方法。我将在此处留下我的答案,以向其他人展示不适合您的内容。
【解决方案2】:

虽然 jurgenreza 已经解释了您的程序无法运行的原因,但解决方案仍然很慢。如果您只检查您知道s[:-1] 重复的子字符串s,您会得到一个更快的解决方案(通常快一百倍甚至更多):

from collections import defaultdict

def pfind(prefix, sequences):
    collector = defaultdict(list)
    for sequence in sequences:
        collector[sequence[0]].append(sequence)
    for item, matching_sequences in collector.items():
        if len(matching_sequences) >= 2:
            new_prefix = prefix + item
            yield (new_prefix, len(matching_sequences))
            for r in pfind(new_prefix, [sequence[1:] for sequence in matching_sequences]):
                yield r

def find_repeated_substrings(s):
    s0 = s + " "
    return pfind("", [s0[i:] for i in range(len(s))])

如果你想要一个字典,你可以这样称呼它:

result = dict(find_repeated_substrings(s))

在我的机器上,运行 2247 个元素需要 0.02 秒,而原始(修正后的)解决方案需要 12.72 秒。

(请注意,这是一个相当幼稚的实现;使用索引而不是子字符串应该更快。)

编辑:以下变体适用于其他序列类型(不仅仅是字符串)。此外,它不需要哨兵元素。

from collections import defaultdict

def pfind(s, length, ends):
    collector = defaultdict(list)
    if ends[-1] >= len(s):
        del ends[-1]
    for end in ends:
        if end < len(s):
            collector[s[end]].append(end)
    for key, matching_ends in collector.items():
        if len(matching_ends) >= 2:
            end = matching_ends[0]
            yield (s[end - length: end + 1], len(matching_ends))
            for r in pfind(s, length + 1, [end + 1 for end in matching_ends if end < len(s)]):
                yield r


def find_repeated_substrings(s):
    return pfind(s, 0, list(range(len(s))))

这仍然存在很长的子字符串会超过递归深度的问题。您可能想捕获异常。

【讨论】:

  • 这要快得多,谢谢!不过,您能解释一下每个步骤的工作原理吗?
  • @WolframH - 这是迄今为止最快的解决方案,我在这里奖励赏金。递归解决方案完全重新定义了问题并且是最优雅的。一个缺点:如果子字符串在主字符串中出现的频率很高,那么函数会超出递归深度而崩溃;应该捕获异常并优雅地失败或恢复到较慢的解决方案之一。您可能想在答案中注明这一点?再次恭喜!
  • 我仍然不确定它是如何工作的(我没有经常使用生成器);我尝试更改为使用列表(将其转换为元组),但我无法让它与它一起使用。您能否添加一个小节,说明它将如何用于列表(或任何非字符串可迭代对象)?谢谢:3
【解决方案3】:

在这里您可以找到一个解决方案,它使用围绕 string.find() 的递归包装器搜索主字符串中子字符串的所有出现。 collectallchuncks() 函数返回一个 defaultdict ,其中所有子字符串都作为键,并且对于每个子字符串,都有一个在主字符串中找到该子字符串的所有索引的列表。

import collections

# Minimum substring size, may be 1
MINSIZE = 3

# Recursive wrapper
def recfind(p, data, pos, acc):
    res = data.find(p, pos)
    if res == -1:
        return acc
    else:
        acc.append(res)
        return recfind(p, data, res+1, acc)

def collectallchuncks(data):
    res = collections.defaultdict(str)
    size = len(data)
    for base in xrange(size):
        for seg in xrange(MINSIZE, size-base+1):
            chunk = data[base:base+seg]
            if data.count(chunk) > 1:
                res[chunk] = recfind(chunk, data, 0, [])
    return res

if __name__ == "__main__":
    data = 'khjkzahklahjTESTkahklaghTESTjklajhkhzkhjkzahklahjTESTkahklaghTESz'
    allchuncks = collectallchuncks(data)
    print 'TEST', allchuncks['TEST']
    print 'hklag', allchuncks['hklag']

编辑:如果您只需要主字符串中每个子字符串的出现次数,您可以轻松获得它,摆脱递归函数:

import collections

MINSIZE = 3

def collectallchuncks2(data):
    res = collections.defaultdict(str)
    size = len(data)
    for base in xrange(size):
        for seg in xrange(MINSIZE, size-base+1):
            chunk = data[base:base+seg]
            cnt = data.count(chunk)
            if cnt > 1:
                res[chunk] = cnt
    return res

if __name__ == "__main__":
    data = 'khjkzahklahjTESTkahklaghTESTjklajhkhzkhjkzahklahjTESTkahklaghTESz'
    allchuncks = collectallchuncks2(data)
    print 'TEST', allchuncks['TEST']
    print 'hklag', allchuncks['hklag']

【讨论】:

  • 这类似于 gauden 所拥有的,除了一个功能。对于一个 2500 字左右的文件,耗时 46 秒,相当长。
  • 我有点怀疑您是否会使用 python 找到更快的方法。这可能是您想降到 C 以提高速度或重新考虑您需要什么的时候。
  • @Vorticity:看我的回答;即使在 python 中它也可以更快。
  • 这是一个绝妙的答案。感谢您提醒我回到这个问题并祝贺您获得赏金。
【解决方案4】:

问题出在您的nChunks 函数中。它不会为您提供所有必要的块。

让我们考虑一个测试字符串:

s='1test2345test'

对于大小为 4 的块,您的 nChunks 函数会给出以下输出:

>>>nChunks(s, step=4)
['1tes', 't234', '5tes', 't']

但你真正想要的是:

>>>def nChunks(iterable, start=0, step=1):
    return [iterable[i:i+step] for i in range(len(iterable)-step+1)]
>>>nChunks(s, step=4)
['1tes', 'test', 'est2', 'st23', 't234', '2345', '345t', '45te', '5tes', 'test']

你可以看到这样有两个 'test' 块,你的 patternFind(s) 会像一个魅力一样工作:

>>> patternFind(s)
{'tes': 2, 'st': 2, 'te': 2, 'e': 2, 't': 4, 'es': 2, 'est': 2, 'test': 2, 's': 2}

【讨论】:

  • 优雅,但本质上与我的捷径功能没有什么不同。问题只是部分子串的生成;真正的问题是 2500 个字符的字符串需要多长时间。
  • @gauden 我没有仔细阅读 cmets。我专注于问题中提出的问题;在那里,你问“我做错了什么?”并且没有提到速度。也许您应该创建另一个问题来解决操作的规模和速度。
  • @gauden 对不起,我以为你是 OP。 :)
  • @jurgenreza 这是(我发现的)最快的方法...对于 2500 个字符的文件,耗时 19 秒,这比其他的要好得多...谢谢!跨度>
猜你喜欢
  • 1970-01-01
  • 2013-03-22
  • 1970-01-01
  • 2015-12-23
  • 1970-01-01
  • 2012-10-12
  • 1970-01-01
相关资源
最近更新 更多