Apakah ada fungsi bawaan untuk string natural sort?

281

Menggunakan Python 3.x, saya memiliki daftar string yang ingin saya lakukan semacam abjad alami.

Sortir alami: Urutan di mana file dalam Windows diurutkan.

Misalnya, daftar berikut ini diurutkan secara alami (apa yang saya inginkan):

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

Dan inilah versi "diurutkan" dari daftar di atas (apa yang saya miliki):

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

Saya mencari fungsi sortir yang berperilaku seperti yang pertama.

berliku-liku
sumber
13
Definisi semacam alami bukanlah "urutan Windows mengurutkan file".
Glenn Maynard
Semua jawaban di situs ini akan menghasilkan hasil yang salah jika Anda ingin pengurutan 'Windows-Explorer-like' dalam beberapa kasus, misalnya pengurutan !1, 1, !a, a. Satu-satunya cara untuk mendapatkan pengurutan seperti Windows adalah dengan menggunakan StrCmpLogicalW fungsi Windows itu sendiri, karena tidak ada yang tampaknya telah mengimplementasikan kembali fungsi ini dengan benar (sumber akan dihargai). Solusi: stackoverflow.com/a/48030307/2441026
user136036

Jawaban:

235

Ada perpustakaan pihak ketiga untuk ini di PyPI yang disebut natsort (pengungkapan penuh, saya penulis paket). Untuk kasus Anda, Anda dapat melakukan salah satu dari yang berikut:

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

Anda harus mencatat bahwa natsortmenggunakan algoritma umum sehingga harus bekerja untuk hampir semua input yang Anda gunakan. Jika Anda ingin lebih detail tentang mengapa Anda dapat memilih perpustakaan untuk melakukan ini daripada menggulirkan fungsi Anda sendiri, periksa halaman natsortdokumentasi Cara Kerja , khususnya Kasus Khusus Di Mana Saja! bagian.


Jika Anda membutuhkan kunci penyortiran alih-alih fungsi penyortiran, gunakan salah satu dari rumus di bawah ini.

>>> 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
sumber
5
Saya juga berpikir itu cukup menarik bahwa natsort juga agak benar ketika jumlahnya tidak di akhir: seperti itu sering terjadi untuk nama file. Jangan ragu untuk memasukkan contoh berikut: pastebin.com/9cwCLdEK
Martin Thoma
1
Natsort adalah perpustakaan yang hebat, harus ditambahkan ke perpustakaan standar python! :-)
Mitch McMabers
natsortjuga 'secara alami' menangani kasus beberapa angka yang terpisah dalam string. Barang bagus!
FlorianH
182

Coba ini:

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)

Keluaran:

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

Kode diadaptasi dari sini: Penyortiran untuk Manusia: Urutan Urut Alami .

Mark Byers
sumber
2
mengapa Anda menggunakan return sorted(l, key)bukan l.sort(key)? Apakah itu untuk peningkatan kinerja atau hanya untuk menjadi lebih pythonic?
jperelli
12
@ Jeffelli Saya pikir tangga akan mengubah daftar asli di pemanggil. Tetapi kemungkinan besar penelepon menginginkan salinan dangkal dari daftar tersebut.
huggie
3
Sebagai catatan saja, ini tidak dapat menangani semua input: str / int splits harus sejajar, jika tidak Anda akan membuat perbandingan seperti ["foo", 0] <[0, "foo"] untuk input ["foo0 "," 0foo "], yang memunculkan TypeError.
user19087
4
@ user19087: Sebenarnya itu berhasil, karena re.split('([0-9]+)', '0foo')kembali ['', '0', 'foo']. Karena itu, string akan selalu berada pada indeks genap dan bilangan bulat pada indeks ganjil dalam array.
Florian Kusche
Bagi siapa pun yang bertanya-tanya tentang kinerja, ini lebih lambat dari jenis asli python. yaitu 25 -50x lebih lambat. Dan jika Anda ingin selalu mengurutkan [elm1, elm2, Elm2, elm2] sebagai [elm1, Elm2, elm2, elm2] andal (tutup sebelum lebih rendah), maka Anda cukup memanggil natural_sort (diurutkan (pertama)). Lebih tidak efisien, tetapi sangat mudah untuk mendapatkan pengulangan. Kompilasi regex untuk speedup ~ 50%. seperti yang terlihat dalam jawaban Claudiu.
Charlie Haley
100

Berikut versi Mark Byer yang jauh lebih pythonic:

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

Sekarang fungsi ini dapat digunakan sebagai kunci dalam fungsi yang menggunakan itu, seperti list.sort, sorted, max, dll

Sebagai lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Claudiu
sumber
9
re module mengkompilasi dan cache regex secara otomatis, jadi tidak perlu mengkompilasi ulang
wim
1
@ wim: ini meng-cache penggunaan X terakhir, jadi secara teknis dimungkinkan untuk menggunakan X + 5 regex dan kemudian melakukan pengurutan alami berulang-ulang, pada titik mana ini tidak akan di-cache. tetapi mungkin diabaikan dalam jangka panjang
Claudiu
Saya tidak melakukannya, tapi mungkin alasannya adalah tidak bisa menangani tupel, seperti jenis python biasa.
The Unfun Cat
1
Penggunaan X yang disebutkan oleh @Claudiu tampaknya 100 pada Python 2.7 dan 512 pada Python 3.4. Dan juga perhatikan bahwa ketika batas tercapai cache sepenuhnya dihapus (jadi bukan hanya yang tertua yang dibuang).
Zitrax
@Zitrax Mengapa / Bagaimana masuk akal untuk menghapus cache sepenuhnya?
Joschua
19

Saya menulis sebuah fungsi berdasarkan http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html yang menambahkan kemampuan untuk tetap meneruskan parameter 'kunci' Anda sendiri. Saya memerlukan ini untuk melakukan semacam daftar alami yang berisi objek yang lebih kompleks (bukan hanya string).

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)

Sebagai contoh:

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
sumber
cara yang lebih sederhana untuk melakukan ini adalah dengan mendefinisikan natural_sort_key, dan kemudian ketika menyortir daftar Anda dapat melakukan rantai kunci Anda, misalnya:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Mari menganalisis data. Kapasitas digit semua elemen adalah 2. Dan ada 3 huruf dalam bagian literal yang sama 'elm'.

Jadi, panjang maksimal elemen adalah 5. Kita dapat meningkatkan nilai ini untuk memastikan (misalnya, menjadi 8).

Mengingat hal itu, kami punya solusi satu baris:

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

tanpa ekspresi reguler dan perpustakaan eksternal!

print(data)

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

Penjelasan:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
SergO
sumber
1
Ini tidak menangani data panjang dinamis / tidak diketahui. Ini juga menyortir berbeda dari solusi lain untuk data yang memiliki angka di dalam data yang ditentang pada akhirnya. * Ini tidak selalu tidak diinginkan tetapi saya pikir itu baik untuk ditunjukkan.
JerodG
1
Jika Anda perlu menangani data panjang dinamis yang dapat Anda gunakan width = max(data, key=len)untuk menghitung apa yang harus 8dimasukkan untuk di atas dan kemudian memasukkannya ke dalam format string dengan'{0:0>{width}}'.format(x, width=width)
roganartu
1
Hanya dengan melakukan tes waktunya dibandingkan dengan yang lain di forum ini, solusi ini sejauh ini tercepat dan paling efisien untuk jenis data yang sedang diproses oleh @snakile
SR Colledge
13

Diberikan:

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

Mirip dengan solusi SergO, 1-liner tanpa perpustakaan eksternal adalah :

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

atau

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

Penjelasan:

Solusi ini menggunakan fitur kunci pengurutan untuk menentukan fungsi yang akan digunakan untuk pengurutan. Karena kita tahu bahwa setiap entri data didahului oleh 'elm', fungsi penyortiran dikonversi menjadi integer bagian dari string setelah karakter ke-3 (yaitu int (x [3:])). Jika bagian numerik dari data berada di lokasi yang berbeda, maka bagian fungsi ini harus berubah.

Bersulang

Camilo
sumber
6
Dan sekarang untuk sesuatu yang lebih * elegan (pythonic) -hanya sentuhan

Ada banyak implementasi di luar sana, dan sementara beberapa telah mendekati, tidak ada yang cukup menangkap keanggunan python modern.

  • Diuji menggunakan python (3.5.1)
  • Termasuk daftar tambahan untuk menunjukkan bahwa itu berfungsi ketika angka-angka adalah string tengah
  • Tidak menguji, bagaimanapun, saya berasumsi bahwa jika daftar Anda cukup besar akan lebih efisien untuk mengkompilasi regex sebelumnya
    • Saya yakin seseorang akan mengoreksi saya jika ini adalah asumsi yang salah

Cepat
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)])
Kode Lengkap
#!/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']

Perhatian saat menggunakan

  • from os.path import split
    • Anda perlu membedakan impor

Inspirasi dari

JerodG
sumber
6

Nilai Posting Ini

Maksud saya adalah menawarkan solusi non regex yang dapat diterapkan secara umum.
Saya akan membuat tiga fungsi:

  1. find_first_digityang saya pinjam dari @AnuragUniyal . Ini akan menemukan posisi digit pertama atau non-digit dalam sebuah string.
  2. split_digitsyang merupakan generator yang mengambil string menjadi potongan digit dan non digit. Ini juga akan yieldbilangan bulat ketika angka.
  3. natural_keyhanya membungkus split_digitsmenjadi tuple. Ini adalah apa yang kita gunakan sebagai kunci untuk sorted, max, min.

Fungsi

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

Kita dapat melihat bahwa itu adalah umum bahwa kita dapat memiliki beberapa digit potongan:

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

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

Atau biarkan sensitif huruf:

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

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

Kita dapat melihat bahwa ia mengurutkan daftar OP dalam urutan yang sesuai

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

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

Tetapi ia dapat menangani daftar yang lebih rumit juga:

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

Setara dengan regex saya adalah

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))
piRquared
sumber
1
Terima kasih banyak! Namun saya ingin menambahkan, bahwa jika Anda memiliki "12345_A" dan "12345_A2", yang terakhir akan disortir sebelum yang pertama. Setidaknya ini bukan bagaimana Windows melakukannya. Masih bekerja untuk masalah di atas!
morph3us
4

Salah satu opsi adalah mengubah string menjadi tuple dan mengganti digit menggunakan form diperluas http://wiki.answers.com/Q/What_does_expanded_form_mean

dengan cara itu a90 akan menjadi ("a", 90,0) dan a1 akan menjadi ("a", 1)

di bawah ini adalah beberapa kode sampel (yang tidak terlalu efisien karena cara ini menghilangkan 0 dari angka-angka terkemuka)

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)

keluaran:

('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']
raja robert
sumber
1
Sayangnya, solusi ini hanya berfungsi untuk Python 2.X. Untuk Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)akan kembaliTypeError: unorderable types: int() < str()
SethMMorton
@SethMMorgon benar, kode ini mudah dipecah dalam Python 3. Alternatif alami akan terlihat natsort, pypi.org/project/natsort
FlorianH
3

Berdasarkan jawaban di sini, saya menulis natural_sortedfungsi yang berperilaku seperti fungsi bawaan 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)

Kode sumber juga tersedia di repositori cuplikan GitHub saya: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Benjamin Drung
sumber
2

Jawaban di atas baik untuk contoh spesifik yang ditunjukkan, tetapi kehilangan beberapa kasus berguna untuk pertanyaan yang lebih umum tentang jenis alami. Saya hanya mendapat sedikit dari salah satu kasus itu, jadi menciptakan solusi yang lebih menyeluruh:

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

Kode uji dan beberapa tautan (mematikan dan menghidupkan StackOverflow) ada di sini: http://productarchitect.com/code/better-natural-sort.py

Umpan balik. Itu tidak dimaksudkan untuk menjadi solusi yang pasti; hanya selangkah ke depan.

Scott Lawton
sumber
Dalam skrip pengujian yang Anda tautkan, natsorteddan humansortedgagal karena salah digunakan ... Anda mencoba meneruskan natsortedsebagai kunci tetapi sebenarnya fungsi penyortiran itu sendiri. Anda harus mencoba natsort_keygen().
SethMMorton
2

Kemungkinan besar functools.cmp_to_key()terkait erat dengan implementasi yang mendasari jenis python. Selain itu, parameter cmp adalah lawas. Cara modern adalah mengubah item input menjadi objek yang mendukung operasi perbandingan kaya yang diinginkan.

Di bawah CPython 2.x, objek dengan tipe yang berbeda dapat dipesan bahkan jika masing-masing operator pembanding yang kaya belum diterapkan. Di bawah CPython 3.x, objek dari tipe yang berbeda harus secara eksplisit mendukung perbandingan. Lihat Bagaimana Python membandingkan string dan int? yang menghubungkan ke dokumentasi resmi . Sebagian besar jawaban tergantung pada pemesanan tersirat ini. Beralih ke Python 3.x akan membutuhkan tipe baru untuk mengimplementasikan dan menyatukan perbandingan antara angka dan string.

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

Ada tiga pendekatan berbeda. Yang pertama menggunakan kelas bersarang untuk mengambil keuntungan dari Iterablealgoritma perbandingan Python . Yang kedua membuka gulungan sarang ini ke dalam satu kelas. Foregoes forcloes ketiga subclassing struntuk fokus pada kinerja. Semua diatur waktunya; yang kedua dua kali lebih cepat sedangkan yang ketiga hampir enam kali lebih cepat. Subclassing strtidak diperlukan, dan mungkin ide yang buruk sejak awal, tetapi memang disertai dengan kenyamanan tertentu.

Sortir karakter digandakan untuk memaksa pemesanan berdasarkan kasus, dan bertukar kasus untuk memaksa huruf kecil untuk mengurutkan terlebih dahulu; ini adalah definisi khas "jenis alami". Saya tidak bisa memutuskan jenis pengelompokan; beberapa mungkin lebih suka yang berikut ini, yang juga membawa manfaat kinerja yang signifikan:

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

Jika digunakan, operator pembanding diatur objectagar tidak diabaikan olehfunctools.total_ordering operator .

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

Penyortiran alami cukup rumit dan samar-samar didefinisikan sebagai masalah. Jangan lupa untuk menjalankan unicodedata.normalize(...)terlebih dahulu, dan mempertimbangkan penggunaan str.casefold()daripada str.lower(). Mungkin ada masalah penyandian halus yang belum saya pertimbangkan. Jadi saya sementara merekomendasikan perpustakaan natsort . Aku melirik sekilas repositori github; pemeliharaan kode telah menjadi bintang.

Semua algoritme yang saya lihat bergantung pada trik seperti menggandakan dan menurunkan karakter, dan mengganti huruf besar-kecil. Meskipun ini menggandakan waktu berjalan, sebuah alternatif akan membutuhkan pemesanan alami total pada set karakter input. Saya tidak berpikir ini adalah bagian dari spesifikasi unicode, dan karena ada lebih banyak digit unicode daripada itu [0-9], membuat penyortiran seperti itu akan sama menakutkannya. Jika Anda ingin perbandingan sadar-lokal, siapkan string Anda dengan locale.strxfrmper Penyortiran Python CARA .

pengguna19087
sumber
1

Izinkan saya mengirimkan pendapat saya sendiri tentang kebutuhan ini:

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

Sekarang jika kita memiliki daftar seperti ini:

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

Kita cukup menggunakan key=kwarg untuk melakukan pengurutan alami:

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

Kelemahan di sini tentu saja, seperti sekarang, fungsinya akan mengurutkan huruf besar sebelum huruf kecil.

Saya akan menyerahkan implementasi dari case-insenstive grouper kepada pembaca :-)

pepoluan
sumber
0

Saya sarankan Anda cukup menggunakan keyargumen kata kunci sorteduntuk mencapai daftar yang Anda inginkan
Misalnya:

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
sumber
1
ini tidak menangani angka. a_51akan menjadi setelah a500, meskipun 500> 51
skjerns
Benar, jawaban saya hanya cocok dengan contoh yang diberikan Elm11 dan elm1. Kehilangan permintaan untuk jenis alami dan jawaban yang ditandai mungkin yang terbaik di sini :)
Johny Vaknin
0

Mengikuti jawaban @Mark Byers, berikut adalah adaptasi yang menerima keyparameter, dan lebih sesuai dengan PEP8.

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)

Saya juga membuat Intisari

edouardtheron
sumber
(-1) jawaban ini tidak membawa sesuatu yang baru dibandingkan dengan Mark (setiap linter dapat PEP8-ify beberapa kode). Atau mungkin keyparameternya? Tapi ini juga dicontohkan dalam jawaban @ beauburrier
Ciprian Tomoiagă
0

Peningkatan pada perbaikan Claudiu pada jawaban Mark Byer ;-)

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)

BTW, mungkin tidak semua orang ingat bahwa argumen fungsi dievaluasi pada defwaktunya

Walter Tross
sumber
-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

Ucapan Terima Kasih :

Pekerjaan Rumah Sortir Bubble

Cara membaca string satu huruf setiap kali dalam python

Varadaraju G
sumber
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
sumber
4
Implementasi Anda hanya menyelesaikan masalah angka. Implementasi gagal jika string tidak memiliki angka di dalamnya. Cobalah di ['diam', 'hantu'] misalnya (daftar indeks di luar jangkauan).
Goyang
2
@ snaklie: pertanyaan Anda gagal memberikan contoh kasus yang layak. Anda belum menjelaskan apa yang Anda coba lakukan, dan Anda juga tidak memperbarui pertanyaan Anda dengan informasi baru ini. Anda belum memposting apa pun yang telah Anda coba, jadi tolong jangan terlalu meremehkan upaya telepati saya.
SilentGhost
5
@ SilentGhost: Pertama, saya memberi Anda upvote karena saya pikir jawaban Anda berguna (meskipun itu tidak menyelesaikan masalah saya). Kedua, saya tidak bisa membahas semua kasus yang mungkin dengan contoh. Saya pikir saya sudah memberikan definisi yang cukup jelas untuk jenis alami. Saya tidak berpikir itu ide yang baik untuk memberikan contoh yang kompleks atau definisi yang panjang untuk konsep yang begitu sederhana. Anda dapat mengedit pertanyaan saya jika Anda dapat memikirkan rumusan yang lebih baik untuk masalah ini.
Goyang
1
@ SilentGhost: Saya ingin berurusan dengan string seperti itu dengan cara yang sama Windows berurusan dengan nama file seperti itu ketika mengurutkan file dengan nama (abaikan case, dll). Tampaknya jelas bagi saya, tetapi apa pun yang saya katakan tampak jelas bagi saya, jadi saya tidak menilai apakah itu jelas atau tidak.
berliku
1
@snakile Anda tidak menemukan tempat yang dekat dengan pencarian alami. Itu akan sangat sulit untuk dilakukan dan akan membutuhkan banyak detail. Jika Anda ingin urutan yang digunakan oleh windows explorer apakah Anda tahu bahwa ada panggilan api sederhana yang menyediakan ini?
David Heffernan