Есть ли в Python встроенная функция для естественной сортировки строк? - PullRequest
223 голосов
/ 29 января 2011

Используя Python 3.x, у меня есть список строк, для которых я хотел бы выполнить естественную сортировку по алфавиту.

Естественная сортировка: Порядок сортировки файлов в Windows.

Например, следующий список естественно отсортирован (что я хочу):

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

А вот «отсортированная» версия приведенного выше списка (что у меня есть):

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

Я ищу функцию сортировки, которая ведет себя как первая.

Ответы [ 15 ]

185 голосов
/ 24 августа 2013

Для этого в 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 документации *1007* Как это работает , в частности к 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']
162 голосов
/ 29 января 2011

Попробуйте:

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 .

Код адаптирован здесь: Сортировка для людей: естественный порядок сортировки .

84 голосов
/ 18 апреля 2013

Вот гораздо более питонная версия ответа Марка Байера:

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, * 1006.* и т. д.

Как лямбда:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
19 голосов
/ 20 января 2012

Я написал функцию, основанную на 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'}]
15 голосов
/ 15 июля 2015
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
10 голосов
/ 14 марта 2017

Дано:

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

Объяснение:

Это решение использует функцию key из sort , чтобы определить функцию, которая будет использоваться для сортировки.Поскольку мы знаем, что каждой записи данных предшествует 'elm', функция сортировки преобразует целую часть строки после 3-го символа (т. Е. Int (x [3:])).Если числовая часть данных находится в другом месте, то эту часть функции придется изменить.

Cheers

5 голосов
/ 04 мая 2016
А теперь для чего-то более * элегантного (pythonic) - просто прикосновение

Существует множество реализаций, и хотя некоторые из них приблизились, ни одна из них не совсем захватила элегантность современного питонапредоставляет.

  • Протестировано с использованием python (3.5.1)
  • Включен дополнительный список, чтобы продемонстрировать, что он работает, когда числа находятся в середине строки
  • Не тестировалтем не менее, я предполагаю, что если бы ваш список был значительным, было бы более эффективно заранее скомпилировать регулярное выражение
    • Я уверен, что кто-то исправит меня, если это ошибочное предположение

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
    • вам нужно будет дифференцировать импорт

Вдохновение от

4 голосов
/ 06 июня 2011

Один из вариантов - превратить строку в кортеж и заменить цифры, используя расширенную форму 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']
3 голосов
/ 20 февраля 2018

Значение этого поста

Моя цель состоит в том, чтобы предложить решение без регулярных выражений, которое можно применять в целом.
Я создам три функции:

  1. find_first_digit, который я позаимствовал у @ AnuragUniyal . Он найдет позицию первой цифры или не цифры в строке.
  2. split_digits, который является генератором, который разбивает строку на цифры и не цифры. Также это будет yield целых чисел, когда это цифра.
  3. 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))
3 голосов
/ 08 февраля 2018

Исходя из ответов здесь, я написал 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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...