распечатать все дубликаты в плохо отформатированном csv

#csv #awk

#csv #awk

Вопрос:

У меня есть огромный файл csv (~ 17 ГБ), и мне нужно создать новый файл, содержащий все строки, в которых значение последнего столбца появляется более одного раза, к сожалению, файл отформатирован неправильно. Есть значения, которые содержат запятые, например, строка 6 в приведенном ниже примере:

 entity,entity_type,component_id
bla@gmail.com,email,1111
lalal@hotmail.com,email,2222
15158112233,phone,3333
15158990000,phone,2222
hello,world@gmail.com,email,3333
1327168,phone,4444
fds_213445,device,3333
 

для следующего примера я бы ожидал этот новый файл:

 lalal@hotmail.com,email,2222
15158990000,phone,2222
15158112233,phone,3333
hello,world@gmail.com,email,3333
fds_213445,device,3333
 

В настоящее время я использую наивное решение:

  1. Подсчитайте размер каждого компонента и сохраните в файле A.
  2. Удалите из файла A все компоненты с размером = 1.
  3. Запуск в скрипте по всем идентификаторам компонентов в файле A распечатайте строки соответствия из исходного файла в новый файл результатов.

Но, как я уже сказал, это решение очень наивно и работает очень долго (почти неделю и все еще работает …)

Как я мог бы создать новый файл, содержащий все строки с component_id, которые появляются более одного раза в bash, и эффективным способом?

Комментарии:

1. Пользователям настоятельно рекомендуется добавлять свои усилия в свои вопросы, поэтому, пожалуйста, добавьте свои усилия в свой вопрос, спасибо.

2. Спасибо @RavinderSingh13, я добавил усилия, как вы и предлагали

Ответ №1:

С awk и двумя проходами через файл? Первый подсчитывает вхождения последнего поля, второй печатает только дубликаты.

 awk -F, 'FNR == NR { ids[$NF]  ; next }
         ids[$NF] > 1 || FNR == 1' hugefile.csv hugefile.csv > newfile.csv
 

Ответ №2:

1-е решение: использовать однократное чтение Input_file и играть с массивами, чтобы проверить, является ли значение последнего поля больше 1 во всем Input_file.

 awk '
BEGIN{
  FS=","
}
{
  arr[$NF]  
  if(!temparr[$NF]  ){
    first[$NF]=$0
  }
}
arr[$NF]>1{
  if(first[$NF]){
    print first[$NF]
    delete first[$NF]
  }
  print
}
' Input_file
 

2-е решение: чтение всего Input_file и получение всех строк и значений последних полей в массивы и игра с ними в END блоке awk once Input_file завершается чтением.

 awk '
BEGIN{
  FS=","
}
{
  arr[$NF]  
  if(!arr1[$NF]  ){
    arr2[  count]=$NF
  }
  val[$NF]=(val[$NF]?val[$NF] ORS:"")$0
}
END{
  for(i=1;i<=count;i  ){
    if(arr[arr2[i]]>1){
      print val[arr2[i]]
    }
  }
}' Input_file
 

ПРИМЕЧАНИЕ: Мое 3-е решение состояло в том, чтобы передать Input_file 2 раза, awk который уже описан Шоном в его ответе 🙂 поэтому удалил его отсюда. Также они тестируются с показанными образцами, а НЕ с огромным набором данных, к вашему сведению.

Ответ №3:

Вот способ сделать это, не считывая весь файл в память сразу в awk, при этом имея возможность обрабатывать входные данные, поступающие из канала или файла (так что это будет работать, даже если ввод был выводом какой-либо другой команды, без создания временного файла, содержащего все входные данные) исохранение исходного порядка ввода и заголовка.

Ввод из файла:

 $ awk 'BEGIN{FS=OFS=","} {print (NR>1), $NF,   cnt[$NF], NR, $0}' file |
    sort -t, -k1,1n -k2,2 -k3,3nr |
    awk -F, '$2!=p2{p2=$2; p3=$3} (NR==1) || (p3>1)' |
    sort -t, -k4,4n |
    cut -d, -f5-
entity,entity_type,component_id
lalal@hotmail.com,email,2222
15158112233,phone,3333
15158990000,phone,2222
hello,world@gmail.com,email,3333
fds_213445,device,3333
 

или ввод из канала:

 $ cat file |
    awk 'BEGIN{FS=OFS=","} {print (NR>1), $NF,   cnt[$NF], NR, $0}' |
    sort -t, -k1,1n -k2,2 -k3,3nr |
    awk -F, '$2!=p2{p2=$2; p3=$3} (NR==1) || (p3>1)' |
    sort -t, -k4,4n |
    cut -d, -f5-
entity,entity_type,component_id
lalal@hotmail.com,email,2222
15158112233,phone,3333
15158990000,phone,2222
hello,world@gmail.com,email,3333
fds_213445,device,3333
 

Обратите внимание, что этот подход позволяет вам получать входные данные из канала, а не только из файла, поэтому вы можете передавать в него выходные данные другой команды, если хотите. В приведенном выше примере требуется обрабатывать только sort весь ввод сразу, и он предназначен для этого с помощью подкачки по запросу и т. Д. Поэтому Крайне маловероятно, что возникнут какие-либо проблемы с обработкой большого ввода.

Вот что делает скрипт по шагам, чтобы вы могли видеть, как это работает:

 $ awk 'BEGIN{FS=OFS=","} {print (NR>1), $NF,   cnt[$NF], NR, $0}' file
0,component_id,1,1,entity,entity_type,component_id
1,1111,1,2,bla@gmail.com,email,1111
1,2222,1,3,lalal@hotmail.com,email,2222
1,3333,1,4,15158112233,phone,3333
1,2222,2,5,15158990000,phone,2222
1,3333,2,6,hello,world@gmail.com,email,3333
1,4444,1,7,1327168,phone,4444
1,3333,3,8,fds_213445,device,3333
 
 $ ... | sort -t, -k1,1n -k2,2 -k3,3nr
0,component_id,1,1,entity,entity_type,component_id
1,1111,1,2,bla@gmail.com,email,1111
1,2222,2,5,15158990000,phone,2222
1,2222,1,3,lalal@hotmail.com,email,2222
1,3333,3,8,fds_213445,device,3333
1,3333,2,6,hello,world@gmail.com,email,3333
1,3333,1,4,15158112233,phone,3333
1,4444,1,7,1327168,phone,4444
 
 $ ... | awk -F, '$2!=p2{p2=$2; p3=$3} (NR==1) || (p3>1)'
0,component_id,1,1,entity,entity_type,component_id
1,2222,2,5,15158990000,phone,2222
1,2222,1,3,lalal@hotmail.com,email,2222
1,3333,3,8,fds_213445,device,3333
1,3333,2,6,hello,world@gmail.com,email,3333
1,3333,1,4,15158112233,phone,3333
 
 $ ... | sort -t, -k4,4n
0,component_id,1,1,entity,entity_type,component_id
1,2222,1,3,lalal@hotmail.com,email,2222
1,3333,1,4,15158112233,phone,3333
1,2222,2,5,15158990000,phone,2222
1,3333,2,6,hello,world@gmail.com,email,3333
1,3333,3,8,fds_213445,device,3333
 
 $ ... | cut -d, -f5-
entity,entity_type,component_id
lalal@hotmail.com,email,2222
15158112233,phone,3333
15158990000,phone,2222
hello,world@gmail.com,email,3333
fds_213445,device,3333
 

Комментарии:

1. Разве sorts для каждого не требуется все в памяти?

2. @keithpjolley нет, это то, что я упомянул в тексте, ...only sort has to handle the whole input at once and it's designed to do so by using demand paging, etc.... поэтому не обязательно сохранять все это в памяти сразу.

Ответ №4:

Я не уверен, что вы подразумеваете под решением «bash». bash — неподходящий инструмент для такого рода задач — не говоря уже о том, что кто-то не придумает элегантное решение для всех bash… В любом случае, поскольку awk ответы уже появились, я решил попробовать решение на python. При этом весь файл загружается в память, но большинство компьютеров в 2020 году должны нормально обрабатывать файл объемом 17 ГБ. Это делает один проход при чтении файла.

 python3 -c "import csv,collections;d=collections.defaultdict(list);[d[r[-1]].append(r) for r in csv.reader(open('hugefile.csv'))];[print(','.join(r)) for (k,v) in d.items() for r in v if len(v)>1]"
 

Разбито:

 import csv,collections

#Create a defaultdict that accepts lists:
d=collections.defaultdict(list)

# For each row in the csv file append the row to the dict with
# the last field (id) as the key:
[d[r[-1]].append(r) for r in csv.reader(open('hugefile.csv'))]

# Print each value in the dict if value if the value has more than one row in it.
[print(','.join(r)) for (k,v) in d.items() for r in v if len(v)>1]
 

Вывод с вашими примерами данных:

 lalal@hotmail.com,email,2222
15158990000,phone,2222
15158112233,phone,3333
hello,world@gmail.com,email,3333
fds_213445,device,3333