【问题标题】:Version number comparison in PythonPython中的版本号比较
【发布时间】:2010-12-15 09:39:06
【问题描述】:

我想编写一个类似cmp 的函数,它比较两个版本号并根据它们的比较值返回-101

  • 如果版本 A 比版本 B 旧,则返回 -1
  • 如果版本 A 和 B 相同,则返回 0
  • 如果版本 A 比版本 B 新,则返回 1

每个小节都应该被解释为一个数字,因此 1.10 > 1.1。

所需的函数输出是

mycmp('1.0', '1') == 0
mycmp('1.0.0', '1') == 0
mycmp('1', '1.0.0.1') == -1
mycmp('12.10', '11.0.0.0.0') == 1
...

这是我的实现,有待改进:

def mycmp(version1, version2):
    parts1 = [int(x) for x in version1.split('.')]
    parts2 = [int(x) for x in version2.split('.')]

    # fill up the shorter version with zeros ...
    lendiff = len(parts1) - len(parts2)
    if lendiff > 0:
        parts2.extend([0] * lendiff)
    elif lendiff < 0:
        parts1.extend([0] * (-lendiff))

    for i, p in enumerate(parts1):
        ret = cmp(p, parts2[i])
        if ret: return ret
    return 0

我正在使用 Python 2.4.5 顺便说一句。 (安装在我的工作地点...)。

这是一个您可以使用的小型“测试套件”

assert mycmp('1', '2') == -1
assert mycmp('2', '1') == 1
assert mycmp('1', '1') == 0
assert mycmp('1.0', '1') == 0
assert mycmp('1', '1.000') == 0
assert mycmp('12.01', '12.1') == 0
assert mycmp('13.0.1', '13.00.02') == -1
assert mycmp('1.1.1.1', '1.1.1.1') == 0
assert mycmp('1.1.1.2', '1.1.1.1') == 1
assert mycmp('1.1.3', '1.1.3.000') == 0
assert mycmp('3.1.1.0', '3.1.2.10') == -1
assert mycmp('1.1', '1.10') == -1

【问题讨论】:

  • 不是一个答案,而是一个建议——可能值得实现 Debian 的版本号比较算法(基本上,非数字和数字部分的交替排序)。该算法描述为here(从“字符串从左到右比较”开始)。
  • 布拉格。 cmets 中支持的降价子集永远不会让我感到困惑。该链接仍然有效,即使它看起来很愚蠢。
  • 如果未来的读者需要这个来解析用户代理版本,我推荐dedicated library,因为它的历史变化太宽了。

标签: python string-comparison


【解决方案1】:

使用 Python 的 distutils.version.StrictVersion 怎么样?

>>> from distutils.version import StrictVersion
>>> StrictVersion('10.4.10') > StrictVersion('10.4.9')
True

所以对于你的cmp 函数:

>>> cmp = lambda x, y: StrictVersion(x).__cmp__(y)
>>> cmp("10.4.10", "10.4.11")
-1

如果您想比较更复杂的版本号distutils.version.LooseVersion 会更有用,但请务必只比较相同的类型。

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion('1.4c3') > LooseVersion('1.3')
True
>>> LooseVersion('1.4c3') > StrictVersion('1.3')  # different types
False

LooseVersion 不是最聪明的工具,很容易被骗:

>>> LooseVersion('1.4') > LooseVersion('1.4-rc1')
False

要在这个品种上取得成功,您需要跳出标准库并使用setuptools 的解析实用程序parse_version

>>> from pkg_resources import parse_version
>>> parse_version('1.4') > parse_version('1.4-rc2')
True

因此,根据您的具体用例,您需要确定内置的 distutils 工具是否足够,或者是否有必要将其添加为依赖项 setuptools

【讨论】:

  • 似乎只使用已经存在的东西最有意义:)
  • 不错!您是否通过阅读源代码弄清楚了这一点?我在任何地方都找不到 distutils.version 的文档:-/
  • 任何时候找不到文档,尝试导入包并使用help()。
  • 请注意,StrictVersion ONLY 最多可用于三个数字版本。对于 0.4.3.6! 之类的东西它失败了!
  • 此答案中distribute 的每个实例都应替换为setuptools,它与pkg_resources 包捆绑在一起,并且从那时起......就像,ever .同样,这是与setuptools 捆绑在一起的pkg_resources.parse_version() 函数的official documentation
【解决方案2】:

删除字符串中不感兴趣的部分(尾随零和点),然后比较数字列表。

import re

def mycmp(version1, version2):
    def normalize(v):
        return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
    return cmp(normalize(version1), normalize(version2))

这与 Pär Wieslander 的方法相同,但更紧凑:

这里有一些测试,感谢“How to compare two strings in dot separated version format in Bash?”:

assert mycmp("1", "1") == 0
assert mycmp("2.1", "2.2") < 0
assert mycmp("3.0.4.10", "3.0.4.2") > 0
assert mycmp("4.08", "4.08.01") < 0
assert mycmp("3.2.1.9.8144", "3.2") > 0
assert mycmp("3.2", "3.2.1.9.8144") < 0
assert mycmp("1.2", "2.1") < 0
assert mycmp("2.1", "1.2") > 0
assert mycmp("5.6.7", "5.6.7") == 0
assert mycmp("1.01.1", "1.1.1") == 0
assert mycmp("1.1.1", "1.01.1") == 0
assert mycmp("1", "1.0") == 0
assert mycmp("1.0", "1") == 0
assert mycmp("1.0", "1.0.1") < 0
assert mycmp("1.0.1", "1.0") > 0
assert mycmp("1.0.2.0", "1.0.2") == 0

【讨论】:

  • 恐怕不行,rstrip(".0")会在“1.0.10”中将“.10”改为“.1”。
  • 抱歉,你的函数:mycmp('1.1', '1.10') == 0
  • 使用正则表达式,上述问题得到解决。
  • 现在您已经将其他人的所有好想法合并到您的解决方案中...... :-P 仍然,这几乎就是我要做的事情。我会接受这个答案。谢谢大家
  • 注意 cmp() 已在 Python 3 中删除:docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
【解决方案3】:

在这种情况下,重用是否被认为是优雅的? :)

# pkg_resources is in setuptools
# See http://peak.telecommunity.com/DevCenter/PkgResources#parsing-utilities
def mycmp(a, b):
    from pkg_resources import parse_version as V
    return cmp(V(a),V(b))

【讨论】:

  • 嗯,当你提到 outside the standard library 的东西而不解释从哪里得到它时,它就不那么优雅了。我提交了包含 URL 的编辑。就我个人而言,我更喜欢使用 distutils - 似乎不值得为如此简单的任务引入 3rd 方软件。
  • @adam-spiers wut? 你读过评论吗? pkg_resources 是一个 setuptools 捆绑包。由于setuptools 在所有Python 安装中都是强制的,所以pkg_resources 在任何地方都有效。也就是说,distutils.version 子包也很有用——尽管比高级pkg_resources.parse_version() 函数智能得多。你应该利用哪一个取决于你对版本字符串的期望程度。
  • @CecilCurry 是的,当然我阅读了评论(ary),这就是为什么我对其进行编辑以使其更好,然后说我有。大概你并不反对我关于setuptools 在标准库之外的说法,而是我对distutils 在这种情况下的偏好。那么你所说的“有效强制”究竟是什么意思,请你提供证据证明 4.5 年前我写这篇评论时它是“有效强制”的吗?
【解决方案4】:

无需遍历版本元组。列表和元组上的内置比较运算符已经完全符合您的要求。您只需要将版本列表零扩展至相应的长度。使用 python 2.6,您可以使用 izip_longest 填充序列。

from itertools import izip_longest
def version_cmp(v1, v2):
    parts1, parts2 = [map(int, v.split('.')) for v in [v1, v2]]
    parts1, parts2 = zip(*izip_longest(parts1, parts2, fillvalue=0))
    return cmp(parts1, parts2)

对于较低版本,需要一些地图黑客。

def version_cmp(v1, v2):
    parts1, parts2 = [map(int, v.split('.')) for v in [v1, v2]]
    parts1, parts2 = zip(*map(lambda p1,p2: (p1 or 0, p2 or 0), parts1, parts2))
    return cmp(parts1, parts2)

【讨论】:

  • 很酷,但对于像散文一样无法阅读代码的人来说很难理解。 :) 好吧,我假设您只能以牺牲可读性为代价来缩短解决方案...
【解决方案5】:

这比您的建议要紧凑一些。我不是用零填充较短的版本,而是在拆分后从版本列表中删除尾随零。

def normalize_version(v):
    parts = [int(x) for x in v.split(".")]
    while parts[-1] == 0:
        parts.pop()
    return parts

def mycmp(v1, v2):
    return cmp(normalize_version(v1), normalize_version(v2))

【讨论】:

  • 不错,谢谢。但我仍然希望有一个或两个班轮......;)
  • +1 @jellybean:两行代码并不总是最好的维护和可读性,这个代码非常清晰紧凑,此外,您可以重复使用mycmp如果您需要,您的代码中有其他用途。
  • @RedGlyph:你说得有道理。应该说“可读的两行”。 :)
  • 嗨@Pär Wieslander,当我使用此解决方案解决 Leetcode 问题中的相同问题时,我在 while 循环中遇到错误,提示“列表索引超出范围”。你能帮忙解释一下为什么会这样吗?这是问题所在:leetcode.com/explore/interview/card/amazon/76/array-and-strings/…
【解决方案6】:

使用正则表达式删除尾随 .0.00split 并使用正确比较数组的 cmp 函数:

def mycmp(v1,v2):
 c1=map(int,re.sub('(\.0+)+\Z','',v1).split('.'))
 c2=map(int,re.sub('(\.0+)+\Z','',v2).split('.'))
 return cmp(c1,c2)

当然,如果您不介意排长队,您可以将其转换为单行。

【讨论】:

    【解决方案7】:
    def compare_version(v1, v2):
        return cmp(*tuple(zip(*map(lambda x, y: (x or 0, y or 0), 
               [int(x) for x in v1.split('.')], [int(y) for y in v2.split('.')]))))
    

    这是一个单一的衬里(为了便于阅读而分开)。不确定可读性...

    【讨论】:

    • 是的!并且进一步缩小(顺便说一句,不需要tuple):cmp(*zip(*map(lambda x,y:(x or 0,y or 0), map(int,v1.split('.')), map(int,v2.split('.')) )))
    【解决方案8】:
    from distutils.version import StrictVersion
    def version_compare(v1, v2, op=None):
        _map = {
            '<': [-1],
            'lt': [-1],
            '<=': [-1, 0],
            'le': [-1, 0],
            '>': [1],
            'gt': [1],
            '>=': [1, 0],
            'ge': [1, 0],
            '==': [0],
            'eq': [0],
            '!=': [-1, 1],
            'ne': [-1, 1],
            '<>': [-1, 1]
        }
        v1 = StrictVersion(v1)
        v2 = StrictVersion(v2)
        result = cmp(v1, v2)
        if op:
            assert op in _map.keys()
            return result in _map[op]
        return result
    

    实现 php version_compare,除了“=”。因为它是模棱两可的。

    【讨论】:

      【解决方案9】:

      列表在 Python 中是可比较的,因此如果有人将表示数字的字符串转换为整数,则可以成功使用基本的 Python 比较。

      我需要稍微扩展这种方法,因为我使用 Python3x,其中 cmp 函数不再存在。我不得不模仿cmp(a,b)(a &gt; b) - (a &lt; b)。而且,版本号一点也不干净,可以包含各种其他字母数字字符。在某些情况下,函数无法判断顺序,因此它返回 False(参见第一个示例)。

      因此,即使问题已经过时并且已经回答,我也会发布此问题,因为它可能会节省一些人的生命。

      import re
      
      def _preprocess(v, separator, ignorecase):
          if ignorecase: v = v.lower()
          return [int(x) if x.isdigit() else [int(y) if y.isdigit() else y for y in re.findall("\d+|[a-zA-Z]+", x)] for x in v.split(separator)]
      
      def compare(a, b, separator = '.', ignorecase = True):
          a = _preprocess(a, separator, ignorecase)
          b = _preprocess(b, separator, ignorecase)
          try:
              return (a > b) - (a < b)
          except:
              return False
      
      print(compare('1.0', 'beta13'))    
      print(compare('1.1.2', '1.1.2'))
      print(compare('1.2.2', '1.1.2'))
      print(compare('1.1.beta1', '1.1.beta2'))
      

      【讨论】:

        【解决方案10】:

        如果您不想在此处引入外部依赖项,这是我为 Python 3.x 编写的尝试。

        rcrel(可能还可以添加c)被视为“候选版本”,并将版本号分为两部分,如果缺少第二部分的值很高(999)。其他字母会产生拆分,并通过 base-36 代码作为子编号处理。

        import re
        from itertools import chain
        def compare_version(version1,version2):
            '''compares two version numbers
            >>> compare_version('1', '2') < 0
            True
            >>> compare_version('2', '1') > 0
            True
            >>> compare_version('1', '1') == 0
            True
            >>> compare_version('1.0', '1') == 0
            True
            >>> compare_version('1', '1.000') == 0
            True
            >>> compare_version('12.01', '12.1') == 0
            True
            >>> compare_version('13.0.1', '13.00.02') <0
            True
            >>> compare_version('1.1.1.1', '1.1.1.1') == 0
            True
            >>> compare_version('1.1.1.2', '1.1.1.1') >0
            True
            >>> compare_version('1.1.3', '1.1.3.000') == 0
            True
            >>> compare_version('3.1.1.0', '3.1.2.10') <0
            True
            >>> compare_version('1.1', '1.10') <0
            True
            >>> compare_version('1.1.2','1.1.2') == 0
            True
            >>> compare_version('1.1.2','1.1.1') > 0
            True
            >>> compare_version('1.2','1.1.1') > 0
            True
            >>> compare_version('1.1.1-rc2','1.1.1-rc1') > 0
            True
            >>> compare_version('1.1.1a-rc2','1.1.1a-rc1') > 0
            True
            >>> compare_version('1.1.10-rc1','1.1.1a-rc2') > 0
            True
            >>> compare_version('1.1.1a-rc2','1.1.2-rc1') < 0
            True
            >>> compare_version('1.11','1.10.9') > 0
            True
            >>> compare_version('1.4','1.4-rc1') > 0
            True
            >>> compare_version('1.4c3','1.3') > 0
            True
            >>> compare_version('2.8.7rel.2','2.8.7rel.1') > 0
            True
            >>> compare_version('2.8.7.1rel.2','2.8.7rel.1') > 0
            True
        
            '''
            chn = lambda x:chain.from_iterable(x)
            def split_chrs(strings,chars):
                for ch in chars:
                    strings = chn( [e.split(ch) for e in strings] )
                return strings
            split_digit_char=lambda x:[s for s in re.split(r'([a-zA-Z]+)',x) if len(s)>0]
            splt = lambda x:[split_digit_char(y) for y in split_chrs([x],'.-_')]
            def pad(c1,c2,f='0'):
                while len(c1) > len(c2): c2+=[f]
                while len(c2) > len(c1): c1+=[f]
            def base_code(ints,base):
                res=0
                for i in ints:
                    res=base*res+i
                return res
            ABS = lambda lst: [abs(x) for x in lst]
            def cmp(v1,v2):
                c1 = splt(v1)
                c2 = splt(v2)
                pad(c1,c2,['0'])
                for i in range(len(c1)): pad(c1[i],c2[i])
                cc1 = [int(c,36) for c in chn(c1)]
                cc2 = [int(c,36) for c in chn(c2)]
                maxint = max(ABS(cc1+cc2))+1
                return base_code(cc1,maxint) - base_code(cc2,maxint)
            v_main_1, v_sub_1 = version1,'999'
            v_main_2, v_sub_2 = version2,'999'
            try:
                v_main_1, v_sub_1 = tuple(re.split('rel|rc',version1))
            except:
                pass
            try:
                v_main_2, v_sub_2 = tuple(re.split('rel|rc',version2))
            except:
                pass
            cmp_res=[cmp(v_main_1,v_main_2),cmp(v_sub_1,v_sub_2)]
            res = base_code(cmp_res,max(ABS(cmp_res))+1)
            return res
        
        
        import random
        from functools import cmp_to_key
        random.shuffle(versions)
        versions.sort(key=cmp_to_key(compare_version))
        

        【讨论】:

          【解决方案11】:

          最难阅读的解决方案,但仍然是单行的!并使用迭代器更快。

          next((c for c in imap(lambda x,y:cmp(int(x or 0),int(y or 0)),
                      v1.split('.'),v2.split('.')) if c), 0)
          

          这是针对 Python2.6 和 3.+ btw,Python 2.5 和更早版本需要捕获 StopIteration。

          【讨论】:

            【解决方案12】:

            我这样做是为了能够解析和比较 Debian 软件包版本字符串。请注意,它对字符验证并不严格。

            这也可能有帮助:

            #!/usr/bin/env python
            
            # Read <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version> for further informations.
            
            class CommonVersion(object):
                def __init__(self, version_string):
                    self.version_string = version_string
                    self.tags = []
                    self.parse()
            
                def parse(self):
                    parts = self.version_string.split('~')
                    self.version_string = parts[0]
                    if len(parts) > 1:
                        self.tags = parts[1:]
            
            
                def __lt__(self, other):
                    if self.version_string < other.version_string:
                        return True
                    for index, tag in enumerate(self.tags):
                        if index not in other.tags:
                            return True
                        if self.tags[index] < other.tags[index]:
                            return True
            
                @staticmethod
                def create(version_string):
                    return UpstreamVersion(version_string)
            
            class UpstreamVersion(CommonVersion):
                pass
            
            class DebianMaintainerVersion(CommonVersion):
                pass
            
            class CompoundDebianVersion(object):
                def __init__(self, epoch, upstream_version, debian_version):
                    self.epoch = epoch
                    self.upstream_version = UpstreamVersion.create(upstream_version)
                    self.debian_version = DebianMaintainerVersion.create(debian_version)
            
                @staticmethod
                def create(version_string):
                    version_string = version_string.strip()
                    epoch = 0
                    upstream_version = None
                    debian_version = '0'
            
                    epoch_check = version_string.split(':')
                    if epoch_check[0].isdigit():
                        epoch = int(epoch_check[0])
                        version_string = ':'.join(epoch_check[1:])
                    debian_version_check = version_string.split('-')
                    if len(debian_version_check) > 1:
                        debian_version = debian_version_check[-1]
                        version_string = '-'.join(debian_version_check[0:-1])
            
                    upstream_version = version_string
            
                    return CompoundDebianVersion(epoch, upstream_version, debian_version)
            
                def __repr__(self):
                    return '{} {}'.format(self.__class__.__name__, vars(self))
            
                def __lt__(self, other):
                    if self.epoch < other.epoch:
                        return True
                    if self.upstream_version < other.upstream_version:
                        return True
                    if self.debian_version < other.debian_version:
                        return True
                    return False
            
            
            if __name__ == '__main__':
                def lt(a, b):
                    assert(CompoundDebianVersion.create(a) < CompoundDebianVersion.create(b))
            
                # test epoch
                lt('1:44.5.6', '2:44.5.6')
                lt('1:44.5.6', '1:44.5.7')
                lt('1:44.5.6', '1:44.5.7')
                lt('1:44.5.6', '2:44.5.6')
                lt('  44.5.6', '1:44.5.6')
            
                # test upstream version (plus tags)
                lt('1.2.3~rc7',          '1.2.3')
                lt('1.2.3~rc1',          '1.2.3~rc2')
                lt('1.2.3~rc1~nightly1', '1.2.3~rc1')
                lt('1.2.3~rc1~nightly2', '1.2.3~rc1')
                lt('1.2.3~rc1~nightly1', '1.2.3~rc1~nightly2')
                lt('1.2.3~rc1~nightly1', '1.2.3~rc2~nightly1')
            
                # test debian maintainer version
                lt('44.5.6-lts1', '44.5.6-lts12')
                lt('44.5.6-lts1', '44.5.7-lts1')
                lt('44.5.6-lts1', '44.5.7-lts2')
                lt('44.5.6-lts1', '44.5.6-lts2')
                lt('44.5.6-lts1', '44.5.6-lts2')
                lt('44.5.6',      '44.5.6-lts1')
            

            【讨论】:

              【解决方案13】:

              另一种解决方案:

              def mycmp(v1, v2):
                  import itertools as it
                  f = lambda v: list(it.dropwhile(lambda x: x == 0, map(int, v.split('.'))[::-1]))[::-1]
                  return cmp(f(v1), f(v2))
              

              也可以这样使用:

              import itertools as it
              f = lambda v: list(it.dropwhile(lambda x: x == 0, map(int, v.split('.'))[::-1]))[::-1]
              f(v1) <  f(v2)
              f(v1) == f(v2)
              f(v1) >  f(v2)
              

              【讨论】:

                【解决方案14】:

                我在我的项目中使用这个:

                cmp(v1.split("."), v2.split(".")) >= 0
                

                【讨论】:

                  【解决方案15】:

                  几年后,但这个问题仍然是最重要的。

                  这是我的版本排序功能。它将版本拆分为数字和非数字部分。数字比较为int,其余为str(作为列表项的一部分)。

                  def sort_version_2(data):
                      def key(n):
                          a = re.split(r'(\d+)', n)
                          a[1::2] = map(int, a[1::2])
                          return a
                      return sorted(data, key=lambda n: key(n))
                  

                  您可以将函数 key 用作带有比较运算符的自定义 Version 类型。如果真的想使用cmp,你可以像这个例子一样:https://stackoverflow.com/a/22490617/9935708

                  def Version(s):
                      s = re.sub(r'(\.0*)*$', '', s)  # to avoid ".0" at end
                      a = re.split(r'(\d+)', s)
                      a[1::2] = map(int, a[1::2])
                      return a
                  
                  def mycmp(a, b):
                      a, b = Version(a), Version(b)
                      return (a > b) - (a < b)  # DSM's answer
                  
                  

                  测试套件通过。

                  【讨论】:

                    【解决方案16】:

                    我的首选解决方案:

                    用额外的零填充字符串并仅使用前四个很容易理解, 不需要任何正则表达式,并且 lambda 或多或少具有可读性。为了便于阅读,我使用了两行代码,对我来说,优雅是简短而简单的。

                    def mycmp(version1,version2):
                      tup = lambda x: [int(y) for y in (x+'.0.0.0.0').split('.')][:4]
                      return cmp(tup(version1),tup(version2))
                    

                    【讨论】:

                      【解决方案17】:

                      这是我的解决方案(用 C 语言编写,抱歉)。希望对你有用

                      int compare_versions(const char *s1, const char *s2) {
                          while(*s1 && *s2) {
                              if(isdigit(*s1) && isdigit(*s2)) {
                                  /* compare as two decimal integers */
                                  int s1_i = strtol(s1, &s1, 10);
                                  int s2_i = strtol(s2, &s2, 10);
                      
                                  if(s1_i != s2_i) return s1_i - s2_i;
                              } else {
                                  /* compare as two strings */
                                  while(*s1 && !isdigit(*s1) && *s2 == *s1) {
                                      s1++;
                                      s2++;
                                  }
                      
                                  int s1_i = isdigit(*s1) ? 0 : *s1;
                                  int s2_i = isdigit(*s2) ? 0 : *s2;
                      
                                  if(s1_i != s2_i) return s1_i - s2_i;
                              }
                          }
                      
                          return 0;
                      }
                      

                      【讨论】:

                        猜你喜欢
                        • 2023-03-20
                        • 1970-01-01
                        • 1970-01-01
                        • 2022-10-24
                        • 1970-01-01
                        相关资源
                        最近更新 更多