Jak mogę numerycznie posortować pojedynczy wiersz rozdzielanych elementów?


11

Mam linię (lub wiele linii) liczb, które są ograniczone dowolnym znakiem. Jakich narzędzi UNIX mogę użyć do sortowania elementów każdego wiersza numerycznie, zachowując separator?

Przykłady obejmują:

  • lista liczb; wkład 10 50 23 42:; posortowane:10 23 42 50
  • Adres IP; wkład 10.1.200.42:; posortowane:1.10.42.200
  • CSV; wkład 1,100,330,42:; posortowane:1,42,100,330
  • rozdzielany potokami; wkład 400|500|404:; posortowane:400|404|500

Ponieważ separator jest arbitralny, możesz podać (lub rozszerzyć) odpowiedź za pomocą jednoznakowego separatora wybranego przez siebie.


8
powinieneś zamieścić go na codegolf :)
ivanivan

1
istnieje podobne pytanie również tutaj Chciałbym dodać jego link Alfabetycznie wyrazy w nazwach plików za pomocą sortowania?
αғsнιη

Tylko wskazówka, która cutobsługuje dowolne ograniczniki z tą -dopcją.
Oleg Lobachev

Proszę wyjaśnić, czy te cztery przykłady DSV znajdują się w tym samym pliku, czy są to próbki z czterech różnych plików.
agc

2
Widząc niektóre inne komentarze: separator jest arbitralny, ale byłby konsekwentnie używany w danych wejściowych. Załóżmy, że producent danych wywiadu tak, że nie używałby przecinków jako separatora i danych (na przykład 4,325 comma 55 comma 42,430nie wystąpiłby, ani 1.5 period 4.2).
Jeff Schaller

Odpowiedzi:


12

Możesz to osiągnąć za pomocą:

tr '.' '\n' <<<"$aline" | sort -n | paste -sd'.' -

zamień kropki . na separator.
dodaj -udo sortpowyższego polecenia, aby usunąć duplikaty.


lub za pomocą gawk( GNU awk ) możemy przetwarzać wiele linii, a powyższe można również rozszerzyć:

gawk -v SEP='*' '{ i=0; split($0, arr, SEP); 
    while ( ++i<=asort(arr) ){ printf("%s%s", i>1?SEP:"", arr[i]) }; 
        print "" 
}' infile

zamień *jako separator pól na SEP='*'swój ogranicznik .


Uwagi:
Może być konieczne użycie -g, --general-numeric-sortopcji sortzamiast -n, --numeric-sortobsługi dowolnej klasy liczb (liczba całkowita, liczba zmiennoprzecinkowa, liczba naukowa, liczba szesnastkowa itp.).

$ aline='2e-18,6.01e-17,1.4,-4,0xB000,0xB001,23,-3.e+11'
$ tr ',' '\n' <<<"$aline" |sort -g | paste -sd',' -
-3.e+11,-4,2e-18,6.01e-17,1.4,23,0xB000,0xB001

W awkbraku zmian potrzeba, to nadal będzie obchodzenia tych.


10

Używanie perljest oczywistą wersją; podziel dane, posortuj je, dołącz ponownie.

Separator musi być wymieniony dwukrotnie (raz w spliti raz w join)

np. dla ,

perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'

Więc

echo 1,100,330,42 | perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'
1,42,100,330

Ponieważ splitjest wyrażeniem regularnym, znak może wymagać cytowania:

echo 10.1.200.42 | perl -lpi -e '$_=join(".",sort {$a <=> $b} split(/\./))'
1.10.42.200

Korzystając z opcji -ai -F, można usunąć podział. Za pomocą -ppętli, jak poprzednio, ustaw wyniki na $_, które automatycznie wydrukują:

perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'

4
możesz użyć tej -lopcji zamiast używać chomp. To także dodaje nową linię po wydrukowaniu. Zobacz także -a(z -F) część dzielącą.
Stéphane Chazelas,

1
Z -li -Fjest jeszcze ładniej:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
muru

@ StéphaneChazelas dzięki za -lopcję; Tęskniłem za tym!
Stephen Harris,

1
@muru Nie użyłem -Fflagi początkowo, ponieważ nie działa ona poprawnie we wszystkich wersjach (np. twoja linia w CentOS 7 - perl 5.16.3 - zwraca puste wyjście, chociaż działa dobrze w Debianie 9). Ale w połączeniu z -ptym daje nieco mniejszy wynik, więc dodałem to jako alternatywę dla odpowiedzi. pokazując, jak -Fmożna z niego korzystać. Dzięki!
Stephen Harris,

2
@StephenHarris to dlatego, że nowsze wersje Perla automatycznie dodają -ai -nopcje, kiedy -Fsą używane i -nkiedy -asą używane ... więc po prostu zmień -lena-lane
Niedziela

4

Używając Pythona i podobnego pomysłu jak w odpowiedzi Stephena Harrisa :

python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' <delmiter>

Więc coś takiego:

$ cat foo
10.129.3.4
1.1.1.1
4.3.2.1
$ python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' . < foo
3.4.10.129
1.1.1.1
1.2.3.4

Niestety konieczność ręcznego wykonania operacji we / wy sprawia, że ​​jest to o wiele mniej eleganckie niż wersja Perla.



3

Muszla

Ładowanie języka wyższego poziomu wymaga czasu.
Dla kilku linii sama powłoka może być rozwiązaniem.
Możemy użyć polecenia zewnętrznego sorti polecenia tr. Jedna jest dość wydajna w sortowaniu linii, a druga skutecznie konwertuje jeden separator na nowe linie:

#!/bin/bash
shsort(){
           while IFS='' read -r line; do
               echo "$line" | tr "$1" '\n' |
               sort -n   | paste -sd "$1" -
           done <<<"$2"
    }

shsort ' '    '10 50 23 42'
shsort '.'    '10.1.200.42'
shsort ','    '1,100,330,42'
shsort '|'    '400|500|404'
shsort ','    '3 b,2       x,45    f,*,8jk'
shsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

To wymaga basha z powodu użycia <<<tylko. Jeśli to zostanie zastąpione tutaj-doc, rozwiązanie jest ważne dla posix.
Jest zdolny do sortowania pól z kartami, spacji lub znaków powłoki glob ( *, ?, [). Nie nowe wiersze, ponieważ każda linia jest sortowana.

Zmień, <<<"$2"aby <"$2"przetwarzać nazwy plików i nazwać to tak:

shsort '.'    infile

Separator jest taki sam dla całego pliku. Jeśli jest to ograniczenie, można je ulepszyć.

Jednak przetworzenie pliku zawierającego zaledwie 6000 linii zajmuje 15 sekund. Naprawdę powłoka nie jest najlepszym narzędziem do przetwarzania plików.

Awk

W przypadku więcej niż kilku wierszy (więcej niż kilku dziesiątek) lepiej jest użyć prawdziwego języka programowania. Rozwiązaniem awk może być:

#!/bin/bash
awksort(){
           gawk -v del="$1" '{
               split($0, fields, del)
               l=asort(fields)
               for(i=1;i<=l;i++){
                   printf( "%s%s" , (i==0)?"":del , fields[i] )
               }
               printf "\n"
           }' <"$2"
         }

awksort '.'    infile

Co zajmuje tylko 0,2 sekundy dla tego samego pliku 6000 linii wspomnianego powyżej.

Zrozum, że <"$2"pliki for można zmienić z powrotem <<<"$2"na wiersze wewnątrz zmiennych powłoki.

Perl

Najszybszym rozwiązaniem jest perl.

#!/bin/bash
perlsort(){  perl -lp -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' <<<"$2";   }

perlsort ' '    '10 50 23 42'
perlsort '.'    '10.1.200.42'
perlsort ','    '1,100,330,42'
perlsort '|'    '400|500|404'
perlsort ','    '3 b,2       x,45    f,*,8jk'
perlsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

Jeśli chcesz posortować zmianę pliku, <<<"$a"po prostu "$a"i dodać -iopcje perla, aby edycja pliku była „na miejscu”:

#!/bin/bash
perlsort(){  perl -lpi -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' "$2"; }

perlsort '.' infile; exit

2

Używanie seddo sortowania oktetów adresu IP

sednie ma wbudowanej sortfunkcji, ale jeśli dane są wystarczająco ograniczone w zakresie (np. z adresami IP), możesz wygenerować skrypt sed, który ręcznie implementuje prosty sortowanie bąbelkowe . Podstawowym mechanizmem jest wyszukiwanie sąsiednich liczb, które są poza kolejnością. Jeśli liczby są nieprawidłowe, zamień je.

Sam sedskrypt zawiera dwie komendy wyszukiwania i zamiany dla każdej pary liczb poza kolejnością: jedna dla pierwszych dwóch par oktetów (wymuszanie obecności ogranicznika końcowego w celu oznaczenia końca trzeciego oktetu) oraz drugi dla trzeciej pary oktetów (koniec EOL). Jeśli wystąpią zamiany, program rozgałęzia się na początek skryptu, szukając liczb, które nie są uporządkowane. W przeciwnym razie wychodzi.

Wygenerowany skrypt jest częściowo:

$ head -n 3 generated.sed
:top
s/255\.254\./254.255./g; s/255\.254$/254.255/
s/255\.253\./253.255./g; s/255\.253$/253.255/

# ... middle of the script omitted ...

$ tail -n 4 generated.sed
s/2\.1\./1.2./g; s/2\.1$/1.2/
s/2\.0\./0.2./g; s/2\.0$/0.2/
s/1\.0\./0.1./g; s/1\.0$/0.1/
ttop

Takie podejście sztywno koduje kropkę jako separator, który musi być poprzedzony znakiem ucieczki, ponieważ w przeciwnym razie byłby „specjalny” w składni wyrażeń regularnych (dopuszczając dowolny znak).

Aby wygenerować taki skrypt sed, ta pętla wykona:

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; n-- )); do
  for (( m = n - 1; m >= 0; m-- )); do
    printf '%s; %s\n' "s/$n\\.$m\\./$m.$n./g" "s/$n\\.$m\$/$m.$n/"
  done
done

echo 'ttop'

Powiedzmy, że przekieruj wyjście tego skryptu do innego pliku sort-ips.sed.

Przykładowy przebieg może wyglądać następująco:

ip=$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))
printf '%s\n' "$ip" | sed -f sort-ips.sed

Poniższa odmiana skryptu generującego używa znaczników granicy słowa \<i \>pozbywa się potrzeby drugiego podstawienia. Zmniejsza to również rozmiar generowanego skryptu z 1,3 MB do nieco poniżej 900 KB, a także znacznie skraca czas działania sedsamego programu (do około 50% -75% oryginału, w zależności od sedużywanej implementacji):

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; --n )); do
  for (( m = n - 1; m >= 0; --m )); do
      printf '%s\n' "s/\\<$n\\>\\.\\<$m\\>/$m.$n/g"
  done
done

echo 'ttop'

1
Ciekawy pomysł, ale wydaje się, że to trochę komplikuje sprawy.
Matt

1
@Matt To trochę w tym rzecz. Sortowanie czegokolwiek sedjest śmieszne, dlatego jest ciekawym wyzwaniem.
Kusalananda

2

Oto kilka bashów, które same odgadują ogranicznik:

#!/bin/bash

delimiter="${1//[[:digit:]]/}"
if echo $delimiter | grep -q "^\(.\)\1\+$"
then
  delimiter="${delimiter:0:1}"
  if [[ -z $(echo $1 | grep "^\([0-9]\+"$delimiter"\([0-9]\+\)*\)\+$") ]]
  then
    echo "You seem to have empty fields between the delimiters."
    exit 1
  fi
  if [[ './\' == *$delimiter* ]]
  then
    n=$( echo $1 | sed "s/\\"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/\\"$delimiter"/g")
  else
    n=$( echo $1 | sed "s/"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/"$delimiter"/g")
  fi
  echo ${n%$delimiter}
  exit 0
else
  echo "The string does not consist of digits separated by one unique delimiter."
  exit 1
fi

Może nie jest zbyt wydajny ani czysty, ale działa.

Użyj jak bash my_script.sh "00/00/18/29838/2".

Zwraca błąd, gdy ten sam ogranicznik nie jest używany konsekwentnie lub gdy dwa lub więcej ograniczników następuje jeden za drugim.

Jeśli użyty ogranicznik jest znakiem specjalnym, to jest on oznaczany znakiem ucieczki (w przeciwnym razie sedzwraca błąd).


To zainspirowało to .
agc

2

Ta odpowiedź jest oparta na nieporozumieniu Q., ale w niektórych przypadkach i tak jest poprawna. Jeśli dane wejściowe są liczbami całkowicie naturalnymi i mają tylko jeden separator na linię (jak w przykładowych danych w Q.), działa poprawnie. Będzie także obsługiwał pliki z liniami, z których każdy ma swój separator, co jest nieco więcej niż to, o co prosiliśmy.

Ta funkcja powłoki reads ze standardowego wejścia, używa podstawienia parametrów POSIX, aby znaleźć określony separator w każdym wierszu (przechowywany w $d), i używa trdo zastąpienia $dznakiem nowej linii \ni sortdanych tego wiersza, a następnie przywraca oryginalne ograniczniki każdego wiersza:

sdn() { while read x; do
            d="${x#${x%%[^0-9]*}}"   d="${d%%[0-9]*}"
            x=$(echo -n "$x" | tr "$d" '\n' | sort -g | tr '\n' "$d")
            echo ${x%?}
        done ; }

Dotyczy danych podanych w PO :

printf "%s\n" "10 50 23 42" "10.1.200.42" "1,100,330,42" "400|500|404" | sdn

Wynik:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500

Separator w dowolnej linii będzie spójny; ogólne rozwiązania, które pozwalają użytkownikowi zadeklarować ogranicznik, są niesamowite, ale odpowiedzi mogą zakładać, że dowolny ogranicznik ma dla nich sens (pojedynczy znak i nie występuje w samych danych liczbowych).
Jeff Schaller

2

W przypadku dowolnych ograniczników:

perl -lne '
  @list = /\D+|\d+/g;
  @sorted = sort {$a <=> $b} grep /\d/, @list;
  for (@list) {$_ = shift@sorted if /\d/};
  print @list'

Na wejściu takim jak:

5,4,2,3
6|5,2|4
There are 10 numbers in those 3 lines

To daje:

2,3,4,5
2|4,5|6
There are 3 numbers in those 10 lines

0

To powinno obsłużyć dowolny niecyfrowy (0-9) ogranicznik. Przykład:

x='1!4!3!5!2'; delim=$(echo "$x" | tr -d 0-9 | cut -b1); echo "$x" | tr "$delim" '\n' | sort -g | tr '\n' "$delim" | sed "s/$delim$/\n/"

Wynik:

1!2!3!4!5

0

Z perl:

$ # -a to auto-split on whitespace, results in @F array
$ echo 'foo baz v22 aimed' | perl -lane 'print join " ", sort @F'
aimed baz foo v22
$ # {$a <=> $b} for numeric comparison, {$b <=> $a} will give descending order
$ echo '1,100,330,42' | perl -F, -lane 'print join ",", sort {$a <=> $b} @F'
1,42,100,330

Z ruby, co jest nieco podobne doperl

$ # -a to auto-split on whitespace, results in $F array
$ # $F is sorted and then joined using the given string
$ echo 'foo baz v22 aimed' | ruby -lane 'print $F.sort * " "'
aimed baz foo v22

$ # (&:to_i) to convert string to integer
$ echo '1,100,330,42' | ruby -F, -lane 'print $F.sort_by(&:to_i) * ","'
1,42,100,330

$ echo '10.1.200.42' | ruby -F'\.' -lane 'print $F.sort_by(&:to_i) * "."'
1.10.42.200


Komenda niestandardowa i przekazywanie tylko łańcucha separatora (nie regex). Działa, jeśli dane wejściowe zawierają również dane zmiennoprzecinkowe

$ # by default join uses value of $,
$ sort_line(){ ruby -lne '$,=ENV["d"]; print $_.split($,).sort_by(&:to_f).join' ; }

$ s='103,14.5,30,24'
$ echo "$s" | d=',' sort_line
14.5,24,30,103
$ s='10.1.200.42'
$ echo "$s" | d='.' sort_line
1.10.42.200

$ # for file input
$ echo '123--87--23' > ip.txt
$ echo '3--12--435--8' >> ip.txt
$ d='--' sort_line <ip.txt
23--87--123
3--8--12--435


Niestandardowe polecenie dla perl

$ sort_line(){ perl -lne '$d=$ENV{d}; print join $d, sort {$a <=> $b} split /\Q$d/' ; }
$ s='123^[]$87^[]$23'
$ echo "$s" | d='^[]$' sort_line 
23^[]$87^[]$123


Dalsza lektura - Mam już tę poręczną listę jednoliniowych perli / rubinów


0

Poniżej znajduje się odmiana odpowiedzi Jeffa w tym sensie, że generuje sedskrypt, który wykona sortowanie bąbelkowe, ale jest wystarczająco różny, aby uzasadnić własną odpowiedź.

Różnica polega na tym, że zamiast generować podstawowe wyrażenia regularne O (n ^ 2), generuje to rozszerzone wyrażenia regularne O (n). Wynikowy skrypt będzie miał około 15 KB. Czas działania sedskryptu to ułamki sekundy (wygenerowanie skryptu trwa nieco dłużej).

Ogranicza się do sortowania dodatnich liczb całkowitych oddzielonych kropkami, ale nie ogranicza się do wielkości liczb całkowitych (po prostu zwiększenie 255w głównej pętli) lub liczby liczb całkowitych. Separator można zmienić, zmieniając delim='.'kod.

Zrobiłem wszystko, aby poprawnie wyobrazić sobie wyrażenia regularne, więc opowiem opisywanie szczegółów na kolejny dzień.

#!/bin/bash

# This function creates a extended regular expression
# that matches a positive number less than the given parameter.
lt_pattern() {
    local n="$1"  # Our number.
    local -a res  # Our result, an array of regular expressions that we
                  # later join into a string.

    for (( i = 1; i < ${#n}; ++i )); do
        d=$(( ${n: -i:1} - 1 )) # The i:th digit of the number, from right to left, minus one.

        if (( d >= 0 )); then
            res+=( "$( printf '%d[0-%d][0-9]{%d}' "${n:0:-i}" "$d" "$(( i - 1 ))" )" )
        fi
    done

    d=${n:0:1} # The first digit of the number.
    if (( d > 1 )); then
        res+=( "$( printf '[1-%d][0-9]{%d}' "$(( d - 1 ))" "$(( ${#n} - 1 ))" )" )
    fi

    if (( n > 9 )); then
        # The number is 10 or larger.
        res+=( "$( printf '[0-9]{1,%d}' "$(( ${#n} - 1 ))" )" )
    fi

    if (( n == 1 )); then
        # The number is 1. The only thing smaller is zero.
        res+=( 0 )
    fi

    # Join our res array of expressions into a '|'-delimited string.
    ( IFS='|'; printf '%s\n' "${res[*]}" )
}

echo ':top'

delim='.'

for (( n = 255; n > 0; --n )); do
    printf 's/\\<%d\\>\\%s\\<(%s)\\>/\\1%s%d/g\n' \
        "$n" "$delim" "$( lt_pattern "$n" )" "$delim" "$n"
done

echo 'ttop'

Skrypt będzie wyglądał mniej więcej tak:

$ bash generator.sh >script.sed
$ head -n 5 script.sed
:top
s/\<255\>\.\<(25[0-4][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.255/g
s/\<254\>\.\<(25[0-3][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.254/g
s/\<253\>\.\<(25[0-2][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.253/g
s/\<252\>\.\<(25[0-1][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.252/g
$ tail -n 5 script.sed
s/\<4\>\.\<([1-3][0-9]{0})\>/\1.4/g
s/\<3\>\.\<([1-2][0-9]{0})\>/\1.3/g
s/\<2\>\.\<([1-1][0-9]{0})\>/\1.2/g
s/\<1\>\.\<(0)\>/\1.1/g
ttop

Ideą wygenerowanych wyrażeń regularnych jest dopasowanie wzorca dla liczb, które są mniejsze niż każda liczba całkowita; te dwie liczby byłyby zepsute, a więc są zamieniane. Wyrażenia regularne są pogrupowane w kilka opcji OR. Zwróć szczególną uwagę na zakresy dołączane do każdego elementu, czasem tak jest {0}, co oznacza, że ​​poprzedni element należy pominąć podczas wyszukiwania. Opcje wyrażenia regularnego od lewej do prawej odpowiadają liczbom mniejszym niż podana liczba o:

  • te miejsca
  • miejsce dziesiątek
  • setki miejsc
  • (ciąg dalszy w razie potrzeby, dla większych liczb)
  • lub mniejszy pod względem wielkości (liczba cyfr)

Aby przeliterować przykład, weź 101(z dodatkowymi spacjami dla czytelności):

s/ \<101\> \. \<(10[0-0][0-9]{0} | [0-9]{1,2})\> / \1.101 /g

Tutaj pierwsza zmiana pozwala na liczby od 100 do 100; druga zmiana pozwala na 0 do 99.

Innym przykładem jest 154:

s/ \<154\> \. \<(15[0-3][0-9]{0} | 1[0-4][0-9]{1} | [0-9]{1,2})\> / \1.154 /g

Tutaj pierwsza opcja pozwala na 150 do 153; drugi pozwala od 100 do 149, a ostatni pozwala od 0 do 99.

Testowanie cztery razy w pętli:

for test_run in {1..4}; do
    nums=$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 ))
    printf 'nums=%s\n' "$nums"
    sed -E -f script.sed <<<"$nums"
done

Wynik:

nums=90.19.146.232
19.90.146.232
nums=8.226.70.154
8.70.154.226
nums=1.64.96.143
1.64.96.143
nums=67.6.203.56
6.56.67.203

-2

Podział danych wejściowych na wiele wierszy

Za pomocą trmożna podzielić dane wejściowe za pomocą dowolnego ogranicznika na wiele wierszy.

Wejście to można następnie uruchomić sort(używając, -njeśli dane wejściowe są numeryczne).

Jeśli chcesz zachować separator na wyjściu, możesz użyć go trponownie, aby dodać separator z powrotem.

np. używając spacji jako separatora

cat input.txt | tr " " "\n" | sort -n | tr "\n" " "

wejście: 1 2 4 1 4 32 18 3 wyjście:1 1 2 3 4 4 18 32


Możesz bezpiecznie założyć elementy liczbowe i tak: ogranicznik powinien zostać wymieniony.
Jeff Schaller
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.