339

我正在浏览一个包含鸡蛋的目录,以将这些鸡蛋添加到sys.path. 如果目录中有同一个 .egg 的两个版本,我只想添加最新的一个。

我有一个正则表达式r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$来从文件名中提取名称和版本。问题是比较版本号,它是一个类似2.3.1.

由于我正在比较字符串,因此 2 排序高于 10,但这对于版本不正确。

>>> "2.3.1" > "10.1.1"
True

我可以做一些拆分、解析、转换为 int 等,我最终会得到一个解决方法。但这是 Python,而不是 Java。有没有一种比较版本字符串的优雅方法?

4

13 回答 13

536

使用packaging.version.parse.

>>> from packaging import version
>>> version.parse("2.3.1") < version.parse("10.1.2")
True
>>> version.parse("1.3.a4") < version.parse("10.1.2")
True
>>> isinstance(version.parse("1.3.a4"), version.Version)
True
>>> isinstance(version.parse("1.3.xy123"), version.LegacyVersion)
True
>>> version.Version("1.3.xy123")
Traceback (most recent call last):
...
packaging.version.InvalidVersion: Invalid version: '1.3.xy123'

packaging.version.parse是第三方实用程序,但由setuptools使用(因此您可能已经安装了它)并且符合当前PEP 440packaging.version.Version如果版本合规,它将返回一个,如果不合规,它将返回一个packaging.version.LegacyVersion。后者总是在有效版本之前排序。

注意:打包最近被卖到了 setuptools中。


您可能会遇到一种古老且现已弃用的方法distutils.version,它没有记录,仅符合被取代的PEP 386

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion("2.3.1") < LooseVersion("10.1.2")
True
>>> StrictVersion("2.3.1") < StrictVersion("10.1.2")
True
>>> StrictVersion("1.3.a4")
Traceback (most recent call last):
...
ValueError: invalid version number '1.3.a4'

如您所见,它将有效的 PEP 440 版本视为“不严格”,因此与现代 Python 的有效版本概念不符。

正如distutils.version未记录的那样,这里是相关的文档字符串。

于 2012-08-09T16:30:03.290 回答
123

打包库包含用于处理版本和其他打包相关功能的实用程序。这实现了PEP 0440 - 版本标识,并且还能够解析不遵循 PEP 的版本。pip 和其他常见的 Python 工具使用它来提供版本解析和比较。

$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')

这是从 setuptools 和 pkg_resources 中的原始代码中分离出来的,以提供更轻量级和更快的包。


在打包库存在之前,这个功能已经(并且仍然可以)在 pkg_resources 中找到,pkg_resources 是 setuptools 提供的一个包。但是,这不再是首选,因为不再保证安装 setuptools(存在其他打包工具),而且 pkg_resources 在导入时使用了相当多的资源。但是,所有文档和讨论仍然相关。

parse_version()文档

解析 PEP 440 定义的项目的版本字符串。返回值将是表示版本的对象。这些对象可以相互比较和分类。排序算法由 PEP 440 定义,此外,任何不是有效 PEP 440 版本的版本都将被视为小于任何有效 PEP 440 版本,无效版本将继续使用原始算法进行排序。

在 PEP 440 存在之前,引用的“原始算法”是在旧版本的文档中定义的。

从语义上讲,格式是 distutilsStrictVersionLooseVersion类之间的粗略交叉;如果您给它提供可以使用的版本StrictVersion,那么它们将以相同的方式进行比较。否则,比较更像是LooseVersion. 可以创建会欺骗此解析器的病态版本编码方案,但在实践中它们应该非常罕见。

文档提供了一些示例:

如果您想确定您选择的编号方案是否按照您认为的方式工作,您可以使用该pkg_resources.parse_version() 函数来比较不同的版本号:

>>> from pkg_resources import parse_version
>>> parse_version('1.9.a.dev') == parse_version('1.9a0dev')
True
>>> parse_version('2.1-rc2') < parse_version('2.1')
True
>>> parse_version('0.6a9dev-r41475') < parse_version('0.6a9')
True
于 2014-01-11T17:36:50.983 回答
67
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
于 2012-08-09T16:26:40.293 回答
24

将版本字符串转换为元组并从那里开始有什么问题?对我来说似乎足够优雅

>>> (2,3,1) < (10,1,1)
True
>>> (2,3,1) < (10,1,1,1)
True
>>> (2,3,1,10) < (10,1,1,1)
True
>>> (10,3,1,10) < (10,1,1,1)
False
>>> (10,3,1,10) < (10,4,1,1)
True

@kindall 的解决方案是代码看起来有多好的一个快速示例。

于 2012-08-09T16:26:27.943 回答
13

这样setuptools做的方式,它使用pkg_resources.parse_version功能。它应该符合PEP440

例子:

#! /usr/bin/python
# -*- coding: utf-8 -*-
"""Example comparing two PEP440 formatted versions
"""
import pkg_resources

VERSION_A = pkg_resources.parse_version("1.0.1-beta.1")
VERSION_B = pkg_resources.parse_version("v2.67-rc")
VERSION_C = pkg_resources.parse_version("2.67rc")
VERSION_D = pkg_resources.parse_version("2.67rc1")
VERSION_E = pkg_resources.parse_version("1.0.0")

print(VERSION_A)
print(VERSION_B)
print(VERSION_C)
print(VERSION_D)

print(VERSION_A==VERSION_B) #FALSE
print(VERSION_B==VERSION_C) #TRUE
print(VERSION_C==VERSION_D) #FALSE
print(VERSION_A==VERSION_E) #FALSE
于 2019-08-23T23:40:48.267 回答
10

有可用的包包,可让您根据PEP-440比较版本以及旧版本。

>>> from packaging.version import Version, LegacyVersion
>>> Version('1.1') < Version('1.2')
True
>>> Version('1.2.dev4+deadbeef') < Version('1.2')
True
>>> Version('1.2.8.5') <= Version('1.2')
False
>>> Version('1.2.8.5') <= Version('1.2.8.6')
True

旧版本支持:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

将旧版本与 PEP-440 版本进行比较。

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
于 2015-02-12T18:38:29.757 回答
7

根据 Kindall 的解决方案发布我的全部功能。通过用前导零填充每个版本部分,我能够支持与数字混合的任何字母数字字符。

虽然肯定不如他的单行函数漂亮,但它似乎与字母数字版本号配合得很好。zfill(#)(如果您的版本控制系统中有长字符串,请务必正确设置该值。)

def versiontuple(v):
   filled = []
   for point in v.split("."):
      filled.append(point.zfill(8))
   return tuple(filled)

.

>>> versiontuple("10a.4.5.23-alpha") > versiontuple("2a.4.5.23-alpha")
True


>>> "10a.4.5.23-alpha" > "2a.4.5.23-alpha"
False
于 2015-02-17T18:00:13.353 回答
7

您可以使用semver包来确定版本是否满足语义版本要求。这与比较两个实际版本不同,而是一种比较。

例如,版本 3.6.0+1234 应与 3.6.0 相同。

import semver
semver.match('3.6.0+1234', '==3.6.0')
# True

from packaging import version
version.parse('3.6.0+1234') == version.parse('3.6.0')
# False

from distutils.version import LooseVersion
LooseVersion('3.6.0+1234') == LooseVersion('3.6.0')
# False
于 2017-09-26T12:48:43.657 回答
1

我正在寻找一种不会添加任何新依赖项的解决方案。查看以下(Python 3)解决方案:

class VersionManager:

    @staticmethod
    def compare_version_tuples(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):

        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as tuples)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        tuple_a = major_a, minor_a, bugfix_a
        tuple_b = major_b, minor_b, bugfix_b
        if tuple_a > tuple_b:
            return 1
        if tuple_b > tuple_a:
            return -1
        return 0

    @staticmethod
    def compare_version_integers(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):
        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as integers)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        # --
        if major_a > major_b:
            return 1
        if major_b > major_a:
            return -1
        # --
        if minor_a > minor_b:
            return 1
        if minor_b > minor_a:
            return -1
        # --
        if bugfix_a > bugfix_b:
            return 1
        if bugfix_b > bugfix_a:
            return -1
        # --
        return 0

    @staticmethod
    def test_compare_versions():
        functions = [
            (VersionManager.compare_version_tuples, "VersionManager.compare_version_tuples"),
            (VersionManager.compare_version_integers, "VersionManager.compare_version_integers"),
        ]
        data = [
            # expected result, version a, version b
            (1, 1, 0, 0, 0, 0, 1),
            (1, 1, 5, 5, 0, 5, 5),
            (1, 1, 0, 5, 0, 0, 5),
            (1, 0, 2, 0, 0, 1, 1),
            (1, 2, 0, 0, 1, 1, 0),
            (0, 0, 0, 0, 0, 0, 0),
            (0, -1, -1, -1, -1, -1, -1),  # works even with negative version numbers :)
            (0, 2, 2, 2, 2, 2, 2),
            (-1, 5, 5, 0, 6, 5, 0),
            (-1, 5, 5, 0, 5, 9, 0),
            (-1, 5, 5, 5, 5, 5, 6),
            (-1, 2, 5, 7, 2, 5, 8),
        ]
        count = len(data)
        index = 1
        for expected_result, major_a, minor_a, bugfix_a, major_b, minor_b, bugfix_b in data:
            for function_callback, function_name in functions:
                actual_result = function_callback(
                    major_a=major_a, minor_a=minor_a, bugfix_a=bugfix_a,
                    major_b=major_b, minor_b=minor_b, bugfix_b=bugfix_b,
                )
                outcome = expected_result == actual_result
                message = "{}/{}: {}: {}: a={}.{}.{} b={}.{}.{} expected={} actual={}".format(
                    index, count,
                    "ok" if outcome is True else "fail",
                    function_name,
                    major_a, minor_a, bugfix_a,
                    major_b, minor_b, bugfix_b,
                    expected_result, actual_result
                )
                print(message)
                assert outcome is True
                index += 1
        # test passed!


if __name__ == '__main__':
    VersionManager.test_compare_versions()

编辑:添加了带有元组比较的变体。当然,带有元组比较的变体更好,但我一直在寻找带有整数比较的变体

于 2019-07-02T09:05:13.117 回答
0

...回到简单的...对于您可以使用的简单脚本:

import sys
needs = (3, 9) # or whatever
pvi = sys.version_info.major, sys.version_info.minor    

稍后在您的代码中

try:
    assert pvi >= needs
except:
    print("will fail!")
    # etc.
于 2020-08-23T22:24:16.673 回答
0

类似于标准strverscmpMark Byers 的这个解决方案,但使用 findall 而不是 split 来避免空的情况。

import re
num_split_re = re.compile(r'([0-9]+|[^0-9]+)')

def try_int(i, fallback=None):
    try:
        return int(i)
    except ValueError:
        pass
    except TypeError:
        pass
    return fallback

def ver_as_list(a):
    return [try_int(i, i) for i in num_split_re.findall(a)]

def strverscmp_lt(a, b):
    a_ls = ver_as_list(a)
    b_ls = ver_as_list(b)
    return a_ls < b_ls
于 2021-10-24T15:00:09.310 回答
0

假设您的语义版本是“干净的”(例如x.x.x)并且您有一个需要排序的版本列表,那么这将起作用。

# Here are some versions
versions = ["1.0.0", "1.10.0", "1.9.0"]

# This does not work
versions.sort() # Result: ['1.0.0', '1.10.0', '1.9.0']

# So make a list of tuple versions
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]

# And sort the string list based on the tuple list
versions = [x for _, x in sorted(zip(tuple_versions, versions))] # Result: ['1.0.0', '1.9.0', '1.10.0']

要获取最新版本,您只需选择列表中的最后一个元素versions[-1]或使用 的reverse属性进行反向排序sorted(),将其设置为True,然后获取[0]元素。

您当然可以将所有这些包装在一个方便的函数中以供重用。

def get_latest_version(versions):
    """
    Get the latest version from a list of versions.
    """
    try:
        tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
        versions = [x for _, x in sorted(zip(tuple_versions, versions), reverse=True)]
        latest_version = versions[0]
    except Exception as e:
        print(e)
        latest_version = None

    return latest_version

print(get_latest_version(["1.0.0", "1.10.0", "1.9.0"]))
于 2022-01-08T05:14:11.027 回答
0

使用 python 增加版本

def increment_version(version):
    version = version.split('.')
    if int(version[len(version) - 1]) >= 99:
        version[len(version) - 1] = '0'
        version[len(version) - 2] = str(int(version[len(version) - 2]) + 1)
    else:
        version[len(version) - 1] = str(int(version[len(version) - 1]) + 1)
    return '.'.join(version)

version = "1.0.0"
version_type_2 = "1.0"
print("old version",version ,"new version",increment_version(version))
print("old version",version_type_2 ,"new version",increment_version(version_type_2))
于 2022-02-01T12:09:56.157 回答