版本号比较

Python必须有一个更优雅的解决scheme…也许你可以帮助你:

我想写一个类似cmp的函数,返回-1,0或1的版本号,允许任意数量的小节。

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

期望的function输出是

 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顺便说一句。 (安装在我的工作地点…)。

这里有一个你可以使用的小testing套件

 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 

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

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

一些testing,感谢这个职位 :

 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 

如何使用Python的distutils.version.StrictVersion

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

所以对于你的cmpfunction:

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

如果你想比较更复杂的版本号distutils.version.LooseVersion会更有用,但是一定要比较相同的types。

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

要获得这个品种的成功,你需要走出标准库,并使用分发的parsing工具parse_version

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

因此,根据您的具体使用情况,您需要确定内置distutils工具是否足够,或者是否有必要添加作为依赖项distribute

在这种情况下重用被认为是优雅吗? 🙂

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

无需迭代版本元组。 列表和元组中的内置比较运算符已经完全按照您的要求工作。 你只需要将版本列表扩展到相应的长度即可。 在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) 

较低的版本,一些地图hackery是必需的。

 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) 

这比你的build议更简洁一点。 我不是用零填充短版本,而是在分割后从版本列表中删除尾部零。

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

用正则expression式去除尾部.0和.00,拆分并使用正确比较数组的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) 

当然如果你不介意排长队的话,你可以把它转换成单线

在Python中,列表是可比的,所以如果将表示数字的string转换为整数,基本的python比较可以成功使用。

我需要扩展一下这个方法,首先导致我使用python3x,其中cmp函数不存在任何更多我不得不模仿cmp(a,b)(a> b) – (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')) 

如果你不想拉外部依赖这是我的一个尝试(为python 3.x写)。 “rc”,“rel”(可能加一个“c”)被认为是“释放候选者”,将版本号分为两部分,如果缺less第二部分的值高(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)) 
 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 ,除了“=”。 因为它是不明确的。

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

这是一个单线(拆分为legability)。 不清楚可读性

最难读的解决scheme,但一个单一的class轮! 并使用迭代器来快速。

 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. +顺便说一句,Python 2.5和更旧的需要赶上StopIteration。

另一个scheme

 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) 

这是为了能够parsing和比较debian包版本string。 请注意,字符validation并不严格。

这也可能有帮助。

 #!/usr/bin/env python # Read <https://www.debian.org/doc/debian-policy/ch-controlfields.html#sf-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') 

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

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

我的首选解决scheme

用额外的零填充string,并首先使用这四个字符,这很容易理解,不需要任何正则expression式,而且lambda或多或less是可读的。 为了便于阅读,我使用了两行代码,对于我来说,优雅是简单而短暂的。

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

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