Có một chức năng được xây dựng cho sắp xếp chuỗi tự nhiên?


281

Sử dụng Python 3.x, tôi có một danh sách các chuỗi mà tôi muốn thực hiện sắp xếp theo thứ tự chữ cái tự nhiên.

Sắp xếp tự nhiên: Thứ tự sắp xếp các tệp trong Windows.

Chẳng hạn, danh sách sau đây được sắp xếp một cách tự nhiên (những gì tôi muốn):

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

Và đây là phiên bản "được sắp xếp" của danh sách trên (những gì tôi có):

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

Tôi đang tìm kiếm một chức năng sắp xếp giống như chức năng đầu tiên.


13
Định nghĩa của một loại tự nhiên không phải là "thứ tự Windows sắp xếp các tệp".
Glenn Maynard


Tất cả các câu trả lời trên trang web này sẽ tạo ra kết quả không chính xác nếu bạn muốn sắp xếp 'giống như Windows-Explorer' trong một số trường hợp, ví dụ như sắp xếp !1, 1, !a, a. Cách duy nhất để có được sự sắp xếp như Windows dường như là sử dụng StrCmpLogicalW chính chức năng của Windows , vì dường như không ai đã thực hiện lại chức năng này một cách chính xác (nguồn sẽ được đánh giá cao). Giải pháp: stackoverflow.com/a/48030307/2441026
user136036

Câu trả lời:


235

Có một thư viện bên thứ ba cho điều này trên PyPI được gọi là natsort (tiết lộ đầy đủ, tôi là tác giả của gói). Đối với trường hợp của bạn, bạn có thể thực hiện một trong các cách sau:

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

Bạn nên lưu ý rằng natsortsử dụng một thuật toán chung để nó hoạt động cho bất kỳ đầu vào nào bạn ném vào nó. Nếu bạn muốn biết thêm chi tiết về lý do tại sao bạn có thể chọn thư viện để thực hiện việc này thay vì thực hiện chức năng của riêng mình, hãy xem trang Cách thức hoạt độngnatsort của tài liệu , đặc biệt là các Trường hợp đặc biệt ở mọi nơi! phần.


Nếu bạn cần một khóa sắp xếp thay vì chức năng sắp xếp, hãy sử dụng một trong các công thức dưới đây.

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

5
Tôi cũng nghĩ khá thú vị khi natsort cũng sắp xếp chính xác khi số không ở cuối: như thường là tên của các tên tệp. Vui lòng bao gồm ví dụ sau: pastebin.com/9cwCLdEK
Martin Thoma

1
Natsort là một thư viện tuyệt vời, nên được thêm vào thư viện chuẩn python! :-)
Mitch McMabers

natsortcũng 'tự nhiên' xử lý trường hợp nhiều số riêng biệt trong chuỗi. Công cụ tuyệt vời!
FlorianH

182

Thử cái này:

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)

Đầu ra:

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

Mã được điều chỉnh từ đây: Sắp xếp cho con người: Thứ tự sắp xếp tự nhiên .


2
Tại sao bạn sử dụng return sorted(l, key)thay vì l.sort(key)? Là cho bất kỳ tăng hiệu suất hoặc chỉ để được nhiều pythonic?
jperelli

12
@jperelli Tôi nghĩ rằng thang sẽ thay đổi danh sách ban đầu trong người gọi. Nhưng rất có thể người gọi muốn một bản sao nông khác của danh sách.
huggie

3
Chỉ dành cho bản ghi, điều này không thể xử lý tất cả các đầu vào: các phân chia str / int phải xếp hàng, nếu không, bạn sẽ tạo các so sánh như ["foo", 0] <[0, "foo"] cho đầu vào ["foo0 "," 0foo "], làm tăng TypeError.
user19087

4
@ user19087: Trong thực tế, nó hoạt động, bởi vì re.split('([0-9]+)', '0foo')trả về ['', '0', 'foo']. Do đó, các chuỗi sẽ luôn nằm trên các chỉ mục và số nguyên chẵn trên các chỉ mục lẻ trong mảng.
Florian Kusche

Đối với bất kỳ ai thắc mắc về hiệu suất, điều này chậm hơn đáng kể so với loại bản địa của python. tức là chậm hơn 25 -50x. Và nếu bạn muốn luôn luôn sắp xếp [elm1, elm2, Elm2, elm2] là [elm1, Elm2, elm2, elm2] một cách đáng tin cậy (mũ trước thấp hơn), thì bạn có thể chỉ cần gọi Natural_sort (được sắp xếp (lst)). Không hiệu quả hơn, nhưng rất dễ dàng để có được một loại lặp lại. Biên dịch regex để tăng tốc ~ 50%. như đã thấy trong câu trả lời của Claudiu.
Charlie Haley

98

Đây là phiên bản pythonic nhiều hơn của câu trả lời của Mark Byer:

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

Bây giờ chức năng này có thể được sử dụng như một chìa khóa trong bất kỳ chức năng sử dụng nó, như list.sort, sorted, maxvv

Là một lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]

9
re-module biên dịch và lưu trữ regexes tự động, do đó không cần phải biên dịch trước
wim

1
@wim: nó lưu trữ các lần sử dụng X cuối cùng, do đó về mặt kỹ thuật có thể sử dụng các biểu thức X + 5 và sau đó thực hiện một cách tự nhiên nhiều lần, tại thời điểm này sẽ không được lưu vào bộ nhớ cache. nhưng có lẽ không đáng kể trong thời gian dài
Claudiu

Tôi đã không làm điều đó, nhưng có lẽ lý do là nó không thể xử lý các bộ dữ liệu, giống như một loại trăn thông thường.
Mèo Unun

1
Các tập quán X được đề cập bởi @Claudiu dường như là 100 trên Python 2.7 và 512 trên Python 3.4. Và cũng lưu ý rằng khi đạt đến giới hạn, bộ đệm sẽ bị xóa hoàn toàn (vì vậy nó không chỉ là cái cũ nhất bị loại bỏ).
Zitrax

@Zitrax Tại sao / Làm thế nào có ý nghĩa để xóa hoàn toàn bộ đệm?
Joschua

19

Tôi đã viết một hàm dựa trên http: //www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html để thêm khả năng vẫn truyền vào tham số 'khóa' của riêng bạn. Tôi cần điều này để thực hiện một loại danh sách tự nhiên có chứa các đối tượng phức tạp hơn (không chỉ các chuỗi).

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)

Ví dụ:

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

một cách đơn giản hơn để làm điều này sẽ là xác định natural_sort_key, và sau đó khi sắp xếp một danh sách bạn có thể thực hiện xâu chuỗi các khóa của mình, ví dụ:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu

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

Hãy phân tích dữ liệu. Dung lượng chữ số của tất cả các yếu tố là 2. Và có 3 chữ cái trong phần nghĩa đen chung 'elm'.

Vì vậy, độ dài tối đa của phần tử là 5. Chúng ta có thể tăng giá trị này để đảm bảo (ví dụ: lên 8).

Nhớ rằng, chúng tôi đã có một giải pháp một dòng:

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

không có biểu thức chính quy và thư viện bên ngoài!

print(data)

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

Giải trình:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13

1
Điều này không xử lý dữ liệu chiều dài động / không xác định. Nó cũng sắp xếp khác với các giải pháp khác cho dữ liệu có số trong dữ liệu trái ngược với cuối. * Điều này không nhất thiết là không mong muốn nhưng tôi nghĩ thật tốt khi chỉ ra.
JerodG

1
Nếu bạn cần xử lý dữ liệu độ dài động, bạn có thể sử dụng width = max(data, key=len)để tính toán những gì cần đăng ký cho phần 8trên và sau đó thêm nó vào chuỗi định dạng với'{0:0>{width}}'.format(x, width=width)
roganartu

1
Chỉ bằng cách thực hiện một bài kiểm tra đúng thời gian so với tất cả các bài kiểm tra khác trên diễn đàn này, giải pháp này nhanh nhất và hiệu quả nhất cho loại dữ liệu @snakile đang cố xử lý
SR Colledge

13

Được:

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

Tương tự như giải pháp của SergO, 1 lớp lót không có thư viện bên ngoài sẽ là :

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

hoặc là

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

Giải trình:

Giải pháp này sử dụng tính năng chính của sắp xếp để xác định chức năng sẽ được sử dụng để sắp xếp. Bởi vì chúng tôi biết rằng mọi mục nhập dữ liệu được bắt đầu bằng 'elm', hàm sắp xếp sẽ chuyển thành số nguyên của chuỗi sau ký tự thứ 3 (tức là int (x [3:])). Nếu phần số của dữ liệu ở một vị trí khác, thì phần này của hàm sẽ phải thay đổi.

Chúc mừng


6
Và bây giờ cho một cái gì đó * thanh lịch hơn (pythonic) - điều chỉnh một liên lạc

Có rất nhiều triển khai ngoài kia, và trong khi một số đã đến gần, không ai thực hiện được các mối quan hệ trăn hiện đại thanh lịch.

  • Đã thử nghiệm bằng python (3.5.1)
  • Bao gồm một danh sách bổ sung để chứng minh rằng nó hoạt động khi các số ở giữa chuỗi
  • Tuy nhiên, đã không kiểm tra, tôi cho rằng nếu danh sách của bạn có kích thước lớn thì việc biên dịch regex trước sẽ hiệu quả hơn
    • Tôi chắc chắn ai đó sẽ sửa tôi nếu đây là một giả định sai lầm

Nhanh lên
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)])
Mã đầy đủ
#!/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']

Thận trọng khi sử dụng

  • from os.path import split
    • bạn sẽ cần phân biệt hàng nhập khẩu

Cảm hứng từ


6

Giá trị của bài này

Quan điểm của tôi là đưa ra một giải pháp không chính quy có thể được áp dụng chung.
Tôi sẽ tạo ba chức năng:

  1. find_first_digitmà tôi đã mượn từ @AnuragUniyal . Nó sẽ tìm vị trí của chữ số đầu tiên hoặc không chữ số trong một chuỗi.
  2. split_digitsđó là một trình tạo chọn một chuỗi thành các phần chữ số và không chữ số. Nó cũng sẽ là yieldsố nguyên khi nó là một chữ số.
  3. natural_keychỉ kết thúc tốt đẹp split_digitsvào tuple. Đây là những gì chúng tôi sử dụng như một chìa khóa cho sorted, max, min.

Chức năng

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

Chúng ta có thể thấy rằng nói chung là chúng ta có thể có nhiều khối chữ số:

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

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

Hoặc để lại như trường hợp nhạy cảm:

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

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

Chúng ta có thể thấy rằng nó sắp xếp danh sách của OP theo thứ tự phù hợp

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

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

Nhưng nó cũng có thể xử lý các danh sách phức tạp hơn:

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

Tương đương regex của tôi sẽ là

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

1
Cảm ơn rất nhiều! Tuy nhiên, tôi muốn thêm rằng nếu bạn có "12345_A" và "12345_A2", thì cái sau sẽ được sắp xếp trước cái đầu tiên. Điều này ít nhất không phải là cách Windows làm điều đó. Vẫn hoạt động cho vấn đề trên, mặc dù!
morph3us

4

Một tùy chọn là biến chuỗi thành một tuple và thay thế các chữ số bằng hình thức mở rộng http://wiki.answers.com/Q/What_does_Exanded_form_mean

theo cách đó a90 sẽ trở thành ("a", 90,0) và a1 sẽ trở thành ("a", 1)

bên dưới là một số mã mẫu (không hiệu quả do cách loại bỏ số 0 đứng đầu khỏi số)

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)

đầu ra:

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

1
Thật không may, giải pháp này chỉ hoạt động cho Python 2.X. Đối với Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)sẽ trở lạiTypeError: unorderable types: int() < str()
SethMMorton

@SethMMorgon đã đúng, mã này dễ dàng bị phá vỡ trong Python 3. Sự thay thế tự nhiên dường như natsort, pypi.org/project/natsort
FlorianH

3

Dựa trên các câu trả lời ở đây, tôi đã viết một natural_sortedhàm hoạt động giống như hàm tích hợp sorted:

# Copyright (C) 2018, Benjamin Drung <bdrung@posteo.de>
#
# 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)

Mã nguồn cũng có sẵn trong kho lưu trữ đoạn trích GitHub của tôi: https://github.com/bdrung/snippets/blob/master/natural_sortic.py


2

Các câu trả lời trên là tốt cho ví dụ cụ thể đã được hiển thị, nhưng bỏ lỡ một số trường hợp hữu ích cho câu hỏi chung hơn về sắp xếp tự nhiên. Tôi chỉ nhận được một trong những trường hợp đó, vì vậy đã tạo ra một giải pháp kỹ lưỡng hơn:

def natural_sort_key(string_or_number):
    """
    by Scott S. Lawton <scott@ProductArchitect.com> 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

Mã kiểm tra và một số liên kết (bật và tắt StackOverflow) có tại đây: http://productarchitect.com/code/better-natural-sort.py

Phản hồi chào mừng. Đó không phải là một giải pháp dứt khoát; chỉ là một bước tiến


Trong tập lệnh thử nghiệm mà bạn liên kết natsortedhumansortedkhông thành công vì chúng được sử dụng không chính xác ... bạn đã cố gắng vượt qua natsortednhư một khóa nhưng thực chất đó là chức năng sắp xếp. Bạn nên thử natsort_keygen().
SethMMorton

2

Rất có thể functools.cmp_to_key()được liên kết chặt chẽ với việc thực hiện cơ bản của loại trăn. Bên cạnh đó, tham số cmp là di sản. Cách hiện đại là biến đổi các mục đầu vào thành các đối tượng hỗ trợ các hoạt động so sánh phong phú mong muốn.

Theo CPython 2.x, các đối tượng của các loại khác nhau có thể được đặt hàng ngay cả khi các toán tử so sánh phong phú tương ứng chưa được triển khai. Theo CPython 3.x, các đối tượng thuộc các loại khác nhau phải hỗ trợ rõ ràng cho việc so sánh. Xem Python so sánh chuỗi và int như thế nào? có liên kết đến các tài liệu chính thức . Hầu hết các câu trả lời phụ thuộc vào thứ tự ngầm này. Chuyển sang Python 3.x sẽ yêu cầu một loại mới để thực hiện và thống nhất so sánh giữa các số và chuỗi.

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

Có ba cách tiếp cận khác nhau. Đầu tiên sử dụng các lớp lồng nhau để tận dụng Iterablethuật toán so sánh của Python . Thứ hai bỏ điều này lồng vào một lớp duy nhất. Nhóm thứ ba bỏ qua phân lớp strđể tập trung vào hiệu suất. Tất cả đều được tính thời gian; cái thứ hai nhanh gấp đôi trong khi cái thứ ba nhanh hơn gần sáu lần. Phân lớp strkhông bắt buộc, và có lẽ là một ý tưởng tồi ở nơi đầu tiên, nhưng nó đi kèm với một số tiện lợi nhất định.

Các ký tự sắp xếp được nhân đôi để buộc thứ tự theo trường hợp và hoán đổi trường hợp để buộc chữ thường viết thường sắp xếp trước; đây là định nghĩa điển hình của "sắp xếp tự nhiên". Tôi không thể quyết định loại nhóm; một số có thể thích những điều sau đây, cũng mang lại lợi ích hiệu suất đáng kể:

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

Khi được sử dụng, các toán tử so sánh được đặt thành đó objectđể chúng không bị bỏ quafunctools.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

Sắp xếp tự nhiên là khá phức tạp và mơ hồ được xác định là một vấn đề. Đừng quên chạy unicodedata.normalize(...)trước và xem xét sử dụng str.casefold()hơn là str.lower(). Có lẽ có những vấn đề mã hóa tinh tế mà tôi chưa xem xét. Vì vậy, tôi tạm thời đề nghị thư viện natsort . Tôi lướt qua kho lưu trữ github; việc bảo trì mã đã được xuất sắc.

Tất cả các thuật toán tôi đã thấy phụ thuộc vào các thủ thuật như sao chép và hạ thấp ký tự và trường hợp hoán đổi. Trong khi điều này tăng gấp đôi thời gian chạy, một sự thay thế sẽ yêu cầu tổng thứ tự tự nhiên trên bộ ký tự đầu vào. Tôi không nghĩ rằng đây là một phần của đặc tả unicode và vì có nhiều chữ số unicode hơn [0-9], việc tạo ra một cách sắp xếp như vậy sẽ gây khó khăn không kém. Nếu bạn muốn so sánh nhận biết miền địa phương, hãy chuẩn bị các chuỗi của bạn với locale.strxfrmmỗi CÁCH Sắp xếp của Python .


1

Hãy để tôi tự đưa ra nhu cầu này:

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

Bây giờ nếu chúng ta có danh sách như vậy:

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

Chúng ta chỉ cần sử dụng key=kwarg để thực hiện một cách tự nhiên:

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

Hạn chế ở đây là tất nhiên, vì bây giờ, hàm sẽ sắp xếp các chữ cái in hoa trước các chữ cái viết thường.

Tôi sẽ để lại việc thực hiện một nhóm cá mú không phân biệt trường hợp cho người đọc :-)


0

Tôi đề nghị bạn chỉ cần sử dụng keyđối số từ khóa sortedđể đạt được danh sách mong muốn của bạn
Ví dụ:

to_order= [e2,E1,e5,E4,e3]
ordered= sorted(to_order, key= lambda x: x.lower())
    # ordered should be [E1,e2,e3,E4,e5]

1
Điều này không xử lý các chữ số. a_51sẽ là sau a500, mặc dù 500> 51
skjerns

Đúng, câu trả lời của tôi chỉ đơn giản phù hợp với ví dụ đã cho của Elm11 và elm1. Bỏ lỡ yêu cầu sắp xếp tự nhiên một cách cụ thể và câu trả lời được đánh dấu có lẽ là câu trả lời hay nhất ở đây :)
Johny Vaknin

0

Theo câu trả lời của @Mark Byers, đây là một điều chỉnh chấp nhận keytham số và tuân thủ PEP8 hơn.

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)

Tôi cũng làm một Gist


(-1) câu trả lời này không mang lại điều gì mới so với Mark (bất kỳ người nói dối nào cũng có thể PEP8-ify một số mã). Hoặc có thể là keytham số? Nhưng điều này cũng được minh họa trong câu trả lời của @ beauburrier
Ciprian Tomoiagă

0

Một cải tiến về sự cải thiện của Claudiu đối với câu trả lời của 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, có lẽ không phải ai cũng nhớ rằng lý lẽ chức năng mặc định được đánh giá tại defthời gian


-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

Lời cảm ơn :

Bong bóng sắp xếp bài tập về nhà

Làm thế nào để đọc một chuỗi một chữ cái tại một thời điểm trong python


-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

4
Việc thực hiện của bạn chỉ giải quyết vấn đề số. Việc thực hiện không thành công nếu các chuỗi không có số trong đó. Ví dụ, hãy thử [[im lặng ',' ma '] (liệt kê danh sách ngoài phạm vi).
snakile

2
@snaklie: câu hỏi của bạn không cung cấp trường hợp ví dụ đàng hoàng. Bạn chưa giải thích những gì bạn đang cố gắng làm và cả bạn đã không cập nhật câu hỏi của mình với thông tin mới này. Bạn chưa đăng bất cứ điều gì bạn đã cố gắng, vì vậy xin đừng từ chối nỗ lực ngoại cảm của tôi.
SilentGhost

5
@SilentGhost: Đầu tiên, tôi đã đưa cho bạn một upvote vì tôi nghĩ câu trả lời của bạn rất hữu ích (mặc dù nó không giải quyết được vấn đề của tôi). Thứ hai, tôi không thể bao gồm tất cả các trường hợp có thể với các ví dụ. Tôi nghĩ rằng tôi đã đưa ra một định nghĩa khá rõ ràng để sắp xếp tự nhiên. Tôi không nghĩ rằng nên đưa ra một ví dụ phức tạp hoặc một định nghĩa dài cho một khái niệm đơn giản như vậy. Bạn có thể chỉnh sửa câu hỏi của tôi nếu bạn có thể nghĩ ra một công thức tốt hơn cho vấn đề.
snakile

1
@SilentGhost: Tôi muốn xử lý các chuỗi như vậy giống như cách Windows xử lý các tên tệp đó khi sắp xếp các tệp theo tên (bỏ qua các trường hợp, v.v.). Nó có vẻ rõ ràng đối với tôi, nhưng bất cứ điều gì tôi nói dường như rõ ràng với tôi, vì vậy tôi không đánh giá liệu nó rõ ràng hay không.
snakile

1
@snakile bạn đã đến gần nơi xác định tìm kiếm tự nhiên. Điều đó sẽ khá khó để làm và sẽ đòi hỏi rất nhiều chi tiết. Nếu bạn muốn thứ tự sắp xếp được sử dụng bởi windows explorer bạn có biết rằng có một cuộc gọi api đơn giản cung cấp điều này không?
David Heffernan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.