Есть ли в Python встроенная функция для естественной сортировки строк?
Используя Python 3.x, у меня есть список строк, для которых я хотел бы выполнить естественную сортировку по алфавиту.
Естественная сортировка: порядок сортировки файлов в Windows.
Например, следующий список естественно отсортирован (что я хочу):
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
И вот "отсортированная" версия приведенного выше списка (что у меня есть):
['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']
Я ищу функцию сортировки, которая ведет себя как первая.
24 ответа
Для этого в 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']
Попробуй это:
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']
Посмотрите, как работает онлайн: ideone.
Код, адаптированный здесь: Сортировка для людей: Порядок естественной сортировки.
Вот гораздо более питонная версия ответа Марка Байера:
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)]
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
Я написал функцию на основе 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'}]
Дано:
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:])). Если числовая часть данных находится в другом месте, то эту часть функции придется изменить.
ура
Существует множество реализаций, и, хотя некоторые из них приблизились, ни одна из них не отразила элегантность, которую предоставляет современный питон.
- Протестировано с использованием 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
- вам нужно будет дифференцировать импорт
Вдохновение от
- Документация Python - Сортировка КАК
- Сортировка для людей: естественный порядок сортировки
- Сортировка людей
- Авторы / комментаторы к этому и ссылкам на посты
Значение этого поста
Я хочу предложить решение без регулярных выражений, которое можно применять в целом.
Я создам три функции:
find_first_digit
который я позаимствовал у @AnuragUniyal. Он найдет позицию первой цифры или не цифры в строке.split_digits
который является генератором, который разбирает строку на куски цифр и не цифр. Это будет такжеyield
целые числа, когда это цифра.natural_key
просто обертыванияsplit_digits
вtuple
, Это то, что мы используем в качестве ключа дляsorted
,max
,min
,
функции
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')
Мы видим, что он сортирует список ОП в соответствующем порядке.
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))
Улучшение улучшения Клауди в ответе Марка Байерса;-)
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
время
Один из вариантов - превратить строку в кортеж и заменить цифры, используя расширенную форму 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']
Основываясь на ответах здесь, я написал 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
Более вероятный functools.cmp_to_key()
тесно связана с базовой реализацией рода Python. Кроме того, параметр 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()
Есть три разных подхода. Первый использует вложенные классы, чтобы использовать преимущества 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 КАК.
Я использую алгоритм
padzero_with_lower
как определено как:
import re
def padzero_with_lower(s):
return re.sub(r'\d+', lambda m: m.group(0).rjust(10, '0'), s).lower()
Алгоритм находит:
- находит и дополняет числа любой длины до достаточно большой, например 10
- затем он переводит строку в нижний регистр
Вот пример использования:
print(padzero_with_lower('file1.txt')) # file0000000001.txt
print(padzero_with_lower('file12.txt')) # file0000000012.txt
print(padzero_with_lower('file23.txt')) # file0000000023.txt
print(padzero_with_lower('file123.txt')) # file0000000123.txt
print(padzero_with_lower('file301.txt')) # file0000000301.txt
print(padzero_with_lower('Dir2/file15.txt')) # dir0000000002/file0000000015.txt
print(padzero_with_lower('dir2/file123.txt')) # dir0000000002/file0000000123.txt
print(padzero_with_lower('dir15/file2.txt')) # dir0000000015/file0000000002.txt
print(padzero_with_lower('Dir15/file15.txt')) # dir0000000015/file0000000015.txt
print(padzero_with_lower('elm0')) # elm0000000000
print(padzero_with_lower('elm1')) # elm0000000001
print(padzero_with_lower('Elm2')) # elm0000000002
print(padzero_with_lower('elm9')) # elm0000000009
print(padzero_with_lower('elm10')) # elm0000000010
print(padzero_with_lower('Elm11')) # elm0000000011
print(padzero_with_lower('Elm12')) # elm0000000012
print(padzero_with_lower('elm13')) # elm0000000013
После тестирования этой функции мы можем использовать ее в качестве ключа, т.е.
lis = ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
lis.sort(key=padzero_with_lower)
print(lis)
# Output: ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
Компактное решение, основанное на преобразовании строки в
List[Tuple(str, int)]
.
Код
def string_to_pairs(s, pairs=re.compile(r"(\D*)(\d*)").findall):
return [(text.lower(), int(digits or 0)) for (text, digits) in pairs(s)[:-1]]
Демонстрация
sorted(['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9'], key=string_to_pairs)
Выход:
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
Тесты
Трансформация
assert string_to_pairs("") == []
assert string_to_pairs("123") == [("", 123)]
assert string_to_pairs("abc") == [("abc", 0)]
assert string_to_pairs("123abc") == [("", 123), ("abc", 0)]
assert string_to_pairs("abc123") == [("abc", 123)]
assert string_to_pairs("123abc456") == [("", 123), ("abc", 456)]
assert string_to_pairs("abc123efg") == [("abc", 123), ("efg", 0)]
Сортировка
# Some extracts from the test suite of the natsort library. Permalink:
# https://github.com/SethMMorton/natsort/blob/e3c32f5638bf3a0e9a23633495269bea0e75d379/tests/test_natsorted.py
sort_data = [
( # same as test_natsorted_can_sort_as_unsigned_ints_which_is_default()
["a50", "a51.", "a50.31", "a-50", "a50.4", "a5.034e1", "a50.300"],
["a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51.", "a-50"],
),
( # same as test_natsorted_numbers_in_ascending_order()
["a2", "a5", "a9", "a1", "a4", "a10", "a6"],
["a1", "a2", "a4", "a5", "a6", "a9", "a10"],
),
( # same as test_natsorted_can_sort_as_version_numbers()
["1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"],
["1.9.9a", "1.9.9b", "1.10.1", "1.11", "1.11.4"],
),
( # different from test_natsorted_handles_filesystem_paths()
[
"/p/Folder (10)/file.tar.gz",
"/p/Folder (1)/file (1).tar.gz",
"/p/Folder/file.x1.9.tar.gz",
"/p/Folder (1)/file.tar.gz",
"/p/Folder/file.x1.10.tar.gz",
],
[
"/p/Folder (1)/file (1).tar.gz",
"/p/Folder (1)/file.tar.gz",
"/p/Folder (10)/file.tar.gz",
"/p/Folder/file.x1.9.tar.gz",
"/p/Folder/file.x1.10.tar.gz",
],
),
( # same as test_natsorted_path_extensions_heuristic()
[
"Try.Me.Bug - 09 - One.Two.Three.[text].mkv",
"Try.Me.Bug - 07 - One.Two.5.[text].mkv",
"Try.Me.Bug - 08 - One.Two.Three[text].mkv",
],
[
"Try.Me.Bug - 07 - One.Two.5.[text].mkv",
"Try.Me.Bug - 08 - One.Two.Three[text].mkv",
"Try.Me.Bug - 09 - One.Two.Three.[text].mkv",
],
),
( # same as ns.IGNORECASE for test_natsorted_supports_case_handling()
["Apple", "corn", "Corn", "Banana", "apple", "banana"],
["Apple", "apple", "Banana", "banana", "corn", "Corn"],
),
]
for (given, expected) in sort_data:
assert sorted(given, key=string_to_pairs) == expected
Бонус
Если в ваших строках смешаны тексты и числа, отличные от ASCII, вам может быть интересно составить
string_to_pairs()
с функцией
remove_diacritics()
отдаю в другом месте .
Приведенные выше ответы хороши для показанного конкретного примера, но пропускают несколько полезных случаев для более общего вопроса естественной сортировки. Я только попал в один из этих случаев, поэтому создал более тщательное решение:
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
Тестовый код и несколько ссылок (вкл и выкл Stackru) находятся здесь: http://productarchitect.com/code/better-natural-sort.py
Обратная связь приветствуется. Это не должно быть окончательным решением; просто шаг вперед.
Это более продвинутое решение, улучшенное Клаудиу и Марком Байерсом:
- Оно использует
casefold()
вместоlower()
чтобы соответствовать строкам - Вы можете передать другую ключевую лямбду, чтобы выбрать внутренний элемент (как вы привыкли к обычной функции сортировки)
- Работает конечно с
list.sort
,sorted
,max
, и т. д.
def natural_sort(key=None, _nsre=re.compile('([0-9]+)')):
return lambda x: [int(text) if text.isdigit() else text.casefold()
for text in _nsre.split(key(x) if key else x)]
Пример использования:
# Original solution
data.sort(key=natural_sort)
# Select an additional key
image_files.sort(key=natural_sort(lambda x: x.original_filename))
def sort_naturally(lst: list) -> list:
max_str_len = max([len(s) for s in lst])
return sorted(lst, key=lambda s: s.zfill(max_str_len + 1))
К вашему сведению, встроенная функцияstr.zfill(width)
возвращает копию строки, заполненной ASCII0
цифры, чтобы получилась строка длиной width . Дополнительную информацию см. в официальной документации: docs.python.org/3/library/stdtypes.html#str.zfill .
После ответа @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)
Я также сделал Gist
Позвольте мне представить свой собственный взгляд на эту потребность:
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']
Недостаток здесь, конечно, в том, что, как и сейчас, функция будет сортировать заглавные буквы перед строчными.
Я оставлю читателю реализацию группировщика без учета регистра:-)
Вот еще одна версия ответа Марка Байерса. В этой версии показано, как передать имя атрибута, которое будет использоваться для оценки объектов в списке.
def natural_sort(l, attrib):
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.__dict__[attrib])]
return sorted(l, key=alphanum_key)
results = natural_sort(albums, 'albumid')
Где
albums
список экземпляров альбома и
albumid
является строковым атрибутом, который номинально содержит числа.
Для сведения, вот еще один вариант простого решения Марка Байерса, аналогичного предложенному Уолтером Троссом, которое позволяет избежать звонков. Это не только ускоряет работу, но и позволяет избежать проблем, которые могут возникнуть из-за
isdigit()
считает больше символов Юникода цифрами, чем регулярное выражение
\d+
.
import re
from itertools import cycle
_re_digits = re.compile(r"(\d+)")
def natural_comparison_key(key):
return tuple(
int(part) if is_digit else part
for part, is_digit in zip(_re_digits.split(key), cycle((False, True)))
)
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
Благодарности:
Я предлагаю вам просто использовать 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]
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']