มีฟังก์ชันในตัวสำหรับการเรียงลำดับสตริงแบบธรรมชาติหรือไม่?


281

ใช้ Python 3.x ฉันมีรายการสตริงที่ฉันต้องการเรียงลำดับตัวอักษรตามธรรมชาติ

เรียงลำดับตามธรรมชาติ:ลำดับที่ไฟล์ใน Windows เรียงลำดับ

ตัวอย่างเช่นรายการต่อไปนี้จะถูกจัดเรียงตามธรรมชาติ (สิ่งที่ฉันต้องการ):

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

และนี่คือรายการ "เรียงลำดับ" ของรายการด้านบน (สิ่งที่ฉันมี):

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

ฉันกำลังมองหาฟังก์ชั่นการเรียงลำดับซึ่งทำหน้าที่เหมือนฟังก์ชั่นแรก


13
คำจำกัดความของการเรียงลำดับแบบธรรมชาติไม่ใช่ "ลำดับของ Windows ที่เรียงลำดับไฟล์"
Glenn Maynard


ทุกคำตอบในเว็บไซต์นี้จะผลิตที่ไม่ถูกต้องผลถ้าคุณต้องการ'ใช้ Windows Explorer เหมือน'!1, 1, !a, aการเรียงลำดับในหลายกรณีเช่นการเรียงลำดับ วิธีเดียวที่จะทำให้การเรียงลำดับเช่น Windows ดูเหมือนว่าจะใช้StrCmpLogicalW ฟังก์ชั่นWindows ด้วยตัวเองเนื่องจากดูเหมือนว่าไม่มีใครนำฟังก์ชั่นนี้กลับมาใช้ใหม่ได้อย่างถูกต้อง การแก้ไข: stackoverflow.com/a/48030307/2441026
user136036

คำตอบ:


235

มีห้องสมุดบุคคลที่สามสำหรับสิ่งนี้ใน PyPI ที่เรียกว่าnatsort (การเปิดเผยแบบเต็มฉันเป็นผู้เขียนแพ็คเกจ) สำหรับกรณีของคุณคุณสามารถเลือกทำอย่างใดอย่างหนึ่งต่อไปนี้:

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

คุณควรทราบว่าnatsortใช้อัลกอริทึมทั่วไปดังนั้นจึงควรใช้งานได้กับอินพุตที่คุณส่งไป หากคุณต้องการรายละเอียดเพิ่มเติมเกี่ยวกับเหตุผลที่คุณอาจเลือกห้องสมุดการทำเช่นนี้มากกว่ากลิ้งฟังก์ชั่นของคุณเองให้ตรวจสอบnatsortเอกสารเป็นวิธีการทำงานหน้าโดยเฉพาะในคดีพิเศษทุกที่! มาตรา.


หากคุณต้องการคีย์การเรียงลำดับแทนที่จะใช้ฟังก์ชันการเรียงลำดับให้ใช้สูตรด้านล่างอย่างใดอย่างหนึ่ง

>>> 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
ฉันยังคิดว่ามันน่าสนใจทีเดียวที่ natsort จัดเรียงที่ถูกต้องเมื่อจำนวนไม่สิ้นสุด: เช่นบ่อยครั้งที่มันเป็นชื่อไฟล์ อย่าลังเลที่จะใส่ตัวอย่างต่อไปนี้: pastebin.com/9cwCLdEK
Martin Thoma

1
Natsort เป็นห้องสมุดที่ดีควรเพิ่มไปยังไลบรารีมาตรฐานของไพ ธ อน! :-)
Mitch McMabers

natsortยัง 'ตามธรรมชาติ' จัดการกรณีของตัวเลขที่แยกจากกันจำนวนมากในสตริง สิ่งที่ยอดเยี่ยม!
FlorianH

182

ลองสิ่งนี้:

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)

เอาท์พุท:

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

รหัสดัดแปลงมาจากที่นี่: เรียงลำดับสำหรับมนุษย์: เรียงธรรมชาติ


2
ทำไมคุณถึงใช้return sorted(l, key)แทนl.sort(key)? สำหรับประสิทธิภาพที่เพิ่มขึ้นหรือเพื่อเพิ่มความไพเราะมากขึ้น?
jperelli

12
@ jerelli ฉันคิดว่าบันไดจะเปลี่ยนรายการเดิมในผู้โทร แต่ผู้โทรส่วนใหญ่ต้องการสำเนาอีกชุดของรายการที่ตื้น
huggie

3
สำหรับเร็กคอร์ดนี้ไม่สามารถจัดการอินพุตทั้งหมดได้: การแยก str / int ต้องเรียงกันมิฉะนั้นคุณจะสร้างการเปรียบเทียบเช่น ["foo", 0] <[0, "foo"] สำหรับอินพุต ["foo0 "," 0foo "] ซึ่งเพิ่ม TypeError
user19087

4
@ user19087: ในความเป็นจริงมันไม่ทำงานเพราะผลตอบแทนre.split('([0-9]+)', '0foo') ['', '0', 'foo']ด้วยเหตุนี้สตริงจึงจะยังคงอยู่บนดัชนีและเลขจำนวนเต็มบนดัชนีคี่ในอาร์เรย์
Florian Kusche

สำหรับทุกคนที่สงสัยเกี่ยวกับการแสดงนี่เป็นเรื่องที่ช้ากว่าภาษาพื้นเมืองของงูเหลือม นั่นคือ 25 -50x ช้าลง และถ้าคุณต้องการเรียงลำดับ [elm1, elm2, Elm2, elm2] เสมอเป็น [elm1, Elm2, elm2, elm2] อย่างน่าเชื่อถือ (ตัวพิมพ์เล็กลงต่ำกว่า) คุณสามารถเรียก natural_sort (เรียงลำดับ (lst)) ไม่มีประสิทธิภาพมากขึ้น แต่ง่ายมากที่จะได้รับการจัดเรียงซ้ำ คอมไพล์ regex เพื่อเร่งความเร็ว ~ 50% เท่าที่เห็นในคำตอบของ Claudiu
Charlie Haley

100

ต่อไปนี้เป็นคำตอบของ 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)]    

ตอนนี้ฟังก์ชั่นนี้สามารถใช้เป็นกุญแจสำคัญในการทำงานใด ๆ ที่จะใช้มันเช่นlist.sort, sorted,maxฯลฯ

ในฐานะแลมบ์ดา:

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

9
อีก compiles โมดูลและแคช regexes โดยอัตโนมัติดังนั้นไม่มีความจำเป็นที่จะ precompile
Wim

1
@wim: มันเก็บการใช้งาน X ครั้งสุดท้ายดังนั้นจึงเป็นไปได้ในทางเทคนิคที่จะใช้ X + 5 regexes จากนั้นทำการเรียงลำดับตามธรรมชาติซ้ำแล้วซ้ำอีก ณ จุดนี้จะไม่ถูกแคช แต่อาจเล็กน้อยในระยะยาว
Claudiu

ฉันไม่ได้ทำมัน แต่บางทีเหตุผลก็คือมันไม่สามารถจัดการสิ่งอันดับได้เช่นเดียวกับการเรียงงูหลามทั่วไป
Cat Unfun

1
การใช้งาน X ที่ @Claudiu ดูเหมือนจะเป็น100บน Python 2.7 และ512บน Python 3.4 และโปรดทราบว่าเมื่อถึงขีด จำกัด แคชจะถูกล้างอย่างสมบูรณ์ (ดังนั้นจึงไม่ใช่เฉพาะแคชที่เก่าที่สุดที่ถูกโยนทิ้งไป)
Zitrax

@Zitrax เหตุใด / เป็นเหตุผลที่ทำให้การล้างแคชสมบูรณ์หรือไม่
Joschua

19

ฉันเขียนฟังก์ชันตามhttp://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.htmlซึ่งเพิ่มความสามารถในการผ่านพารามิเตอร์ 'key' ของคุณเอง ฉันต้องการสิ่งนี้เพื่อดำเนินการเรียงลำดับรายการตามธรรมชาติที่มีวัตถุที่ซับซ้อนมากขึ้น (ไม่ใช่แค่สตริง)

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)

ตัวอย่างเช่น:

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

วิธีที่ง่ายกว่าในการทำเช่นนี้คือการกำหนดnatural_sort_keyและเมื่อเรียงลำดับรายการคุณสามารถเชื่อมโยงกุญแจของคุณได้เช่น:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu

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

มาวิเคราะห์ข้อมูลกัน ความจุหลักขององค์ประกอบทั้งหมดคือ 2 และมี 3 ตัวอักษรในส่วนที่แท้จริง'elm'ตัวอักษรในส่วนของตัวอักษรที่พบบ่อย

ดังนั้นความยาวสูงสุดขององค์ประกอบคือ 5 เราสามารถเพิ่มค่านี้เพื่อให้แน่ใจ (ตัวอย่างเช่นถึง 8)

จำไว้ว่าเรามีวิธีแก้ปัญหาแบบบรรทัดเดียว:

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

โดยไม่ต้องมีการแสดงออกปกติและห้องสมุดภายนอก!

print(data)

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

คำอธิบาย:

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

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

1
สิ่งนี้ไม่ได้จัดการข้อมูลความยาวแบบไดนามิก / ไม่รู้จัก นอกจากนี้ยังจัดเรียงแตกต่างจากโซลูชันอื่น ๆ สำหรับข้อมูลที่มีตัวเลขภายในข้อมูลซึ่งตรงข้ามกับตอนท้าย * สิ่งนี้ไม่ได้เป็นสิ่งที่ไม่พึงประสงค์ แต่ฉันคิดว่าเป็นการดี
JerodG

1
หากคุณจำเป็นต้องจัดการกับข้อมูลความยาวแบบไดนามิกคุณสามารถใช้width = max(data, key=len)ในการคำนวณสิ่งที่จะย่อยสำหรับ8ข้างต้นแล้วย่อยลงในสตริงรูปแบบด้วย'{0:0>{width}}'.format(x, width=width)
roganartu

1
เพียงแค่ทำการทดสอบหมดเวลาเมื่อเทียบกับคนอื่น ๆ ทั้งหมดในฟอรัมนี้การแก้ปัญหานี้เป็นวิธีที่เร็วที่สุดและมีประสิทธิภาพมากที่สุดสำหรับประเภทของข้อมูล @snakile กำลังพยายามประมวลผล
SR Colledge

13

ได้รับ:

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

เช่นเดียวกับโซลูชันของ SergO, 1 ซับโดยไม่มีไลบรารี่ภายนอกจะเป็น :

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

หรือ

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

คำอธิบาย:

วิธีการแก้ปัญหานี้ใช้คุณสมบัติที่สำคัญของการจัดเรียงเพื่อกำหนดฟังก์ชันที่จะใช้สำหรับการเรียงลำดับ เนื่องจากเรารู้ว่าการป้อนข้อมูลทุกรายการนำหน้าด้วย 'elm' ฟังก์ชันการเรียงลำดับจะแปลงเป็นจำนวนเต็มส่วนของสตริงหลังอักขระที่ 3 (เช่น int (x [3:])) หากส่วนที่เป็นตัวเลขของข้อมูลอยู่ในตำแหน่งที่แตกต่างกันดังนั้นส่วนนี้ของฟังก์ชั่นจะต้องเปลี่ยน

ไชโย


6
และตอนนี้สำหรับบางสิ่งบางอย่าง * หรูหรา (pythonic) - เพียงแค่สัมผัส

มีการติดตั้งใช้งานมากมายและในขณะที่บางคนเข้าใกล้

  • ทดสอบโดยใช้ python (3.5.1)
  • รวมรายการเพิ่มเติมเพื่อแสดงให้เห็นว่ามันทำงานเมื่อตัวเลขเป็นสตริงกลาง
  • ไม่ได้ทดสอบ แต่ฉันสมมติว่าถ้ารายการของคุณมีขนาดใหญ่จะมีประสิทธิภาพมากขึ้นในการรวบรวม regex ล่วงหน้า
    • ฉันแน่ใจว่าบางคนจะแก้ไขฉันหากนี่เป็นข้อสันนิษฐานที่ผิดพลาด

quicky
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)])
เต็มรูปแบบรหัส
#!/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']

ข้อควรระวังเมื่อใช้

  • from os.path import split
    • คุณจะต้องแยกความแตกต่างการนำเข้า

แรงบันดาลใจจาก


6

คุณค่าของโพสต์นี้

จุดของฉันคือการเสนอโซลูชันที่ไม่ใช่ regex ที่สามารถนำไปใช้โดยทั่วไป
ฉันจะสร้างสามฟังก์ชัน:

  1. find_first_digitซึ่งฉันยืมมาจาก@AnuragUniyal @AnuragUniyalมันจะหาตำแหน่งของตัวเลขตัวแรกหรือไม่ใช่ตัวเลขในสตริง
  2. split_digitsซึ่งเป็นตัวกำเนิดที่แยกสตริงออกเป็นส่วน ๆ และไม่ใช่ตัวเลข มันจะเป็นyieldจำนวนเต็มด้วยเมื่อมันเป็นตัวเลข
  3. natural_keyเพียงแค่ตัดเป็นsplit_digits tupleนี่คือสิ่งที่เราใช้เป็นกุญแจสำคัญสำหรับsorted, ,maxmin

ฟังก์ชั่น

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

เราจะเห็นว่ามันเป็นเรื่องธรรมดาที่เราสามารถมีได้หลายหลัก:

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

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

หรือออกจากกรณีที่สำคัญ:

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

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

เราจะเห็นว่ามันเรียงลำดับรายการของ OP ตามลำดับที่เหมาะสม

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

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

แต่สามารถจัดการรายการที่ซับซ้อนได้มากขึ้นเช่นกัน:

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

regex ของฉันจะเทียบเท่า

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
ขอบคุณมาก! ฉันต้องการเพิ่มอย่างไรก็ตามหากคุณมี "12345_A" และ "12345_A2" ตัวหลังจะถูกจัดเรียงก่อนหน้าแรก นี่เป็นอย่างน้อยไม่ใช่วิธีที่ Windows ทำ ยังคงใช้งานได้สำหรับปัญหาข้างต้น แต่!
morph3us

4

ทางเลือกหนึ่งคือเปลี่ยนสตริงให้เป็น tuple และแทนที่ตัวเลขด้วยรูปแบบที่ขยายเพิ่มเติม http://wiki.answers.com/Q/What_does_expanded_form_mean

วิธีนั้นจะกลายเป็น a90 ("a", 90,0) และ a1 จะกลายเป็น ("a", 1)

ด้านล่างคือโค้ดตัวอย่างบางส่วน (ซึ่งไม่ได้มีประสิทธิภาพมากนักเนื่องจากวิธีการลบเลข 0 นำหน้าออกจากตัวเลข)

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)

เอาท์พุท:

('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
น่าเสียดายที่โซลูชันนี้ใช้งานได้กับ Python 2.X เท่านั้น สำหรับ Python 3 ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)จะกลับมาTypeError: unorderable types: int() < str()
SethMMorton

@SethMMorgon ถูกต้องรหัสนี้ได้อย่างง่ายดายแบ่งในหลาม 3. ทางเลือกที่ธรรมชาติจะดูเหมือนnatsort, pypi.org/project/natsort
FlorianH

3

จากคำตอบที่นี่ฉันได้เขียนnatural_sortedฟังก์ชั่นที่ทำหน้าที่เหมือนฟังก์ชั่นในตัว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)

ซอร์สโค้ดยังมีอยู่ในพื้นที่เก็บข้อมูลตัวอย่าง GitHub ของฉัน: https://github.com/bdrung/snippets/blob/master/natural_sorted.py


2

คำตอบข้างต้นเป็นสิ่งที่ดีสำหรับตัวอย่างที่เฉพาะเจาะจงที่แสดง แต่พลาดกรณีที่มีประโยชน์หลายกรณีสำหรับคำถามทั่วไปของการเรียงลำดับที่เป็นธรรมชาติ ฉันเพิ่งได้รับหนึ่งในกรณีเหล่านั้นเพื่อสร้างโซลูชันที่ละเอียดยิ่งขึ้น:

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

รหัสการทดสอบและลิงค์ต่างๆ (เปิดและปิด StackOverflow) อยู่ที่นี่: http://productarchitect.com/code/better-natural-sort.py

ข้อเสนอแนะยินดีต้อนรับ นั่นไม่ได้หมายความว่าจะเป็นทางออกที่ชัดเจน เพียงก้าวไปข้างหน้า


ในสคริปต์ทดสอบที่คุณเชื่อมโยงnatsortedและhumansortedล้มเหลวเนื่องจากมีการใช้อย่างไม่ถูกต้อง ... คุณพยายามส่งผ่านnatsortedเป็นรหัส แต่จริง ๆ แล้วเป็นฟังก์ชันเรียงลำดับ natsort_keygen()คุณควรจะได้พยายาม
SethMMorton

2

ส่วนใหญ่มีแนวโน้มที่functools.cmp_to_key()จะเชื่อมโยงอย่างใกล้ชิดกับการดำเนินการเรียงลำดับของงูหลาม นอกจากนี้พารามิเตอร์cmpเป็นแบบดั้งเดิม วิธีที่ทันสมัยคือการแปลงรายการอินพุตให้เป็นวัตถุที่สนับสนุนการดำเนินการเปรียบเทียบที่ต้องการ

ภายใต้ CPython 2.x คุณสามารถสั่งวัตถุประเภทที่แตกต่างกันได้แม้ว่าตัวดำเนินการเปรียบเทียบการเปรียบเทียบที่เกี่ยวข้องนั้นจะไม่ได้รับการใช้งาน ภายใต้ CPython 3.x วัตถุประเภทต่าง ๆ ต้องสนับสนุนการเปรียบเทียบอย่างชัดเจน ดูPython เปรียบเทียบสตริงและ int อย่างไรซึ่งเชื่อมโยงไปยังเอกสารที่เป็นทางการ คำตอบส่วนใหญ่ขึ้นอยู่กับการสั่งซื้อโดยนัยนี้ การสลับไปใช้ Python 3.x จะต้องใช้รูปแบบใหม่เพื่อนำไปใช้และรวมการเปรียบเทียบระหว่างตัวเลขและสตริง

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

มีสามวิธีที่แตกต่างกัน วิธีแรกใช้คลาสที่ซ้อนกันเพื่อใช้ประโยชน์จากIterableอัลกอริธึมการเปรียบเทียบของ Python ครั้งที่สอง unrolls ซ้อนนี้เป็นชั้นเดียว foregoes ลำดับที่สามstrจะเน้นไปที่ประสิทธิภาพ หมดเวลาแล้ว ที่สองคือเร็วเป็นสองเท่าในขณะที่สามเร็วขึ้นเกือบหกเท่า strไม่จำเป็นต้องใช้การแบ่งคลาสย่อยและอาจเป็นความคิดที่ไม่ดีในตอนแรก แต่มันมาพร้อมกับความสะดวกสบายบางอย่าง

อักขระการเรียงลำดับถูกทำซ้ำเพื่อบังคับให้เรียงลำดับตามตัวพิมพ์ใหญ่และตัวพิมพ์เล็ก - ใหญ่เพื่อบังคับให้ตัวอักษรตัวเล็กพิมพ์เล็กเพื่อเรียงลำดับก่อน นี่คือคำจำกัดความทั่วไปของ "การจัดเรียงตามธรรมชาติ" ฉันไม่สามารถตัดสินใจเกี่ยวกับประเภทของการจัดกลุ่ม; บางคนอาจชอบสิ่งต่อไปนี้ซึ่งนำประโยชน์ด้านประสิทธิภาพมาให้ด้วย:

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

ในกรณีที่ใช้ประกอบการเปรียบเทียบจะถูกกำหนดให้เป็นobjectเพื่อให้พวกเขาจะไม่ได้รับการปฏิเสธโดยfunctools.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

การเรียงลำดับตามธรรมชาตินั้นค่อนข้างซับซ้อนและกำหนดไม่ชัดเจนว่าเป็นปัญหา อย่าลืมที่จะทำงานunicodedata.normalize(...)ก่อนและพิจารณาการใช้งานมากกว่าstr.casefold() str.lower()อาจมีปัญหาการเข้ารหัสที่บอบบางที่ฉันไม่ได้พิจารณา ดังนั้นฉันจึงแนะนำห้องสมุดนัตซอร์อย่างไม่แน่นอน ฉันดูที่ที่เก็บ github อย่างรวดเร็ว การบำรุงรักษารหัสเป็นตัวเอก

อัลกอริธึมทั้งหมดที่ฉันเห็นขึ้นอยู่กับเทคนิคเช่นการทำซ้ำและลดตัวอักษรและการสลับตัวพิมพ์เล็ก ขณะนี้เพิ่มเวลาการทำงานเป็นสองเท่าตัวเลือกอื่นจะต้องมีการเรียงลำดับแบบเป็นธรรมชาติในชุดอักขระอินพุต ฉันไม่คิดว่านี่เป็นส่วนหนึ่งของข้อมูลจำเพาะของ Unicode และเนื่องจากมีตัวเลขหลักของ Unicode มากกว่า[0-9]การสร้างการเรียงลำดับจะน่ากลัวเท่า ๆ กัน หากคุณต้องการเปรียบเทียบรู้จักสถานที่เตรียมความพร้อมสายของคุณที่มีlocale.strxfrmต่อ ธคัด HOW TO


1

ให้ฉันส่งของฉันเองในความต้องการนี้:

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

ตอนนี้ถ้าเรามีรายการเช่น:

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

เราสามารถใช้key=kwarg เพื่อจัดเรียงแบบธรรมชาติ:

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

ข้อเสียเปรียบที่นี่แน่นอนเพราะตอนนี้ฟังก์ชั่นจะเรียงตัวอักษรตัวพิมพ์ใหญ่ก่อนตัวอักษรตัวเล็ก

ฉันจะปล่อยให้การใช้งานปลาเก๋าที่ไม่สนใจกรณีไปยังผู้อ่าน :-)


0

ฉันขอแนะนำให้คุณใช้keyอาร์กิวเมนต์คำหลักของsortedเพื่อให้บรรลุรายการที่คุณต้องการ
ตัวอย่างเช่น

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
นี่ไม่ได้จัดการกับตัวเลข a_51จะเป็นหลังa500แม้ว่า 500> 51
skjerns

จริงคำตอบของฉันตรงกับตัวอย่างที่กำหนดของ Elm11 และ elm1 ไม่ได้รับการร้องขอสำหรับการจัดเรียงแบบธรรมชาติโดยเฉพาะและคำตอบที่ถูกต้องน่าจะเป็นคำตอบที่ดีที่สุดที่นี่ :)
Johny Vaknin

0

ทำตามคำตอบ @Mark Byers ต่อไปนี้คือการปรับเปลี่ยนที่ยอมรับkeyพารามิเตอร์และสอดคล้องกับ 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)

ฉันยังทำแก่นสาร


(-1) คำตอบนี้ไม่ได้นำสิ่งใหม่มาเปรียบเทียบกับเครื่องหมายของมาร์ค (linter ใด ๆ สามารถใช้รหัส PEP8-ify ได้) หรืออาจkeyพารามิเตอร์? แต่นี่เป็นตัวอย่างในคำตอบของ @ beauburrier
Ciprian Tomoiagă

0

การปรับปรุงการปรับปรุง Claudiu ในคำตอบของ 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 บางทีทุกคนอาจจำไม่ได้ว่าค่าเริ่มต้นของฟังก์ชั่นอาร์กิวเมนต์จะได้รับการประเมิน ณdefเวลานั้น


-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

รับทราบ :

Bubble Sort Homework

วิธีอ่านสตริงทีละตัวอักษรในไพ ธ อน


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

4
การใช้งานของคุณจะแก้ปัญหาหมายเลขได้เท่านั้น การใช้งานล้มเหลวหากสตริงไม่มีตัวเลขในนั้น ลองใช้ตัวอย่างเช่น ['silent', 'ghost'] (ทำดัชนีนอกช่วง)
snakile

2
@snaklie: คำถามของคุณล้มเหลวในการสร้างกรณีตัวอย่างที่ดี คุณยังไม่ได้อธิบายสิ่งที่คุณพยายามทำและคุณไม่ได้อัปเดตคำถามของคุณด้วยข้อมูลใหม่นี้ คุณยังไม่ได้โพสต์สิ่งที่คุณได้ลองดังนั้นโปรดอย่าเพิกเฉยต่อความพยายามกระแสจิตของฉัน
SilentGhost

5
@SilentGhost: อันดับแรกฉันให้ upvote เพราะฉันคิดว่าคำตอบของคุณมีประโยชน์ (แม้ว่ามันจะไม่ได้แก้ปัญหาของฉัน) ประการที่สองฉันไม่สามารถครอบคลุมกรณีที่เป็นไปได้ทั้งหมดด้วยตัวอย่าง ฉันคิดว่าฉันได้ให้คำจำกัดความที่ชัดเจนกับธรรมชาติ ฉันไม่คิดว่ามันเป็นความคิดที่ดีที่จะให้ตัวอย่างที่ซับซ้อนหรือคำจำกัดความที่ยาวสำหรับแนวคิดง่ายๆ คุณยินดีที่จะแก้ไขคำถามของฉันถ้าคุณสามารถคิดสูตรที่ดีกว่าของปัญหา
snakile

1
@SilentGhost: ฉันต้องการจัดการกับสตริงเช่นเดียวกับที่ Windows จัดการกับชื่อไฟล์ดังกล่าวเมื่อเรียงลำดับไฟล์ตามชื่อ (ไม่สนใจกรณีและอื่น ๆ ) ดูเหมือนจะชัดเจนสำหรับฉัน แต่สิ่งที่ฉันพูดดูเหมือนชัดเจนกับฉันดังนั้นฉันจะไม่ตัดสินว่ามันชัดเจนหรือไม่
snakile

1
@snakile ที่คุณมาไม่มีที่ไหนใกล้ใกล้กับการค้นหาตามธรรมชาติ ซึ่งค่อนข้างยากที่จะทำและต้องการรายละเอียดมากมาย หากคุณต้องการลำดับการเรียงที่ใช้โดย windows explorer คุณรู้หรือไม่ว่ามีการเรียก API แบบง่ายที่ให้สิ่งนี้
David Heffernan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.