有没有针对字符串自然排序的内置函数?

414

我有一个字符串列表,希望能够进行自然字母顺序排序

例如,下面的列表是按照自然顺序排序的(我想要的):

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

以下是上述列表的“排序”版本(使用sorted()获得):

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

我正在寻找一个类似于第一个的排序函数。


24个回答

370

在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文档的How It Works页面,特别是Special Cases Everywhere!部分。


如果您需要排序键而不是排序函数,请使用以下任一公式。

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

2020年11月更新

鉴于一个常见的请求/问题是“如何像Windows Explorer一样排序?”(或你的操作系统的文件系统浏览器),natsort版本7.1.0引入了名为os_sorted的函数,可以完美解决此问题,在Windows上,它将按照与Windows Explorer相同的顺序进行排序;在其他操作系统上,应该会按照本地文件系统浏览器的方式进行排序。

>>> from natsort import os_sorted
>>> os_sorted(list_of_paths)
# your paths sorted like your file system browser

如果需要排序关键字,您可以使用os_sort_keygen(或者如果只需要默认值,则可以使用os_sort_key)。

注意 - 请在使用此函数之前阅读API文档,以了解其限制和如何获得最佳结果。


7
我认为很有趣的是,natsort在数字不在结尾的情况下也可以正确排序,这通常适用于文件名。请随意使用以下示例:http://pastebin.com/9cwCLdEK。 - Martin Thoma
4
Natsort是一个很棒的库,应该被加入Python标准库! :-) - Mitch McMabers
1
@SethMMorton Windows 按照以下顺序排序文件 ['!2020', '.2020', '2020']。Natsort 返回的是 ['2020', '!2020', '.2020']。它能像 Windows 那样排序吗? - F. Vosnim
1
谢谢您提供的 os_sorted 函数,它让我的程序在 Mac Finder 中的排序得到了匹配。 - Nic Scozzaro
1
@Stef 不是真的。请参见 https://github.com/SethMMorton/natsort/issues/150 了解详细信息。 - SethMMorton
显示剩余2条评论

263

试试这个:

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

代码改编自此处:Sorting for Humans: Natural Sort Order


4
为什么你使用 return sorted(l, key) 而不是 l.sort(key)?是为了提高性能还是只是为了更符合 Python 风格? - jperelli
16
我认为这个梯子会改变调用者原来的列表。但很可能调用者想要另一个浅拷贝的列表。 - huggie
3
仅供参考,这个函数无法处理所有输入:字符串/整数切分必须对齐,否则您将得到类似于 ["foo",0] < [0,"foo"] 的比较结果,对于输入 ["foo0","0foo"],会引发 TypeError。 - user19087
5
实际上这是有效的,因为re.split('([0-9]+)', '0foo')返回['', '0', 'foo']。因此,在该数组中,字符串始终位于偶数索引上,而整数则位于奇数索引上。 - Florian Kusche
split方法会返回空字符串,我使用findall()和正则/(\d+|\D+)/,请看我的答案https://dev59.com/VWct5IYBdhLWcg3wsfgZ#69697978。 - Muayyad Alsadi
显示剩余4条评论

143
这是Mark Byer回答的更Pythonic的版本:
import re

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

现在这个函数可以作为关键字用于任何使用它的函数,例如 list.sortsortedmax 等。

作为lambda表达式:

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

完全可重现的演示代码:
import re
natsort = lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
L = ["a1", "a10", "a11", "a2", "a22", "a3"]   
print(sorted(L, key=natsort))  
# ['a1', 'a2', 'a3', 'a10', 'a11', 'a22'] 

11
re模块会自动编译和缓存正则表达式,因此不需要提前进行预编译。 - wim
2
@wim:它会缓存最后X个用法,因此在技术上可以使用X+5个正则表达式,然后一遍又一遍地进行自然排序,此时这些内容将不再被缓存。但在长期运行中可能是可以忽略的。 - Claudiu
3
@Claudiu提到的在Python 2.7中使用次数似乎为100,在Python 3.4中为512。还要注意的是,当达到限制时,缓存将被完全清除(因此不仅仅是最旧的那个会被抛弃)。 - Zitrax
4
当列表元素是 Path 对象时,这种方法无效。不过你可以修改函数使其正常工作,只需将最后一个 _nsre.split(s) 改为 _nsre.split(str(s)) 即可。对于 lambda 表达式也一样,在表达式的末尾将 s 替换为 str(s) - cbrnr
1
责怪@Zitrax :D(https://stackoverflow.com/revisions/16090640/7) - Claudiu
显示剩余5条评论

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

让我们分析数据。所有元素的数字容量为2。且公共字母部分'elm'有3个字母。

因此,元素的最大长度为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尝试处理的数据类型最快速和高效的。 - S. R. Colledge
FYI 内置函数 str.zfill(width) 返回一个左填充ASCII 0数字的字符串副本,以形成长度为_width_的字符串。请查看官方文档了解更多信息:https://docs.python.org/3/library/stdtypes.html#str.zfill - KiriSakow

21

我根据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
如果你想要返回一个已排序的列表,可以使用list.sort(key=sort_key)。如果你想在lambda表达式中使用它,也可以这样做。另外,你不应该将变量命名为内置函数list。谢谢! - jtlz2

20

给定:

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

与SergO的解决方案类似,一个没有外部库的一行代码是:

data.sort(key=lambda x: int(x[3:]))
或者
sorted_data = sorted(data, key=lambda x: int(x[3:]))

说明:

这个解决方案使用了sortkey特性来定义一个排序所需的函数。因为我们知道每个数据条目都是以'elm'开头的,所以排序函数将字符串中第三个字符之后的部分转换为整数(即int(x[3:]))。如果数据的数字部分位于不同的位置,则该函数的这一部分需要更改。


2
我认为依赖于三个字符长的前缀不是一个好主意。从长远来看,这种方法非常不灵活。 - NicoHood

9
现在,让我们来看一些更加优雅(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)])

Full-Code 全代码
#!/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
    • 您需要区分导入

灵感来源于:


7

本文的价值

我的观点是提供一个通用的非正则表达式解决方案。
我将创建三个函数:

  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

5
Claudiu 对 Mark Byers 的回答进行了改进,这是更好的改进。;-)
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时间进行求值的


4

一种方法是将字符串转换为元组,并使用扩展形式替换数字,这样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 是正确的,这段代码在 Python 3 中很容易出问题。自然的替代方案似乎是 natsort,https://pypi.org/project/natsort/ - FlorianH

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接