Разбивать файл на несколько файлов по одной строке за раз

#bash #awk #grep #cut

#bash #awk #grep #вырезать

Вопрос:

У меня есть файл журнала (около 50 тыс. строк) в формате:

 email1@gmail.com:address0:some_details0
email2@gmail.com:address1:some_details1
email1@yahoo.com:address2:some_details2
email2@yahoo.com:address3:some_details3
 

Я пытаюсь прочитать этот файл и разделить его на две папки (gmail.com и yahoo.com ), а затем запишите каждую строку в уникальный файл, названный в честь идентификатора электронной почты. Мой приведенный ниже код работает, но он очень медленный. Может кто-нибудь, пожалуйста, помочь мне сделать это быстрее и эффективнее? Было бы оценено.

 #/bin/sh
grep -hv -P "[^[:ascii:]]" * |
awk -F":" '
    {
        if ($1 ~ /^[[:alnum:]_. -] @[[:alnum:]_.-] .[[:alnum:]]/ amp;amp; NF>1 amp;amp; $NF!="")
        {
            split($1, arr, "@")
            system("mkdir -p "tolower(arr[2]))
            print $0 >> tolower(arr[2])"/"tolower(arr[1])
        }
    }'
 

PS: регулярное выражение — это базовая проверка правильности адреса электронной почты. Я не выполняю чрезмерно тяжелую проверку. Сначала я подумал, что регулярное выражение замедляет мой код, но на самом деле это не так. Даже без регулярного выражения код работает очень медленно. Я думаю, что ввод-вывод замедляет это. Как мы можем улучшить?

Ответ №1:

В основном это порождает новую подоболочку для вызова mkdir один раз на строку ввода, из-за чего ваш код выполняется так медленно. Вместо этого сделайте что-то вроде этого:

 filename = tolower(arr[1])
dirname = tolower(arr[2])
if ( !seen[dirname]   ) {
    system("mkdir -p 47" dirname "47")
}
print > (dirname "/" filename)
 

таким образом, вы создаете подоболочку для вызова mkdir только один раз для каждого каталога.

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

Учитывая это, вот как я бы действительно написал вашу программу:

 #!/usr/bin/env bash

grep -hv -P '[^[:ascii:]]' "${@:--}" |
sort -t':' -k1,1 -s |
awk -F':' '
    !($1 ~ /^[[:alnum:]_. -] @[[:alnum:]_.-] .[[:alnum:]]/ amp;amp; NF>1 amp;amp; $NF!="") { next }
    { curr = tolower($1) }
    curr != prev {
        close(out)
        split(curr, arr, /@/)
        filename = arr[1]
        dirname = arr[2]
        if ( !seen[dirname]   ) {
            system("mkdir -p 47" dirname "47")
        }
        out = dirname "/" filename
        prev = $1
    }
    { print > out }
'
 

Я использовал GNU sort выше -s для «стабильной сортировки», если у вас этого нет и вы заботитесь о том, чтобы относительный порядок строк ввода для данного адреса электронной почты сохранялся в выходных данных, есть другие способы справиться с этим, например awk -v OFS=':' '{print NR, $0}' | sort -t':' -k2,2 -k1,1n | cut -d':' -f2- .

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

1. Привет @Ed. еще раз спасибо за помощь. Я получаю сообщение об ошибке: grep: недопустимое имя символьного класса

2. Ах, так вот почему -P у вас был вызов grep. Я просто скопировал код, который у вас был для этого, но избавился от него, -P поскольку не видел в нем смысла, и теперь добавил его обратно.

Ответ №2:

Вот так:

 awk -F'[@:]' '{system("mkdir -p 47"$2"47");f=$2"/"$1;print>>f;close(f)}' file
 

-F'[@:]' устанавливает разделитель поля ввода в значение или @ или : , что позволяет использовать следующие записи:

 email1 gmail.com address0 some_details0
email2 gmail.com address1 some_details1
email1 yahoo.com address2 some_details2
email2 yahoo.com address3 some_details3
 

Выходное имя файла — это просто второе поле ‘/’ первое поле, когда записи разделяются таким образом. print >> $2"/"$1 добавит текущую запись в этот файл. Если он не существует, awk создаст его.

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