Python中的版本号比较

100

我想编写一个类似于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的版本号比较算法(基本上是非数字和数字部分的交替排序)。该算法在此处描述(从“字符串从左到右进行比较”开始)。 - hobbs
在注释中支持的Markdown子集总是让我感到困惑。即使看起来很愚蠢,链接仍然有效。 - hobbs
如果未来的读者需要用于用户代理版本解析,我建议使用专用库,因为历史变化太广泛了。 - James Broadhead
@hobbs。这基本上就是natsort的功能。 - Mad Physicist
1
尽管这里的问题比较老,但是似乎这个问题已经被指定为规范问题,因为许多问题都被关闭为该问题的副本。 - John Y
17个回答

293

使用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作为依赖项添加。


2
看起来最合理的做法是利用已经存在的东西 :) - Patrick Wolf
2
我认为没有文档。是的,我曾经在考虑编写自己的打包解决方案时阅读过源代码,但后来我发现了distutils2。 - bradley.ayers
3
当你找不到文档时,尝试导入包并使用help()函数。 - rspeed
14
请注意,StrictVersion 只能处理三个数字版本号,对于像 0.4.3.6 这样的版本号会失败! - abergmeier
6
本答案中的所有 distribute 都应更改为 setuptools,它随附在 pkg_resources 包中,自那时以来一直存在。同样,这是关于 setuptools 捆绑的 pkg_resources.parse_version() 函数的官方文档 - Cecil Curry
显示剩余4条评论

41

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

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相同的方法,但更加简洁:

以下是一些测试,感谢"如何在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

2
很抱歉,这样做不起作用,rstrip(".0")会将"1.0.10"中的".10"变成".1"。 - RedGlyph
抱歉,但是根据您的函数: mycmp('1.1', '1.10') == 0 - Johannes Charra
在某些 Firefox 版本中会失败,可在此处找到。例如 3.0a2。 - James Broadhead
是的,这个问题不涉及包含字母的版本号。 - gnud
2
请注意,cmp()在Python 3中已被删除:https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons - Dominic Cleal
显示剩余3条评论

31

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

# 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))

7
当您提到一些标准库外的内容时,如果没有解释如何获取它,那么它可能不太优雅。我提交了一个编辑来包含这个网址。就我个人而言,我更喜欢使用distutils——引入第三方软件似乎对于一个如此简单的任务来说不值得花费那么多力气。 - Adam Spiers
2
@adam-spiers 干嘛? 你有读评论吗?pkg_resources是一个由setuptools捆绑的包。因为在所有Python安装中实际上都需要setuptools,所以pkg_resources实际上可以在任何地方使用。话虽如此,distutils.version子包也很有用-尽管比高级别的pkg_resources.parse_version()函数要低效得多。您应该利用哪个取决于您期望版本字符串出现多少疯狂的程度。 - Cecil Curry
@CecilCurry 当然我读了评论,这就是为什么我编辑它以使其更好,然后声明我已经这样做了。你可能不是在反对我的说法,即setuptools在标准库之外,而是反对我在这种情况下表达对distutils的偏好。那么你所说的“有效强制执行”到底是什么意思?请提供证据证明在我写下这条评论4.5年前,“有效强制执行”是什么。 - Adam Spiers

12
不需要遍历版本元组。列表和元组上的内置比较运算符已经按你想要的方式正常工作。您只需要将版本列表零扩展到相应的长度即可。在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)

很酷,但对于那些不能像阅读散文一样阅读代码的人来说很难理解。 :) 嗯,我想你只能以可读性为代价缩短解决方案... - Johannes Charra

10

这个方法比你的建议更紧凑。我不是用零填充较短的版本,而是在拆分后从版本列表中删除尾随零。

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))

不错,谢谢。但我仍然希望有一行或两行的代码... ;) - Johannes Charra
4
+1 @jellybean:两行代码并不总是最易维护和阅读的,但这段代码清晰紧凑,而且你如果需要的话可以在代码中重复使用 mycmp - RedGlyph
@RedGlyph:你说得有道理。应该说“可读性强的两行代码”。 :) - Johannes Charra
嗨@Pär Wieslander,当我使用这个解决方案来解决Leetcode问题时,我在while循环中遇到一个错误,显示“列表索引超出范围”。你能帮忙解释一下为什么会出现这种情况吗?这是问题的链接:https://leetcode.com/explore/interview/card/amazon/76/array-and-strings/502/ - YouHaveaBigEgo

7

使用正则表达式删除末尾的.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)

当然,如果你不介意代码变得很长,你也可以将其转换为一行代码。

2
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,但不包括"=",因为它含糊不清。


2
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('.')]))))

这是一行代码(为了易读性而分割成多行)。不确定可读性如何...


1
是的!而且可以进一步缩小(顺便说一下,不需要使用“tuple”): cmp(*zip(*map(lambda x,y:(x or 0,y or 0), map(int,v1.split('.')), map(int,v2.split('.')) ))) - Paul

2

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

rcrel(可能还可以添加 c)被视为“发布候选版”,将版本号分成两个部分,如果缺失,则第二部分的值很高(999)。否则,字母会产生分裂,并通过基于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))

2
列表在Python中是可以比较的,因此如果有人将表示数字的字符串转换为整数,基本的Python比较就可以成功使用。我需要稍微扩展一下这种方法,因为我使用的是Python3x,其中cmp函数已经不存在了。我不得不用(a > b) - (a < b)来模拟cmp(a, 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'))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接