web-dev-qa-db-de.com

Versionsnummernvergleich in Python

Ich möchte eine cmp-ähnliche Funktion schreiben, die zwei Versionsnummern vergleicht und -1, 0 Oder 1 Basierend auf ihren verglichenen Werten zurückgibt.

  • Geben Sie -1 Zurück, wenn Version A älter als Version B ist
  • Geben Sie 0 Zurück, wenn Version A und B gleichwertig sind
  • Geben Sie 1 Zurück, wenn Version A neuer als Version B ist

Jeder Unterabschnitt soll als Zahl interpretiert werden, also 1.10> 1.1.

Gewünschte Funktionsausgaben sind

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

Und hier ist meine Implementierung, offen für Verbesserungen:

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

Ich verwende Python 2.4.5 übrigens (an meinem Arbeitsplatz installiert ...).

Hier ist eine kleine Testsuite, die Sie verwenden können

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
93
Johannes Charra

Entfernen Sie den uninteressanten Teil der Zeichenfolge (nachgestellte Nullen und Punkte) und vergleichen Sie dann die Zahlenlisten.

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

EDIT: Gleicher Ansatz wie Pär Wieslander, jedoch etwas kompakter.

Einige Tests, danke an diesen Beitrag :

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

Wie wäre es mit Pythons distutils.version.StrictVersion?

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

Also für deine cmp Funktion:

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

Wenn Sie komplexere Versionsnummern vergleichen möchten, distutils.version.LooseVersion ist nützlicher, aber vergleichen Sie immer nur die gleichen Typen.

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

LooseVersion ist nicht das intelligenteste Werkzeug und kann leicht ausgetrickst werden:

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

Um mit dieser Rasse erfolgreich zu sein, müssen Sie die Standardbibliothek verlassen und setuptools s Parsing-Dienstprogramm parse_version .

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

Abhängig von Ihrem speziellen Anwendungsfall müssen Sie entscheiden, ob die integrierten distutils - Tools ausreichen oder ob es gerechtfertigt ist, sie als Abhängigkeit setuptools hinzuzufügen.

267
bradley.ayers

Wird Wiederverwendung in diesem Fall als Eleganz betrachtet? :)

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

Es ist nicht erforderlich, die Versionstupel zu durchlaufen. Der eingebaute Vergleichsoperator für Listen und Tupel funktioniert bereits genau so, wie Sie es möchten. Sie müssen nur die Versionslisten auf die entsprechende Länge erweitern. Mit python 2.6 können Sie izip_longest verwenden, um die Sequenzen aufzufüllen.

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)

In niedrigeren Versionen ist etwas Kartenhackery erforderlich.

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)
12
Ants Aasma

Dies ist etwas kompakter als Ihr Vorschlag. Anstatt die kürzere Version mit Nullen zu füllen, entferne ich nach dem Teilen nachfolgende Nullen aus den Versionslisten.

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))
10
Pär Wieslander

Entfernen Sie nachfolgende .0 und .00 mit Regex, teilen Sie sie und verwenden Sie die cmp-Funktion, die Arrays korrekt vergleicht.

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)

und natürlich können Sie es in einen Einzeiler umwandeln, wenn Sie die langen Schlangen nicht stören

6
yu_sha

Listen sind in Python vergleichbar. Wenn man also die Zeichenfolgen, die die Zahlen darstellen, in Ganzzahlen konvertiert, kann der grundlegende Vergleich python mit Erfolg verwendet werden.

Ich musste diesen Ansatz jedoch etwas erweitern, da ich zuerst python3x verwende, wobei cmp Funktion nicht mehr existiert - ich musste cmp (a, b) mit - emulieren (a> b) - (a <b).

Zweitens sind Versionsnummern leider überhaupt nicht so sauber, sie können alle möglichen anderen alphanumerischen Zeichen enthalten. Es gibt Fälle, in denen die Funktion die Reihenfolge nicht bestimmen kann, geben Sie False zurück (siehe das erste Beispiel).

Also poste dies, auch wenn die Frage schon alt und beantwortet ist, aber vielleicht ein paar Minuten vor dem eigenen Leben retten kann.

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

Es ist ein Einzeiler (zwecks Lesbarkeit aufgeteilt). Ich bin mir nicht sicher, ob ich lesbar bin ...

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

Implementiere für PHP version_compare, außer "=". Weil es mehrdeutig ist.

2
heronotears

Für den Fall, dass Sie keine externe Abhängigkeit einbauen möchten, ist hier ein Versuch von mir (geschrieben für python 3.x). "Rc", "rel" (und möglicherweise könnte man hinzufügen) "c") gelten als "Release Candidate" und unterteilen die Versionsnummer in zwei Teile, und wenn sie fehlen, ist der Wert des zweiten Teils hoch (999). Anderenfalls erzeugen Buchstaben einen Split und werden über den Base-36-Code als Subnummern behandelt .


    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))
2
Roland Puntaier

Die am schwierigsten zu lesende Lösung, aber trotzdem ein Einzeiler! und Iteratoren verwenden, um schnell zu sein.

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)

das ist für Python2.6 und 3. + Übrigens, Python 2.5 und älter müssen die StopIteration abfangen.

1
Paul

Eine andere Lösung:

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

Man kann das auch so benutzen:

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

Habe dies getan, um in der Lage zu sein, den Debian-Paketversions-String zu analysieren und zu vergleichen. Bitte beachten Sie, dass dies bei der Zeichenüberprüfung nicht streng ist.

Dies könnte ebenfalls hilfreich sein.

#!/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 __== '__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')
0
Pius Raeder

ich benutze dieses für mein Projekt:

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