Команда оболочки для суммирования чисел в одинаковых строках текста в файле - PullRequest
0 голосов
/ 04 июля 2019

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

Например:

25 cup of coffee
75 sign on the dotted
28 take a test
2 take a test
12 cup of coffee

Вывод будет:

37 cup of coffee
75 sign on the dotted
30 take a test

Есть предложения, как этого добиться в оболочке unix?

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

Ответы [ 6 ]

3 голосов
/ 04 июля 2019

Нет необходимости в нескольких процессах и трубах.awk более чем способен справиться со всей работой (и будет работать на несколько порядков быстрее с большими файлами).С помощью awk просто добавьте каждое из полей 2-NF в виде строки и используйте его в качестве индекса для суммирования чисел в поле 1 в массиве.Затем в разделе END просто выведите содержимое массива, например, предположив, что ваши данные хранятся в file, вы можете сделать:

awk '{
    for (i=2; i<=NF; i++)
        str = str " " $i
    a[str] += $1
    str=""
}
END {
    for (i in a) print a[i], i
}' file

Выше, первый цикл for просто добавляетсявсе поля из 2-NF в str, a[str] += $1 суммируют значения в поле 1 в массив a, используя str в качестве индекса.Это гарантирует, что значения для похожих строк суммируются.В разделе END вы просто зацикливаетесь на каждом элементе массива, выводя значение элемента (сумму), а затем индекс (оригинал str для полей 2-NF).

ПримерИспользуйте / Output

Просто возьмите то, что выше, выберите его, а затем вставьте его средней кнопкой мыши в командную строку в каталоге, где расположен ваш file (измените имя fileк имени файла данных)

$ awk '{
>     for (i=2; i<=NF; i++)
>         str = str " " $i
>     a[str] += $1
>     str=""
> }
> END {
>     for (i in a) print a[i], i
> }' file
30  take a test
37  cup of coffee
75  sign on the dotted

Если вы хотите, чтобы строки были отсортированы в другом порядке, просто добавьте | sort [options] после имени файла, чтобы направить вывод в sort.Например, для вывода в указанном порядке вы должны использовать | sort -k 2, а результат будет:

37  cup of coffee
75  sign on the dotted
30  take a test

Сохранение исходного порядка строк

В соответствии сВаш комментарий относительно того, как сохранить исходный порядок строк текста, видимого во входном файле, вы можете сохранить второй массив, где строки хранятся в порядке их просмотра, используя последовательный индекс, чтобы сохранить их в порядке.Например, массив o (массив заказов) используется ниже для хранения уникальной строки (поля 2-NF), а переменная n используется в качестве счетчика.Цикл над массивом используется для проверки того, содержит ли строка уже, и если это так, next используется, чтобы избежать сохранения строки и перейти к следующей записи ввода.В END цикл затем использует форму for (i = 0; i < n; i++) для вывода информации из обоих массивов в том порядке, в котором строка была видна в исходном файле, например,

awk -v n=0 '{
    for (i=2; i<=NF; i++)
        str = str " " $i
    a[str] += $1
    for (i = 0; i < n; i++)
        if (o[i] == str) {
            str=""
            next;
        }
    o[n++] = str;
    str=""
}
END {
    for (i = 0; i < n; i++) print a[o[i]], o[i]
}' file

Вывод

37  cup of coffee
75  sign on the dotted
30  take a test
0 голосов
/ 06 июля 2019

Использование datamash является относительно кратким. Сначала используйте sed, чтобы заменить первый пробел на вкладку (для этого задания datamash должен иметь один и только один разделитель табуляции), затем используйте -s -g2, чтобы отсортировать группы по 2-му полю, ( т.е."чашка" и т. Д. ), затем используйте sum 1, чтобы сложить номера первых столбцов по группам, и все готово. Нет, не совсем - числовой столбец по какой-то причине мигрировал в поле 2nd , поэтому reverse переносит его обратно в поле 1st :

sed 's/ /\t/' file | datamash -s -g2 sum 1 | datamash reverse

Выход:

37  cup of coffee
75  sign on the dotted
30  take a test
0 голосов
/ 04 июля 2019

Другая версия, основанная на той же логике, что и упомянутая здесь @ David.
Изменения: он пропускает циклы для ускорения процесса.

awk '
{
  text=substr($0, index($0,$2))
  if(!(text in text_sums)){ texts[i++]=text }
  text_sums[text]+=$1
}
END {
 for (i in texts) print text_sums[texts[i]],texts[i] 
}' input.txt  

Пояснение:
substr возвращает строку, начинающуюся с поля 2. т.е. текстовая часть
массив texts хранит текст в целочисленном индексе, если его нет в массиве text_sums.
text_sums продолжайте добавлять поле 1 для соответствующего текста.

Причина существования отдельного массива для сохранения текста в качестве значения, подкрепленного последовательным целым числом в качестве индекса, заключается в обеспечении порядка значения (текста) при доступе в том же последовательном порядке.

См. Array Intro

Сноски гласят:

Порядок будет отличаться в разных реализациях awk, которые обычно используют хеш-таблицы для хранения элементов и значений массива.

0 голосов
/ 04 июля 2019

Вот простой awk скрипт, который выполняет задачу:

script.awk

{                          # for each input line
    inpText = substr($0, length($1)+2);  # read the input text after 1st field
    inpArr[inpText] = inpArr[inpText] + 0 + $1; # accumulate the 1st field in array
}
END {                     # post processing
    for (i in inpArr) {   # for each element in inpArr
        print inpArr[i], i; # print the sum and the key
    }
}

input.txt

25 cup of coffee
75 sign on the dotted
28 take a test
2 take a test
12 cup of coffee

выполняется:

awk -f script.awk input.txt

вывод:

75 sign on the dotted
37 cup of coffee
30 take a test
0 голосов
/ 04 июля 2019

Вы имеете в виду что-то подобное?

#!/bin/bash

# define a dictionary
declare -A dict

# loop over all lines
while read -r line; do

   # read first word as value and the rest as text
   IFS=' ' read value text <<< "$line"

   # use 'text' as key, get value for 'text', default 0
   [ ${dict[$text]+exists} ] && dictvalue="${dict[$text]}" || dictvalue=0

   # sum value

   value=$(( $dictvalue + value )) 

   # save new value in dictionary
   dict[$text]="$value" 
done < data.txt  

# loop over dictionary, print sum and text
for key in "${!dict[@]}"; do
   printf "%s %s\n" "${dict[$key]}" "$key"
done

вывод

37 cup of coffee
75 sign on the dotted
30 take a test
0 голосов
/ 04 июля 2019

Вы можете сделать следующее (предположим, имя файла file.txt):

for key in $(sort -k2  -u file.txt   | cut -d ' ' -f2)
do 
    cat file.txt|grep $key  | awk '{s+=$1} END {print $2 "\t" s}'
done

Объяснение: 1. получить все уникальные ключи (чашка кофе, подписать пунктир, взятьтест):

sort -k2  -u file.txt   | cut -d ' ' -f2

2.grep всех строк с уникальным ключом из файла:

cat file.txt | grep $key 

3.Суммируйте строки, используя awk, где $ 1 = номер столбца и $ 2 = ключ

awk '{s+=$1} END {print $2 "\t" s}'
Поместите все в цикл и переберите уникальные ключи

Примечание: если ключ может быть подстрокой другого ключа, например, "кофе" и "чашка кофе", вынужно будет изменить шаг 2 на grep с регулярным выражением

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