Gibt es eine eingebaute Funktion für die natürliche Sortierung von Strings?

281

Mit Python 3.x habe ich eine Liste von Zeichenfolgen, für die ich eine natürliche alphabetische Sortierung durchführen möchte.

Natürliche Sortierung: Die Reihenfolge, in der Dateien in Windows sortiert werden.

Zum Beispiel ist die folgende Liste natürlich sortiert (was ich will):

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Und hier ist die "sortierte" Version der obigen Liste (was ich habe):

['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']

Ich suche eine Sortierfunktion, die sich wie die erste verhält.

snakile
quelle
13
Die Definition einer natürlichen Sortierung lautet nicht "die Reihenfolge, in der Windows Dateien sortiert".
Glenn Maynard
Alle Antworten auf dieser Site führen zu falschen Ergebnissen, wenn Sie in mehreren Fällen eine Windows-Explorer-ähnliche Sortierung wünschen , z. B. eine Sortierung !1, 1, !a, a. Die einzige Möglichkeit, wie Windows zu sortieren, scheint darin zu bestehen, die Windows- StrCmpLogicalW Funktion selbst zu verwenden, da diese Funktion anscheinend niemand korrekt implementiert hat (Quelle wäre willkommen). Lösung: stackoverflow.com/a/48030307/2441026
user136036

Antworten:

235

Auf PyPI gibt es dafür eine Drittanbieter-Bibliothek namens natsort (vollständige Offenlegung, ich bin der Autor des Pakets). Für Ihren Fall können Sie eine der folgenden Aktionen ausführen:

>>> from natsort import natsorted, ns
>>> x = ['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']
>>> natsorted(x, key=lambda y: y.lower())
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> natsorted(x, alg=ns.IGNORECASE)  # or alg=ns.IC
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Sie sollten beachten, dass natsortein allgemeiner Algorithmus verwendet wird, damit er für nahezu jede Eingabe funktioniert, die Sie darauf werfen. Wenn Sie weitere Informationen darüber wünschen, warum Sie möglicherweise eine Bibliothek auswählen, um dies zu tun, anstatt Ihre eigene Funktion zu erweitern, lesen Sie die Seite Funktionsweise der natsortDokumentation , insbesondere die Sonderfälle überall! Sektion.


Wenn Sie anstelle einer Sortierfunktion einen Sortierschlüssel benötigen, verwenden Sie eine der folgenden Formeln.

>>> from natsort import natsort_keygen, ns
>>> l1 = ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> l2 = l1[:]
>>> natsort_key1 = natsort_keygen(key=lambda y: y.lower())
>>> l1.sort(key=natsort_key1)
>>> l1
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> natsort_key2 = natsort_keygen(alg=ns.IGNORECASE)
>>> l2.sort(key=natsort_key2)
>>> l2
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SethMMorton
quelle
5
Ich finde es auch sehr interessant, dass natsort auch dann richtig sortiert, wenn die Nummer nicht am Ende steht: wie es häufig bei Dateinamen der Fall ist. Fühlen Sie sich frei, das folgende Beispiel aufzunehmen: pastebin.com/9cwCLdEK
Martin Thoma
1
Natsort ist eine großartige Bibliothek, sollte zur Python-Standardbibliothek hinzugefügt werden! :-)
Mitch McMabers
natsortbehandelt 'natürlich' auch den Fall mehrerer separater Zahlen in den Zeichenfolgen. Tolles Zeug!
FlorianH
182

Versuche dies:

import re

def natural_sort(l): 
    convert = lambda text: int(text) if text.isdigit() else text.lower() 
    alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] 
    return sorted(l, key = alphanum_key)

Ausgabe:

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Von hier angepasster Code: Sortieren für Menschen: Natürliche Sortierreihenfolge .

Mark Byers
quelle
2
warum benutzt du return sorted(l, key)statt l.sort(key)? Ist es für einen Leistungsgewinn oder nur um pythonischer zu sein?
Jperelli
12
@jperelli Ich denke, die Leiter würde die ursprüngliche Liste im Anrufer ändern. Höchstwahrscheinlich möchte der Anrufer jedoch eine weitere flache Kopie der Liste.
Huggie
3
Nur für den Datensatz kann dies nicht alle Eingaben verarbeiten: Die str / int-Teilungen müssen ausgerichtet sein, andernfalls erstellen Sie Vergleiche wie ["foo", 0] <[0, "foo"] für die Eingabe ["foo0" "," 0foo "], wodurch ein TypeError ausgelöst wird.
user19087
4
@ user19087: Tatsächlich funktioniert es, weil es re.split('([0-9]+)', '0foo')zurückkehrt ['', '0', 'foo']. Aus diesem Grund befinden sich Zeichenfolgen immer in geraden Indizes und Ganzzahlen in ungeraden Indizes im Array.
Florian Kusche
Für alle, die sich über die Leistung wundern, ist dies deutlich langsamer als die native Python-Sorte. dh 25-50x langsamer. Und wenn Sie [elm1, elm2, Elm2, elm2] immer zuverlässig als [elm1, Elm2, elm2, elm2] sortieren möchten (Großbuchstaben vor dem unteren), können Sie einfach natural_sort (sortiert (lst)) aufrufen. Ineffizienter, aber sehr einfach, eine wiederholbare Sorte zu erhalten. Kompilieren Sie den regulären Ausdruck für eine Beschleunigung von ~ 50%. wie in Claudius Antwort zu sehen.
Charlie Haley
98

Hier ist eine viel pythonischere Version von Mark Byers Antwort:

import re

def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
    return [int(text) if text.isdigit() else text.lower()
            for text in _nsre.split(s)]    

Nun kann diese Funktion als Schlüssel in einem beliebigen Funktion verwendet werden , die es verwendet, wie list.sort, sorted, maxusw.

Als Lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Claudiu
quelle
9
re Modul kompiliert und zwischenspeichert Regexes automatisch, so dass keine Vorkompilierung
erforderlich ist
1
@wim: Es werden die letzten X-Verwendungen zwischengespeichert, daher ist es technisch möglich, X + 5-Regexe zu verwenden und dann immer wieder eine natürliche Sortierung durchzuführen. Zu diesem Zeitpunkt wird dies nicht zwischengespeichert. aber wahrscheinlich auf lange Sicht vernachlässigbar
Claudiu
Ich habe es nicht getan, aber vielleicht war der Grund, dass es nicht mit Tupeln umgehen kann, wie eine normale Python-Sorte.
Die Unfun Cat
1
Die von @Claudiu erwähnten X-Verwendungen scheinen unter Python 2.7 100 und unter Python 3.4 512 zu sein. Beachten Sie auch, dass der Cache bei Erreichen des Grenzwerts vollständig gelöscht wird (es wird also nicht nur der älteste verworfen).
Zitrax
@Zitrax Warum / Wie ist es sinnvoll, den Cache vollständig zu leeren?
Joschua
19

Ich habe eine Funktion geschrieben, die auf http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html basiert und die Möglichkeit bietet, weiterhin Ihren eigenen 'Schlüssel'-Parameter zu übergeben. Ich brauche dies, um eine natürliche Art von Listen auszuführen, die komplexere Objekte enthalten (nicht nur Zeichenfolgen).

import re

def natural_sort(list, key=lambda s:s):
    """
    Sort the list into natural alphanumeric order.
    """
    def get_alphanum_key_func(key):
        convert = lambda text: int(text) if text.isdigit() else text 
        return lambda s: [convert(c) for c in re.split('([0-9]+)', key(s))]
    sort_key = get_alphanum_key_func(key)
    list.sort(key=sort_key)

Beispielsweise:

my_list = [{'name':'b'}, {'name':'10'}, {'name':'a'}, {'name':'1'}, {'name':'9'}]
natural_sort(my_list, key=lambda x: x['name'])
print my_list
[{'name': '1'}, {'name': '9'}, {'name': '10'}, {'name': 'a'}, {'name': 'b'}]
Beauburrier
quelle
Ein einfacherer Weg, dies zu tun, wäre zu definieren natural_sort_key, und dann, wenn Sie eine Liste sortieren, könnten Sie Ihre Schlüssel verketten, zB:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Lassen Sie uns die Daten analysieren. Die Ziffernkapazität aller Elemente beträgt 2. Und es gibt 3 Buchstaben im gemeinsamen wörtlichen Teil 'elm'.

Die maximale Länge des Elements beträgt also 5. Wir können diesen Wert erhöhen, um sicherzugehen (z. B. auf 8).

Vor diesem Hintergrund haben wir eine einzeilige Lösung:

data.sort(key=lambda x: '{0:0>8}'.format(x).lower())

ohne reguläre Ausdrücke und externe Bibliotheken!

print(data)

>>> ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'elm13']

Erläuterung:

for elm in data:
    print('{0:0>8}'.format(elm).lower())

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
SergO
quelle
1
Dies behandelt keine dynamischen / unbekannten Längenangaben. Es wird auch anders sortiert als bei anderen Lösungen für Daten, deren Zahlen Zahlen am Ende enthalten. * Dies ist nicht unbedingt unerwünscht, aber ich denke, es ist gut, darauf hinzuweisen.
JerodG
1
Wenn Sie dynamische Längenangaben verarbeiten müssen, können Sie width = max(data, key=len)berechnen, was für die 8oben genannten Daten '{0:0>{width}}'.format(x, width=width)
eingereicht werden soll,
1
Nur durch einen zeitgesteuerten Test im Vergleich zu allen anderen in diesem Forum ist diese Lösung bei weitem die schnellste und effizienteste für die Art von Daten, die @snakile zu verarbeiten versucht
SR Colledge
13

Gegeben:

data=['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']

Ähnlich wie bei SergOs Lösung wäre ein 1-Liner ohne externe Bibliotheken :

data.sort(key=lambda x : int(x[3:]))

oder

sorted_data=sorted(data, key=lambda x : int(x[3:]))

Erläuterung:

Diese Lösung nutzt das Schlüsselmerkmal sort eine Funktion zu definieren , die für die Sortierung verwendet werden. Da wir wissen, dass vor jeder Dateneingabe 'elm' steht, konvertiert die Sortierfunktion den Teil der Zeichenfolge nach dem 3. Zeichen (dh int (x [3:])) in eine Ganzzahl. Wenn sich der numerische Teil der Daten an einer anderen Stelle befindet, müsste sich dieser Teil der Funktion ändern.

Prost

Camilo
quelle
6
Und jetzt zu etwas eleganterem (pythonischem) - nur eine Berührung

Es gibt viele Implementierungen, und während einige nahe gekommen sind, hat keine die Eleganz, die moderne Python bietet, ganz eingefangen.

  • Getestet mit Python (3.5.1)
  • Enthält eine zusätzliche Liste, um zu demonstrieren, dass es funktioniert, wenn die Zahlen eine mittlere Zeichenfolge sind
  • Nicht getestet, aber ich gehe davon aus, dass es effizienter wäre, die Regex vorher zu kompilieren, wenn Ihre Liste umfangreich wäre
    • Ich bin sicher, jemand wird mich korrigieren, wenn dies eine falsche Annahme ist

Schnelle Nummer
from re import compile, split    
dre = compile(r'(\d+)')
mylist.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])
Vollcode
#!/usr/bin/python3
# coding=utf-8
"""
Natural-Sort Test
"""

from re import compile, split

dre = compile(r'(\d+)')
mylist = ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13', 'elm']
mylist2 = ['e0lm', 'e1lm', 'E2lm', 'e9lm', 'e10lm', 'E12lm', 'e13lm', 'elm', 'e01lm']

mylist.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])
mylist2.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])

print(mylist)  
  # ['elm', 'elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
print(mylist2)  
  # ['e0lm', 'e1lm', 'e01lm', 'E2lm', 'e9lm', 'e10lm', 'E12lm', 'e13lm', 'elm']

Vorsicht bei der Verwendung

  • from os.path import split
    • Sie müssen die Importe differenzieren

Inspiration von

JerodG
quelle
6

Wert dieses Beitrags

Mein Ziel ist es, eine Nicht-Regex-Lösung anzubieten, die allgemein angewendet werden kann.
Ich werde drei Funktionen erstellen:

  1. find_first_digitwas ich von @AnuragUniyal ausgeliehen habe . Es wird die Position der ersten Ziffer oder Nicht-Ziffer in einer Zeichenfolge gefunden.
  2. split_digitsDies ist ein Generator, der eine Zeichenfolge in Ziffern- und Nicht-Ziffernblöcke aufteilt. Es werden auch yieldganze Zahlen angezeigt, wenn es sich um eine Ziffer handelt.
  3. natural_keywickelt sich einfach split_digitsin ein tuple. Dies ist , was wir als Schlüssel verwenden für sorted, max, min.

Funktionen

def find_first_digit(s, non=False):
    for i, x in enumerate(s):
        if x.isdigit() ^ non:
            return i
    return -1

def split_digits(s, case=False):
    non = True
    while s:
        i = find_first_digit(s, non)
        if i == 0:
            non = not non
        elif i == -1:
            yield int(s) if s.isdigit() else s if case else s.lower()
            s = ''
        else:
            x, s = s[:i], s[i:]
            yield int(x) if x.isdigit() else x if case else x.lower()

def natural_key(s, *args, **kwargs):
    return tuple(split_digits(s, *args, **kwargs))

Wir können sehen, dass es allgemein ist, dass wir mehrstellige Chunks haben können:

# Note that the key has lower case letters
natural_key('asl;dkfDFKJ:sdlkfjdf809lkasdjfa_543_hh')

('asl;dkfdfkj:sdlkfjdf', 809, 'lkasdjfa_', 543, '_hh')

Oder als Groß- und Kleinschreibung beachten:

natural_key('asl;dkfDFKJ:sdlkfjdf809lkasdjfa_543_hh', True)

('asl;dkfDFKJ:sdlkfjdf', 809, 'lkasdjfa_', 543, '_hh')

Wir können sehen, dass die Liste des OP in der richtigen Reihenfolge sortiert wird

sorted(
    ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13'],
    key=natural_key
)

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Es kann aber auch kompliziertere Listen verarbeiten:

sorted(
    ['f_1', 'e_1', 'a_2', 'g_0', 'd_0_12:2', 'd_0_1_:2'],
    key=natural_key
)

['a_2', 'd_0_1_:2', 'd_0_12:2', 'e_1', 'f_1', 'g_0']

Mein Regex-Äquivalent wäre

def int_maybe(x):
    return int(x) if str(x).isdigit() else x

def split_digits_re(s, case=False):
    parts = re.findall('\d+|\D+', s)
    if not case:
        return map(int_maybe, (x.lower() for x in parts))
    else:
        return map(int_maybe, parts)

def natural_key_re(s, *args, **kwargs):
    return tuple(split_digits_re(s, *args, **kwargs))
piRSquared
quelle
1
Vielen Dank! Ich möchte jedoch hinzufügen, dass, wenn Sie "12345_A" und "12345_A2" haben, letzteres vor dem ersten sortiert wird. So macht es zumindest Windows nicht. Funktioniert jedoch immer noch für das oben genannte Problem!
morph3us
4

Eine Möglichkeit besteht darin, die Zeichenfolge in ein Tupel umzuwandeln und Ziffern mithilfe des erweiterten Formulars http://wiki.answers.com/Q/What_does_expanded_form_mean zu ersetzen

auf diese Weise würde a90 ("a", 90,0) und a1 ("a", 1) werden

Im Folgenden finden Sie einen Beispielcode (der aufgrund der Art und Weise, wie führende Nullen aus Zahlen entfernt werden, nicht sehr effizient ist.)

alist=["something1",
    "something12",
    "something17",
    "something2",
    "something25and_then_33",
    "something25and_then_34",
    "something29",
    "beta1.1",
    "beta2.3.0",
    "beta2.33.1",
    "a001",
    "a2",
    "z002",
    "z1"]

def key(k):
    nums=set(list("0123456789"))
        chars=set(list(k))
    chars=chars-nums
    for i in range(len(k)):
        for c in chars:
            k=k.replace(c+"0",c)
    l=list(k)
    base=10
    j=0
    for i in range(len(l)-1,-1,-1):
        try:
            l[i]=int(l[i])*base**j
            j+=1
        except:
            j=0
    l=tuple(l)
    print l
    return l

print sorted(alist,key=key)

Ausgabe:

('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 1)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 10, 2)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 10, 7)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 2)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 5, 'a', 'n', 'd', '_', 't', 'h', 'e', 'n', '_', 30, 3)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 5, 'a', 'n', 'd', '_', 't', 'h', 'e', 'n', '_', 30, 4)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 9)
('b', 'e', 't', 'a', 1, '.', 1)
('b', 'e', 't', 'a', 2, '.', 3, '.')
('b', 'e', 't', 'a', 2, '.', 30, 3, '.', 1)
('a', 1)
('a', 2)
('z', 2)
('z', 1)
['a001', 'a2', 'beta1.1', 'beta2.3.0', 'beta2.33.1', 'something1', 'something2', 'something12', 'something17', 'something25and_then_33', 'something25and_then_34', 'something29', 'z1', 'z002']
Robert King
quelle
1
Leider funktioniert diese Lösung nur für Python 2.X. Für Python 3 ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)wird zurückkehrenTypeError: unorderable types: int() < str()
SethMMorton
@ SethMMorgon ist richtig, dieser Code bricht leicht in Python 3. Die natürliche Alternative scheint natsort, pypi.org/project/natsort
FlorianH
3

Basierend auf den Antworten hier habe ich eine natural_sortedFunktion geschrieben, die sich wie die eingebaute Funktion verhält sorted:

# Copyright (C) 2018, Benjamin Drung <[email protected]>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import re

def natural_sorted(iterable, key=None, reverse=False):
    """Return a new naturally sorted list from the items in *iterable*.

    The returned list is in natural sort order. The string is ordered
    lexicographically (using the Unicode code point number to order individual
    characters), except that multi-digit numbers are ordered as a single
    character.

    Has two optional arguments which must be specified as keyword arguments.

    *key* specifies a function of one argument that is used to extract a
    comparison key from each list element: ``key=str.lower``.  The default value
    is ``None`` (compare the elements directly).

    *reverse* is a boolean value.  If set to ``True``, then the list elements are
    sorted as if each comparison were reversed.

    The :func:`natural_sorted` function is guaranteed to be stable. A sort is
    stable if it guarantees not to change the relative order of elements that
    compare equal --- this is helpful for sorting in multiple passes (for
    example, sort by department, then by salary grade).
    """
    prog = re.compile(r"(\d+)")

    def alphanum_key(element):
        """Split given key in list of strings and digits"""
        return [int(c) if c.isdigit() else c for c in prog.split(key(element)
                if key else element)]

    return sorted(iterable, key=alphanum_key, reverse=reverse)

Der Quellcode ist auch in meinem GitHub-Snippets-Repository verfügbar: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Benjamin Drung
quelle
2

Die obigen Antworten sind gut für das spezifische Beispiel , das gezeigt wurde, aber es fehlen einige nützliche Fälle für die allgemeinere Frage der natürlichen Art. Ich bin gerade von einem dieser Fälle gebissen worden und habe eine gründlichere Lösung gefunden:

def natural_sort_key(string_or_number):
    """
    by Scott S. Lawton <[email protected]> 2014-12-11; public domain and/or CC0 license

    handles cases where simple 'int' approach fails, e.g.
        ['0.501', '0.55'] floating point with different number of significant digits
        [0.01, 0.1, 1]    already numeric so regex and other string functions won't work (and aren't required)
        ['elm1', 'Elm2']  ASCII vs. letters (not case sensitive)
    """

    def try_float(astring):
        try:
            return float(astring)
        except:
            return astring

    if isinstance(string_or_number, basestring):
        string_or_number = string_or_number.lower()

        if len(re.findall('[.]\d', string_or_number)) <= 1:
            # assume a floating point value, e.g. to correctly sort ['0.501', '0.55']
            # '.' for decimal is locale-specific, e.g. correct for the Anglosphere and Asia but not continental Europe
            return [try_float(s) for s in re.split(r'([\d.]+)', string_or_number)]
        else:
            # assume distinct fields, e.g. IP address, phone number with '.', etc.
            # caveat: might want to first split by whitespace
            # TBD: for unicode, replace isdigit with isdecimal
            return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_or_number)]
    else:
        # consider: add code to recurse for lists/tuples and perhaps other iterables
        return string_or_number

Testcode und mehrere Links (ein- und ausschalten von StackOverflow) finden Sie hier: http://productarchitect.com/code/better-natural-sort.py

Feedback willkommen. Das soll keine endgültige Lösung sein. nur ein Schritt vorwärts.

Scott Lawton
quelle
In Ihrem Testskript, auf das Sie verlinken, natsortedund das humansortedfehlschlägt, weil sie falsch verwendet wurden ... Sie haben versucht, natsortedals Schlüssel zu übergeben, aber es ist eigentlich die Sortierfunktion selbst. Du hättest es versuchen sollen natsort_keygen().
SethMMorton
2

Höchstwahrscheinlich functools.cmp_to_key()hängt es eng mit der zugrunde liegenden Implementierung der Python-Sortierung zusammen. Außerdem ist der Parameter cmp ein Legacy. Die moderne Methode besteht darin, die Eingabeelemente in Objekte umzuwandeln, die die gewünschten umfangreichen Vergleichsoperationen unterstützen.

Unter CPython 2.x können Objekte unterschiedlicher Typen bestellt werden, auch wenn die jeweiligen Rich-Vergleichsoperatoren nicht implementiert wurden. Unter CPython 3.x müssen Objekte unterschiedlichen Typs den Vergleich explizit unterstützen. Siehe Wie vergleicht Python String und Int? die Links zur offiziellen Dokumentation . Die meisten Antworten hängen von dieser impliziten Reihenfolge ab. Für den Wechsel zu Python 3.x ist ein neuer Typ erforderlich, um Vergleiche zwischen Zahlen und Zeichenfolgen zu implementieren und zu vereinheitlichen.

Python 2.7.12 (default, Sep 29 2016, 13:30:34) 
>>> (0,"foo") < ("foo",0)
True  
Python 3.5.2 (default, Oct 14 2016, 12:54:53) 
>>> (0,"foo") < ("foo",0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  TypeError: unorderable types: int() < str()

Es gibt drei verschiedene Ansätze. Die erste verwendet verschachtelte Klassen, um den IterableVergleichsalgorithmus von Python zu nutzen . Der zweite rollt diese Verschachtelung in eine einzelne Klasse ab. Die dritte verzichtet auf Unterklassen, strum sich auf die Leistung zu konzentrieren. Alle sind zeitgesteuert; Der zweite ist doppelt so schnell, während der dritte fast sechsmal schneller ist. Unterklassen strsind nicht erforderlich und waren wahrscheinlich an erster Stelle eine schlechte Idee, bringen jedoch gewisse Annehmlichkeiten mit sich.

Die Sortierzeichen werden dupliziert, um die Reihenfolge nach Groß- und Kleinschreibung zu erzwingen, und die Groß- und Kleinschreibung ausgetauscht, um zu erzwingen, dass Kleinbuchstaben zuerst sortiert werden. Dies ist die typische Definition von "natürlicher Sorte". Ich konnte mich nicht für die Art der Gruppierung entscheiden. Einige bevorzugen möglicherweise Folgendes, was ebenfalls erhebliche Leistungsvorteile mit sich bringt:

d = lambda s: s.lower()+s.swapcase()

Wo verwendet, werden die Vergleichsoperatoren auf die von eingestellt, objectdamit sie von nicht ignoriert werdenfunctools.total_ordering .

import functools
import itertools


@functools.total_ordering
class NaturalStringA(str):
    def __repr__(self):
        return "{}({})".format\
            ( type(self).__name__
            , super().__repr__()
            )
    d = lambda c, s: [ c.NaturalStringPart("".join(v))
                        for k,v in
                       itertools.groupby(s, c.isdigit)
                     ]
    d = classmethod(d)
    @functools.total_ordering
    class NaturalStringPart(str):
        d = lambda s: "".join(c.lower()+c.swapcase() for c in s)
        d = staticmethod(d)
        def __lt__(self, other):
            if not isinstance(self, type(other)):
                return NotImplemented
            try:
                return int(self) < int(other)
            except ValueError:
                if self.isdigit():
                    return True
                elif other.isdigit():
                    return False
                else:
                    return self.d(self) < self.d(other)
        def __eq__(self, other):
            if not isinstance(self, type(other)):
                return NotImplemented
            try:
                return int(self) == int(other)
            except ValueError:
                if self.isdigit() or other.isdigit():
                    return False
                else:
                    return self.d(self) == self.d(other)
        __le__ = object.__le__
        __ne__ = object.__ne__
        __gt__ = object.__gt__
        __ge__ = object.__ge__
    def __lt__(self, other):
        return self.d(self) < self.d(other)
    def __eq__(self, other):
        return self.d(self) == self.d(other)
    __le__ = object.__le__
    __ne__ = object.__ne__
    __gt__ = object.__gt__
    __ge__ = object.__ge__
import functools
import itertools


@functools.total_ordering
class NaturalStringB(str):
    def __repr__(self):
        return "{}({})".format\
            ( type(self).__name__
            , super().__repr__()
            )
    d = lambda s: "".join(c.lower()+c.swapcase() for c in s)
    d = staticmethod(d)
    def __lt__(self, other):
        if not isinstance(self, type(other)):
            return NotImplemented
        groups = map(lambda i: itertools.groupby(i, type(self).isdigit), (self, other))
        zipped = itertools.zip_longest(*groups)
        for s,o in zipped:
            if s is None:
                return True
            if o is None:
                return False
            s_k, s_v = s[0], "".join(s[1])
            o_k, o_v = o[0], "".join(o[1])
            if s_k and o_k:
                s_v, o_v = int(s_v), int(o_v)
                if s_v == o_v:
                    continue
                return s_v < o_v
            elif s_k:
                return True
            elif o_k:
                return False
            else:
                s_v, o_v = self.d(s_v), self.d(o_v)
                if s_v == o_v:
                    continue
                return s_v < o_v
        return False
    def __eq__(self, other):
        if not isinstance(self, type(other)):
            return NotImplemented
        groups = map(lambda i: itertools.groupby(i, type(self).isdigit), (self, other))
        zipped = itertools.zip_longest(*groups)
        for s,o in zipped:
            if s is None or o is None:
                return False
            s_k, s_v = s[0], "".join(s[1])
            o_k, o_v = o[0], "".join(o[1])
            if s_k and o_k:
                s_v, o_v = int(s_v), int(o_v)
                if s_v == o_v:
                    continue
                return False
            elif s_k or o_k:
                return False
            else:
                s_v, o_v = self.d(s_v), self.d(o_v)
                if s_v == o_v:
                    continue
                return False
        return True
    __le__ = object.__le__
    __ne__ = object.__ne__
    __gt__ = object.__gt__
    __ge__ = object.__ge__
import functools
import itertools
import enum


class OrderingType(enum.Enum):
    PerWordSwapCase         = lambda s: s.lower()+s.swapcase()
    PerCharacterSwapCase    = lambda s: "".join(c.lower()+c.swapcase() for c in s)


class NaturalOrdering:
    @classmethod
    def by(cls, ordering):
        def wrapper(string):
            return cls(string, ordering)
        return wrapper
    def __init__(self, string, ordering=OrderingType.PerCharacterSwapCase):
        self.string = string
        self.groups = [ (k,int("".join(v)))
                            if k else
                        (k,ordering("".join(v)))
                            for k,v in
                        itertools.groupby(string, str.isdigit)
                      ]
    def __repr__(self):
        return "{}({})".format\
            ( type(self).__name__
            , self.string
            )
    def __lesser(self, other, default):
        if not isinstance(self, type(other)):
            return NotImplemented
        for s,o in itertools.zip_longest(self.groups, other.groups):
            if s is None:
                return True
            if o is None:
                return False
            s_k, s_v = s
            o_k, o_v = o
            if s_k and o_k:
                if s_v == o_v:
                    continue
                return s_v < o_v
            elif s_k:
                return True
            elif o_k:
                return False
            else:
                if s_v == o_v:
                    continue
                return s_v < o_v
        return default
    def __lt__(self, other):
        return self.__lesser(other, default=False)
    def __le__(self, other):
        return self.__lesser(other, default=True)
    def __eq__(self, other):
        if not isinstance(self, type(other)):
            return NotImplemented
        for s,o in itertools.zip_longest(self.groups, other.groups):
            if s is None or o is None:
                return False
            s_k, s_v = s
            o_k, o_v = o
            if s_k and o_k:
                if s_v == o_v:
                    continue
                return False
            elif s_k or o_k:
                return False
            else:
                if s_v == o_v:
                    continue
                return False
        return True
    # functools.total_ordering doesn't create single-call wrappers if both
    # __le__ and __lt__ exist, so do it manually.
    def __gt__(self, other):
        op_result = self.__le__(other)
        if op_result is NotImplemented:
            return op_result
        return not op_result
    def __ge__(self, other):
        op_result = self.__lt__(other)
        if op_result is NotImplemented:
            return op_result
        return not op_result
    # __ne__ is the only implied ordering relationship, it automatically
    # delegates to __eq__
>>> import natsort
>>> import timeit
>>> l1 = ['Apple', 'corn', 'apPlE', 'arbour', 'Corn', 'Banana', 'apple', 'banana']
>>> l2 = list(map(str, range(30)))
>>> l3 = ["{} {}".format(x,y) for x in l1 for y in l2]
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalStringA)', number=10000, globals=globals()))
362.4729259099986
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalStringB)', number=10000, globals=globals()))
189.7340817489967
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalOrdering.by(OrderingType.PerCharacterSwapCase))', number=10000, globals=globals()))
69.34636392899847
>>> print(timeit.timeit('natsort.natsorted(l3+["0"], alg=natsort.ns.GROUPLETTERS | natsort.ns.LOWERCASEFIRST)', number=10000, globals=globals()))
98.2531585780016

Natürliche Sortierung ist sowohl ziemlich kompliziert als auch vage als Problem definiert. Vergessen Sie nicht , laufen unicodedata.normalize(...)vorher, und betrachten den Einsatz str.casefold()statt str.lower(). Es gibt wahrscheinlich subtile Codierungsprobleme, die ich nicht berücksichtigt habe. Daher empfehle ich vorläufig die Natsort- Bibliothek. Ich warf einen kurzen Blick auf das Github-Repository. Die Code-Wartung war hervorragend.

Alle Algorithmen, die ich gesehen habe, hängen von Tricks ab, wie dem Duplizieren und Verringern von Zeichen und dem Vertauschen von Groß- und Kleinschreibung. Während dies die Laufzeit verdoppelt, würde eine Alternative eine vollständige natürliche Reihenfolge des Eingabezeichensatzes erfordern. Ich denke nicht, dass dies Teil der Unicode-Spezifikation ist, und da es viel mehr Unicode-Ziffern als gibt [0-9], wäre das Erstellen einer solchen Sortierung ebenso entmutigend. Wenn Sie Vergleiche mit Gebietsschema wünschen, bereiten Sie Ihre Zeichenfolgen locale.strxfrmgemäß Pythons Sortieranleitung vor .

user19087
quelle
1

Lassen Sie mich meine eigene Meinung zu diesem Bedürfnis einreichen:

from typing import Tuple, Union, Optional, Generator


StrOrInt = Union[str, int]


# On Python 3.6, string concatenation is REALLY fast
# Tested myself, and this fella also tested:
# https://blog.ganssle.io/articles/2019/11/string-concat.html
def griter(s: str) -> Generator[StrOrInt, None, None]:
    last_was_digit: Optional[bool] = None
    cluster: str = ""
    for c in s:
        if last_was_digit is None:
            last_was_digit = c.isdigit()
            cluster += c
            continue
        if c.isdigit() != last_was_digit:
            if last_was_digit:
                yield int(cluster)
            else:
                yield cluster
            last_was_digit = c.isdigit()
            cluster = ""
        cluster += c
    if last_was_digit:
        yield int(cluster)
    else:
        yield cluster
    return


def grouper(s: str) -> Tuple[StrOrInt, ...]:
    return tuple(griter(s))

Wenn wir nun eine solche Liste haben:

filelist = [
    'File3', 'File007', 'File3a', 'File10', 'File11', 'File1', 'File4', 'File5',
    'File9', 'File8', 'File8b1', 'File8b2', 'File8b11', 'File6'
]

Wir können den key=Kwarg einfach verwenden , um eine natürliche Sortierung durchzuführen:

>>> sorted(filelist, key=grouper)
['File1', 'File3', 'File3a', 'File4', 'File5', 'File6', 'File007', 'File8', 
'File8b1', 'File8b2', 'File8b11', 'File9', 'File10', 'File11']

Der Nachteil hierbei ist natürlich, dass die Funktion wie jetzt Großbuchstaben vor Kleinbuchstaben sortiert.

Ich überlasse die Implementierung eines case-insenstive Grouper dem Leser :-)

pepoluan
quelle
0

Ich schlage vor, Sie verwenden einfach das keySchlüsselwortargument von sorted, um Ihre gewünschte Liste zu erhalten.
Zum Beispiel:

to_order= [e2,E1,e5,E4,e3]
ordered= sorted(to_order, key= lambda x: x.lower())
    # ordered should be [E1,e2,e3,E4,e5]
Johny Vaknin
quelle
1
Dies behandelt keine Ziffern. a_51wäre danach a500, obwohl 500> 51
skjerns
Richtig, meine Antwort entspricht einfach dem gegebenen Beispiel von Elm11 und elm1. Verpasste die Anfrage nach natürlicher Sorte speziell und die markierte Antwort ist wahrscheinlich die beste hier :)
Johny Vaknin
0

Nach der Antwort von @Mark Byers finden Sie hier eine Anpassung, die den keyParameter akzeptiert und PEP8-kompatibler ist.

def natsorted(seq, key=None):
    def convert(text):
        return int(text) if text.isdigit() else text

    def alphanum(obj):
        if key is not None:
            return [convert(c) for c in re.split(r'([0-9]+)', key(obj))]
        return [convert(c) for c in re.split(r'([0-9]+)', obj)]

    return sorted(seq, key=alphanum)

Ich habe auch einen Kern gemacht

Edouardtheron
quelle
(-1) Diese Antwort bringt nichts Neues im Vergleich zu Marks (jeder Linter kann PEP8-ify Code). Oder vielleicht der keyParameter? Dies zeigt sich aber auch in der Antwort von @ beauburrier
Ciprian Tomoiagă
0

Eine Verbesserung gegenüber Claudius Verbesserung gegenüber Mark Byers Antwort ;-)

import re

def natural_sort_key(s, _re=re.compile(r'(\d+)')):
    return [int(t) if i & 1 else t.lower() for i, t in enumerate(_re.split(s))]

...
my_naturally_sorted_list = sorted(my_list, key=natural_sort_key)

Übrigens erinnert sich vielleicht nicht jeder daran, dass die Standardeinstellungen für Funktionsargumente zur defZeit ausgewertet werden

Walter Tross
quelle
-1
a = ['H1', 'H100', 'H10', 'H3', 'H2', 'H6', 'H11', 'H50', 'H5', 'H99', 'H8']
b = ''
c = []

def bubble(bad_list):#bubble sort method
        length = len(bad_list) - 1
        sorted = False

        while not sorted:
                sorted = True
                for i in range(length):
                        if bad_list[i] > bad_list[i+1]:
                                sorted = False
                                bad_list[i], bad_list[i+1] = bad_list[i+1], bad_list[i] #sort the integer list 
                                a[i], a[i+1] = a[i+1], a[i] #sort the main list based on the integer list index value

for a_string in a: #extract the number in the string character by character
        for letter in a_string:
                if letter.isdigit():
                        #print letter
                        b += letter
        c.append(b)
        b = ''

print 'Before sorting....'
print a
c = map(int, c) #converting string list into number list
print c
bubble(c)

print 'After sorting....'
print c
print a

Danksagung :

Bubble Sort Hausaufgaben

Wie man eine Zeichenfolge in Python buchstabenweise liest

Varadaraju G.
quelle
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
quelle
4
Ihre Implementierung löst nur das Zahlenproblem. Die Implementierung schlägt fehl, wenn die Zeichenfolgen keine Zahlen enthalten. Versuchen Sie es zum Beispiel mit ['Silent', 'Ghost'] (Listenindex außerhalb des Bereichs).
Snakile
2
@snaklie: Ihre Frage liefert keinen anständigen Beispielfall. Sie haben nicht erklärt, was Sie versuchen, und Sie haben Ihre Frage auch nicht mit diesen neuen Informationen aktualisiert. Sie haben nichts veröffentlicht, was Sie versucht haben. Bitte lehnen Sie meinen Telepathieversuch nicht so ab.
SilentGhost
5
@ SilentGhost: Zuerst habe ich dir eine Gegenstimme gegeben, weil ich denke, dass deine Antwort nützlich ist (obwohl sie mein Problem nicht löst). Zweitens kann ich nicht alle möglichen Fälle mit Beispielen abdecken. Ich glaube, ich habe die natürliche Sorte ziemlich klar definiert. Ich halte es nicht für eine gute Idee, einem so einfachen Konzept ein komplexes Beispiel oder eine lange Definition zu geben. Sie können meine Frage gerne bearbeiten, wenn Sie sich eine bessere Formulierung für das Problem vorstellen können.
Snakile
1
@SilentGhost: Ich möchte mit solchen Zeichenfolgen genauso umgehen wie Windows mit solchen Dateinamen, wenn Dateien nach Namen sortiert werden (Groß- und Kleinschreibung ignorieren usw.). Es scheint mir klar zu sein, aber alles, was ich sage, scheint mir klar zu sein, also kann ich nicht beurteilen, ob es klar ist oder nicht.
Snakile
1
@snakile Sie sind der natürlichen Suche bei weitem nicht nahe gekommen. Das wäre ziemlich schwierig und würde viele Details erfordern. Wenn Sie die vom Windows Explorer verwendete Sortierreihenfolge verwenden möchten, wissen Sie, dass es einen einfachen API-Aufruf gibt, der dies ermöglicht?
David Heffernan