【问题标题】:Python string comparison pointing to the result指向结果的Python字符串比较
【发布时间】:2012-10-11 13:17:09
【问题描述】:

我正在尝试比较 2 1000 字节的字符串,并想知道差异究竟从哪里开始,即;字符串与哪个字节不同..有什么函数可以确定吗?

【问题讨论】:

  • 好像这样的东西应该在difflib,但我找不到。
  • @mgilson,我使用 difflib 给出了答案,但我认为与生成器表达式相比使用起来相当笨拙

标签: python


【解决方案1】:

也许使用next 加上一个生成器?

next(idx for idx,c in enumerate(your_string1) if c != your_string2[idx])

这将为您提供差异开始的索引,如果它们相等,则提高 StopIteration

使用itertools.izip 可能会更优雅一些:

next(idx for idx,(c1,c2) in enumerate(izip(s1,s2)) if c1 != c2)

例子:

>>> from itertools import izip
>>> s1 = 'stop at the bus'
>>> s2 = 'stop at the car'
>>> next(idx for idx,(c1,c2) in enumerate(izip(s1,s2)) if c1 != c2)
12
>>> s1[12]
'b'
>>> s2[12]
'c'

【讨论】:

  • 我最喜欢的 Python 模式之一。我经常想知道这是否是在引入基因组时预见到的,或者这是否只是良好设计的自然结果。
  • @senderle -- 是的,我不完全确定。我猜这是可以预见的,但我想肯定知道我们需要询问 Guido 和 python 开发人员。它也迅速成为我最喜欢的模式之一。非常有用。
【解决方案2】:

我已经尝试测试这里给出的答案,并且我想出了另一个更快(通常情况下)的解决方案,尽管它不太优雅。

首先让我们看看提出的解决方案有多快:

In [15]: def check_genexp(a, b):
    ...:     return next(idx for idx, c in enumerate(a) if c != b[idx])

In [16]: %timeit check_genexp("a"*9999 + "b", "a"*9999 + "c")
1000 loops, best of 3: 1.04 ms per loop

In [17]: from difflib import SequenceMatcher

In [18]: def check_matcher(a, b):
    ...:     return next(SequenceMatcher(a=a, b=b).get_matching_blocks())
    ...: 

In [19]: %timeit check_matcher("a"*9999+"b", "a"*9999+"c")
100 loops, best of 3: 11.5 ms per loop

如您所见,genexp 比 difflib 快很多,但这可能是因为 SequenceMatcher 所做的远不止找到第一个不相等的字符。

现在,我们怎样才能加快速度? 好吧,我们可以使用“二分查找”!!!这个想法是,如果两个字符串不相等,则前半部分不同或第二部分不同(或两者都不同,但在这种情况下,我们只关心前半部分,因为我们想要第一个不同的索引)。

所以我们可以这样做:

def binary_check(a, b):
    len_a, len_b = len(a), len(b)
    if len_a == len_b:
        return binary_check_helper(a, b)
    min_length, max_length = min(len_a, len_b), max(len_a, len_b)
    res = binary_check_helper(a[:min_length], b[:min_length])
    return res if res >= 0 else min_length

def binary_check_helper(a, b):
    if a == b:
        return -1
    length = len(a)

    if length == 1:
        return int(a[0] == b[0])
    else:
        half_length = length // 2
        r = binary_check_helper(a[:half_length], b[:half_length])
        if r >= 0:
            return r
        r = binary_check(a[half_length:], b[half_length:])
        if r >= 0:
            return r + half_length
        return r

结果:

In [34]: %timeit binary_check("a"*9999 + "b", "a"*9999 + "c")
10000 loops, best of 3: 28.4 µs per loop

这比 genexp 快 三十五倍

为什么会这样?比较显然需要线性时间,所以看起来我们比以前做了更多的工作......这确实是真的,但它是在“C级”完成的, 因此结果是这种方法实际上更快。

请注意,这在某种程度上是“特定于实现的”,因为诸如 PyPy 之类的实现可能会将 genexp 优化为单个 C-for 循环,这将胜过任何事情; 同样在 Jython 或 IronPython 等实现上可能比使用 CPython 慢很多。

此方法具有与其他方法相同的渐近复杂度,即 O(n)。字符串最多被分成两半 log_2(n) 次,每次进行相等性测试时,这需要线性时间。乍一看,它似乎是一个 Θ(n * logn) 算法,但事实并非如此。递推方程为:

T(n) = T(n//2) + Θ(n) = Σ_{i=0}^{logn}(n/2^i)
     = Θ(n(1 + 1/2 + 1/4 + ...)) <= Θ(2n) = Θ(n)

更多结果:

In [37]: %timeit binary_check("a"*10**6 + "b", "a"*10**6 + "c")
100 loops, best of 3: 2.84 ms per loop

In [38]: %timeit check_genexp("a"*10**6 + "b", "a"*10**6 + "c")
10 loops, best of 3: 101 ms per loop

In [39]: %timeit binary_check(15 * "a"*10**6 + "b", 15 * "a"*10**6 + "c")
10 loops, best of 3: 53.3 ms per loop

In [40]: %timeit check_genexp(15 * "a"*10**6 + "b", 15 * "a"*10**6 + "c")
1 loops, best of 3: 1.5 s per loop

正如您所看到的,即使使用巨大的字符串,这种方法仍然快大约 30 倍。

注意:这个解决方案的缺点是它是 ϴ(n) 而不是 O(n),即它总是读取整个 em> 字符串返回结果。即使第一个字符已经不同。事实上:

In [49]: a = "b" + "a" * 15 * 10**6
    ...: b = "c" + "a" * 15 * 10**6
    ...: 

In [50]: %timeit binary_check(a, b)
100 loops, best of 3: 10.3 ms per loop

In [51]: %timeit check_genexp(a, b)
1000000 loops, best of 3: 1.3 µs per loop

这是意料之中的。然而,这个解决方案只需要很少的时间就可以变得比显式循环更高效:

In [59]: a = "a" * 2 * 10**5 + "b" + "a" * 15*10**6
    ...: b = "a" * 2 * 10**5 + "c" + "a" * 15*10**6

In [60]: %timeit check_genexp(a, b)
10 loops, best of 3: 20.3 ms per loop

In [61]: %timeit binary_check(a, b)
100 loops, best of 3: 17.3 ms per loop

根据这个简单的基准,如果字符串的差异大于总长度的大约1.3%,则二进制检查更好。

也可以引入一些启发式方法。例如,如果两个字符串的最小长度大于某个截断值,您首先只检查该截断处的前缀是否不同,如果是,则不能忽略之后的所有内容,从而避免比较整个字符串。这可以很容易地实现:

def binary_check2(a, b, cutoff=1000):
    len_a, len_b = len(a), len(b)
    if min(len_a, len_b) > cutoff:
        small_a, small_b = a[:cutoff], b[:cutoff]
        if small_a != small_b:
            return binary_check_helper(a[:cutoff], b[:cutoff])
    # same as before

根据应用程序,您可以选择最小化平均时间的截止值。 在任何情况下,这是一个临时的启发式方法,可能会或可能不会很好地工作,所以如果你正在处理只有短公共前缀的 非常长 字符串,你应该使用“快速失败”算法genexp 方法。


在 python3.4 上执行的计时。使用字节而不是 unicode 字符串不会显着改变结果

【讨论】:

  • +1 -- 聪明!这个效果很好的事实让我很开心。我想知道在这个策略变慢之前字符串可以得到多长时间。
  • @senderle:很多。我添加了一些更多的“分析”运行,您可以看到将其速度提高到 8 倍以下是相当困难的。
  • 太棒了!虽然没有 gen-expression 那样的 Python 风格,而且 OP 并没有特别要求性能,但我喜欢这种方法,因为它向我展示了一种更好的编码许多东西的方法。好吧,这并不是什么新鲜事,我已经知道了!
【解决方案3】:

如果你想要更复杂的东西,你可以看看SequenceMatcher

它有点毛茸茸,但非常强大。如果你只是想回答你的问题,那么:

from  difflib import SequenceMatcher

s1 = 'stop at the bus'
s2 = 'stop at the car'

s = SequenceMatcher(None, s1, s2)

print s.get_matching_blocks()[0].size

返回解决方案:)

但是如果你想要所有的匹配项:

小例子:

from  difflib import SequenceMatcher

s1 = 'stop at the bus'
s2 = 'stop at the car'

s = SequenceMatcher(None, s1, s2)

print s.get_matching_blocks()

返回

[Match(a=0, b=0, size=12), Match(a=15, b=15, size=0)]

这意味着您的字符串中最长的匹配大小为 12,并且从开头 (0) 开始。但是还有另一场比赛,从 s1[15] 开始,大小为 0 。 . .

对于像您这样的大字符串,这可能会非常有趣。 :)

【讨论】:

    【解决方案4】:
    for i, (x, y) in enumerate(zip(a, b)):
        if x != y:
            print('First index where strings are different:', i)
            break
    else:
        print('Strings are identical.')
    

    在 Python 2.x 中,zip() 返回元组列表,而不是迭代器。正如 gnibbler 指出的那样,如果您使用的是 Python 2.x,那么使用 izip 而不是 zip 可能是值得的(izip 返回一个很好的、内存高效的迭代器,它可以避免评估一次)。正如我在评论中所说,在 Python 3 中,izip 已重命名为 zip,而旧的 zip 已不复存在。

    【讨论】:

    • 我认为你需要在x,y 周围加上一个括号,因为枚举会产生(idx, (aval,bval)) 形式的元组
    • 可能值得使用izip,因为字符串很长,除非您希望差异总是接近结尾
    • @gnibbler:你是对的。我最初在 Python 3 中对此进行了测试,其中 zip==izip.
    【解决方案5】:
    >>> s1 = 'stop at the bus'
    >>> s2 = 'stop at the car'
    >>> import difflib
    >>> next(difflib.SequenceMatcher(a=s1, b=s2).get_matching_blocks())
    Match(a=0, b=0, size=12)
    

    这意味着第一个匹配块的长度为 12 个字符。

    如果 a 或 b 不为 0,则字符串从一开始就不同

    【讨论】:

    • 确实如此。这个问题让我发现了一个非常有趣的库。
    • @jlengrand,是的,它非常强大,但对于这个特定问题来说不是很干净。
    • @gnibbler -- 我没有经常使用difflib,所以很高兴看到这个例子。谢谢。 (和+1——我认为它相当干净——当然不会比next( ... enumerate(izip(s1,s2)) ...)差多少)
    • @gnibbler 有趣的是,得分更高的人获得了 +1,即使之前发布的另一个人使用相同的解决方案:D。为什么说它不干净?最好使用标准库而不是可能有问题的自定义代码,不是吗?
    • @jlengrand,因为在这种情况下,您不能只检查大小来获得结果。你必须做类似match.size if match.a==match.b==0 else 0
    【解决方案6】:

    这可能有点矫枉过正,但由于您似乎关心速度,您可以考虑使用 numpy.可能需要改进(由于某种原因,内联对我来说产生了 25 我们的差异),但这是第一步:

    >>> def diff_index(s1, s2):
    ...     s1 = numpy.fromstring(s1, dtype=numpy.uint8)
    ...     s2 = numpy.fromstring(s2, dtype=numpy.uint8)
    ...     return (~(s1 == s2)).nonzero()[0][0]
    ... 
    >>> base = string.lowercase * 385
    >>> s1 = base + 'a'
    >>> s2 = base + 'z'
    >>> diff_index(s1, s2)
    10010
    

    对于最后的差异,这比基因快得多:

    >>> %timeit next(idx for idx,(c1,c2) in enumerate(izip(s1,s2)) if c1 != c2)
    1000 loops, best of 3: 1.46 ms per loop
    >>> %timeit diff_index(s1, s2)
    10000 loops, best of 3: 87.6 us per loop
    

    一开始的差异会慢很多...

    >>> s1 = 'a' + base
    >>> s2 = 'z' + base
    >>> %timeit next(idx for idx,(c1,c2) in enumerate(izip(s1,s2)) if c1 != c2)
    100000 loops, best of 3: 2.12 us per loop
    >>> %timeit diff_index(s1, s2)
    10000 loops, best of 3: 87.5 us per loop
    

    但平均而言,它会胜出一个数量级:

    >>> s1 = base[:5000] + 'a' + base[5000:]
    >>> s2 = base[:5000] + 'z' + base[5000:]
    >>> %timeit next(idx for idx,(c1,c2) in enumerate(izip(s1,s2)) if c1 != c2)
    1000 loops, best of 3: 724 us per loop
    >>> %timeit diff_index(s1, s2)
    10000 loops, best of 3: 87.2 us per loop
    

    如果速度不是问题,那么我个人会选择mgilson 的答案。

    【讨论】:

    • 不错!即使此解决方案需要第三方库。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-26
    • 1970-01-01
    • 2021-10-27
    • 2016-08-19
    • 1970-01-01
    • 2020-08-17
    相关资源
    最近更新 更多