是否有用于字符串自然排序的内置函数?


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


如果要在多种情况下(例如sorting)进行“类似于Windows Explorer的排序,此站点上的所有答案都将产生错误的结果!1, 1, !a, a。像Windows那样进行排序的唯一方法似乎是使用Windows StrCmpLogicalW 函数本身,因为似乎没有人正确地重新实现了此函数(将不胜感激)。解决方案:stackoverflow.com/a/48030307/2441026
user136036

Answers:


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是一个很棒的库,应该添加到python标准库中!:-)
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)?是为了提高性能还是仅仅是增加Python风格?
jperelli 2012年

12
@jperelli我认为阶梯会更改调用方中的原始列表。但最有可能的是,呼叫者想要列表的另一个浅表副本。
huggie 2012年

3
仅作记录,这不能处理所有输入:str / int拆分必须对齐,否则您将为输入[“ foo0]创建比较,例如[” foo“,0] <[0,” foo“] “,” 0foo“],它引发TypeError。
user19087

4
@ user19087:其实它的工作,因为re.split('([0-9]+)', '0foo')回报['', '0', 'foo']。因此,字符串将始终位于数组的偶数索引上,而整数将始终位于奇数索引上。
弗洛里安·库舍

对于任何想知道性能的人来说,这明显慢于python的本机排序。即慢25 -50倍。而且,如果您想始终将[elm1,elm2,Elm2,elm2]始终可靠地排序为[elm1,Elm2,elm2,elm2](大写字母在下),那么您可以简单地调用natural_sort(sorted(lst))。效率较低,但很容易获得可重复的排序。编译正则表达式可以使速度提高约50%。正如克劳迪(Claudiu)的回答所示。
查理·哈利

99

这是Mark Byer答案的更多Python版本:

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.sortsortedmax,等。

作为lambda:

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

9
重新模块自动编译并缓存正则表达式,因此无需进行预编译
2014年

1
@wim:它会缓存最近的X个用法,因此从技术上讲,可以使用X + 5正则表达式,然后一遍又一遍地进行自然排序,这时就不会缓存它。但从长远来看可能可以忽略不计
Claudiu 2014年

我没有这样做,但是也许原因是它不能像常规的python排序那样处理元组。
Unfun Cat 2015年

1
@Claudiu提到的X用法在Python 2.7上似乎是100,在Python 3.4上是512。还要注意,达到限制后,缓存将被完全清除(因此,不仅抛出了最旧的缓存)。
Zitrax

@Zitrax为什么/彻底清除缓存有何意义?
约书亚

19

我基于http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html编写了一个函数,该函数增加了仍可以传递您自己的“键”参数的功能。我需要这样做,以便执行包含更复杂对象(不仅仅是字符串)的自然列表。

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 2013年

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-liner将是

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

要么

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

说明:

此解决方案使用排序关键功能来定义将用于排序的功能。因为我们知道每个数据条目都以'elm'开头,所以排序函数会将字符串中第三个字符之后的部分(即int(x [3:]))转换为整数。如果数据的数字部分位于其他位置,则函数的这一部分将必须更改。

干杯


6
现在, 有了更多*优雅(pythonic )的功能,只需触摸一下

有很多实现,尽管有些实现已经接近,但没有一个实现能够完全抓住现代python提供的优雅。

  • 使用python(3.5.1)测试
  • 包括一个附加列表,以证明当数字为中间字符串时它可以工作
  • 但是,没有测试,我假设如果您的列表很大,那么事先编译正则表达式会更有效
    • 如果这是错误的假设,我敢肯定有人会纠正我

速成
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

这篇文章的价值

我的观点是提供可以普遍应用的非正则表达式解决方案。
我将创建三个函数:

  1. find_first_digit我从@AnuragUniyal借来的。它将查找字符串中第一个数字或非数字的位置。
  2. split_digits这是一个生成器,用于将字符串分成数字和非数字块。yield如果是数字,它也将是整数。
  3. natural_key只是包装split_digits成一个tuple。这是我们作为一个关键的使用sortedmaxmin

功能

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

我的正则表达式将是

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

一种选择是使用扩展形式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
不幸的是,该解决方案仅适用于Python2.X。对于Python 3,('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)将返回TypeError: unorderable types: int() < str()
SethMMorton'1

@SethMMorgon是正确的,该代码很容易在Python 3中中断。自然的替代方法似乎是natsortpypi.org
project/

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

欢迎反馈。这并不是一个确定的解决方案。只是前进了一步。


在您链接到的测试脚本中,由于未正确使用它们natsortedhumansorted失败了……您试图将natsorted其作为键传递,但实际上是排序功能本身。您应该尝试过natsort_keygen()
SethMMorton 2015年

2

最有可能functools.cmp_to_key()与python sort的底层实现紧密相关。此外,cmp参数是旧式的。现代方法是将输入项转换为支持所需的丰富比较操作的对象。

在CPython 2.x中,即使尚未实现各自的丰富比较运算符,也可以对不同类型的对象进行排序。在CPython 3.x下,不同类型的对象必须显式支持比较。请参阅Python如何比较string和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()

有三种不同的方法。第一种使用嵌套类来利用Python的Iterable比较算法。第二个将嵌套展开为单个类。第三类放弃了子类化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()。我可能没有考虑过细微的编码问题。因此,我暂时推荐natsort库。我快速浏览了github仓库;代码维护一直很出色。

我见过的所有算法都取决于技巧,例如复制和降低字符以及交换大小写。虽然这使运行时间加倍,但是另一种方法将要求对输入字符集进行自然排序。我认为这不是unicode规范的一部分,并且由于unicode位数比得多[0-9],因此创建这种排序同样令人生畏。如果要进行区域设置比较,请locale.strxfrm按照Python的Sorting 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关键字参数of 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)与Mark的答案相比,这个答案没有带来任何新的东西(任何短毛猫都可以PEP8修改某些代码)。还是key参数?但这也体现在@beauburrier的答案中
CiprianTomoiagă

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)

顺便说一句,也许不是每个人都记得函数参数默认值是在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

致谢

泡泡排序作业

如何在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
您的实现只能解决数字问题。如果字符串中没有数字,则实现将失败。例如,在['silent','ghost']上尝试(列表索引超出范围)。
snakile 2011年

2
@snaklie:您的问题未能提供恰当的示例案例。您没有解释您要做什么,也没有使用此新信息更新您的问题。您尚未发布任何尝试过的内容,所以请不要对我的心灵感应尝试感到不屑一顾。
SilentGhost

5
@SilentGhost:首先,我给了你一个赞,因为我认为你的回答很有用(即使它不能解决我的问题)。其次,我无法举例说明所有可能的情况。我想我已经对自然排序给出了一个非常明确的定义。我认为为这样一个简单的概念提供一个复杂的例子或一个较长的定义不是一个好主意。如果您能想到一个更好的解决方案,欢迎您编辑我的问题。
snakile 2011年

1
@SilentGhost:我想像按Windows按名称对文件排序时(忽略大小写等)一样处理此类字符串。对我来说似乎很清楚,但是我所说的话对我来说都很清楚,因此我无法判断它是否清楚。
snakile 2011年

1
@snakile您几乎没有定义自然搜索。那将很难做到,并且需要很多细节。如果您想要Windows资源管理器使用的排序顺序,您知道有一个简单的api调用可以提供此功能吗?
David Heffernan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.