代码之家  ›  专栏  ›  技术社区  ›  Johannes Charra

python中的版本号比较

  •  82
  • Johannes Charra  · 技术社区  · 15 年前

    我想写一篇 cmp -类似于比较两个版本号并返回 -1 , 0 1 基于它们的比较价值。

    • 返回 - 1 如果版本A比版本B旧
    • 返回 如果A版和B版是等效的
    • 返回 如果版本A比版本B更新

    每个小节应解释为一个数字,因此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.5btw(安装在我的工作场所…)。

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

    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
    
    16 回复  |  直到 5 年前
        1
  •  32
  •   Community Reversed Engineer    7 年前

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

    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的方法相同,但更紧凑一些。

    一些测试,多亏了 this post :

    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
    
        2
  •  253
  •   Sam Hartsfield    5 年前

    使用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 .

        3
  •  29
  •   Adam Spiers    12 年前

    重新使用 在这种情况下被认为是优雅吗?:)

    # 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))
    
        4
  •  12
  •   Ants Aasma    15 年前

    不需要迭代版本元组。列表和元组上的内置比较运算符的工作方式与您想要的完全相同。您只需要将版本列表零扩展到相应的长度。使用python 2.6,可以使用izip_longst填充序列。

    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
  •  10
  •   Pär Wieslander    15 年前

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

    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))
    
        6
  •  6
  •   yu_sha    15 年前

    用regex删除trailing.0和.00,split并使用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
  •  2
  •   mavnn    15 年前
    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('.')]))))
    

    这是一条单行线(为了腿部的伸展性而分开)。不确定可读性…

        8
  •  2
  •   sanyi    12 年前

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

    但是我需要扩展一点这种方法,首先因为我使用了python3x,其中 化学机械抛光 函数不再存在,我必须模仿它 化学机械抛光(A,B) 具有 (A>B)-(A<B) .

    第二,遗憾的是,版本号一点也不干净,可以包含所有类型的其他字母数字字符。 有些情况下,函数不能告诉顺序,所以返回为假(参见第一个示例)。

    所以,即使这个问题已经很老了,而且已经得到了解答,也可以在生活中节省几分钟。

    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'))
    
        9
  •  2
  •   Roland Puntaier    12 年前

    如果您不想引入外部依赖项,这里有一个我的尝试(为python 3.x编写)。”rc,“rel”(可能还有一个可以添加“c”)被视为“发布候选”,将版本号分为两部分,如果缺少第二部分的值则为高(999)。否则,字母将产生一个拆分,并通过基36代码作为子编号进行处理。

    
        import re
        from itertools import chain
        def compare_version(version1,version2):
            '''compares two version numbers
            >>> compare_version('1', '2') >> 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') >> 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') >> compare_version('1.1', '1.10') >> 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') >> 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))
    
        10
  •  2
  •   Ryan Fau    9 年前
    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 除了“=”。因为它是模棱两可的。

        11
  •  1
  •   Paul    15 年前

    最难读懂的解决方案,不过还是一句话!并且使用迭代器来加快速度。

    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,python2.5及更旧版本需要捕获stopIteration。

        12
  •  0
  •   pedrormjunior    8 年前

    另一个解决方案:

    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)
    
        13
  •  0
  •   Pius Raeder    8 年前

    这样做是为了能够分析和比较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')
    
        14
  •  0
  •   Keyrr Perino tefozi    7 年前

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

    cmp(v1.split("."), v2.split(".")) >= 0
    
        15
  •  -1
  •   daramarak    15 年前

    我的首选解决方案:

    用额外的零填充字符串,只使用前四个很容易理解, 不需要任何regex,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))
    
        16
  •  -1
  •   e_asphyx    13 年前

    这是我的解决方案(用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;
    }