2012-05-15 2 views
-1

Я зацикливание над серией больших файлов со сценарием оболочки:Awk: цикл и сохранить разные строки в разные файлы?

i=0 
while read line 
do 

    # get first char of line 
    first=`echo "$line" | head -c 1` 

    # make output filename 
    name="$first" 
    if [ "$first" = "," ]; then 
     name='comma' 
    fi 
    if [ "$first" = "." ]; then 
     name='period' 
    fi 

    # save line to new file 
    echo "$line" >> "$2/$name.txt" 

    # show live counter and inc 
    echo -en "\rLines:\t$i" 
    ((i++)) 

done <$file 

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

Это слишком медленно.

5 000 строк занимает 128 секунд.

На этом уровне у меня есть твердый месяц обработки.

Будет ли awk быстрее работать здесь?

Если да, то как я вписываю логику в awk?

+1

'$ []' является устаревшим, используйте '((я ++)) или' ((я + = 1)) '. Кроме того, когда вы 'echo' переменная (и чаще всего используете переменную), вы должны ее процитировать:' echo '$ LINE "'. И лучше всего использовать имена переменных в нижнем регистре или смешанном регистре, чтобы избежать потенциального столкновения имен с переменными оболочки или среды. –

+0

@DennisWilliamson спасибо. Обновлено. – HappyTimeGopher

ответ

3

Это может быть сделано более эффективно в bash.

Чтобы дать вам пример: echo foo | head делает fork() вызов, создает подоболочку, устанавливает трубопровод, запускает внешнюю программу head ... и нет никаких оснований для этого вообще.

Если вы хотите, первый символ строки, без какого-либо неэффективно отвода с подпроцессами, это так просто, как это:

c=${line:0:1} 

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

То есть - предобработка с родом (как путем замены <$file с < <(sort "$file")) и выполните следующие действия каждый раз через петлю, повторное открытие выходного файла только условно:

if [[ $name != "$current_name" ]] ; then 
    current_name="$name" 
    exec 4>>"$2/$name" # open the output file on FD 4 
fi 

... а затем добавить к открытый дескриптор файла:

printf '%s\n' "$line" >&4 

(не используя эхо, потому что он может вести себя нежелательную если линия, скажем, -e или -n).

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

+0

Вам не нужна новая линия? 'printf '% s \ n'" $ line "> & 4' –

+0

@DennisWilliamson Совершенно верно, спасибо. –

+0

Большое спасибо @CharlesDuffy. Вышеизложенные идеи приводят к 35-кратному увеличению скорости. – HappyTimeGopher

2
#!/usr/bin/awk -f 
BEGIN { 
    punctlist = ", . ? ! - '" 
    pnamelist = "comma period question_mark exclamation_mark hyphen apostrophe" 
    pcount = split(punctlist, puncts) 
    ncount = split(pnamelist, pnames) 
    if (pcount != ncount) {print "error: counts don't match, pcount:", pcount, "ncount:", ncount; exit} 
    for (i = 1; i <= pcount; i++) { 
     punct_lookup[puncts[i]] = pnames[i] 
    } 
} 
{ 
    print > punct_lookup[substr($0, 1, 1)] ".txt" 
    printf "\r%6d", i++ 
} 
END { 
    printf "\n" 
} 

BEGIN блок создает ассоциативный массив, так что вы можете сделать punct_lookup[","] и получить «запятая».

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

+0

Выполняет ли файлы кэша awk или выполняет ли новая пара вызовов open() и close() в каждой строке? –

+1

Я просто 'strace'd, и он открывает (и держит открытым) отдельные дескрипторы файлов для каждого файла. На самом деле, похоже, он кэширует записи в буфере 4K. –

+0

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

2

Несколько вещей, чтобы ускорить его:

  1. Не используйте эхо/голову, чтобы получить первый символ. Вы - , порождающий не менее двух дополнительных процессов в строке. Вместо этого использует возможности расширения параметров bash для получения первого символа.

  2. Использовать if-elif, чтобы избежать проверки $first против всех возможностей каждый раз. Еще лучше, если вы используете bash 4.0 или новее, используйте ассоциативный массив для хранения имен выходных файлов, а не для проверки на $first в большом операторе if для каждой строки.

  3. Если у вас нет версии bash, которая поддерживает ассоциативные массивы , замените ваши операторы if следующим.

    if [[ "$first" = "," ]]; then 
        name='comma' 
    elif [[ "$first" = "." ]]; then 
        name='period' 
    else 
        name="$first" 
    fi 
    

Но следующий предлагается. Обратите внимание на использование $REPLY в качестве переменной по умолчанию, используемой read, если имя не указано (только FYI).

declare -A OUTPUT_FNAMES 
output[","]=comma 
output["."]=period 
output["?"]=question_mark 
output["!"]=exclamation_mark 
output["-"]=hyphen 
output["'"]=apostrophe 
i=0 
while read 
do 

    # get first char of line 
    first=${REPLY:0:1} 

    # make output filename 
    name=${output[$first]:-$first} 

    # save line to new file 
    echo $REPLY >> "$name.txt" 

    # show live counter and inc 
    echo -en "\r$i" 
    ((i++)) 

done <$file 
+0

Точка условного обозначения - имена верхнего регистра являются условными только для переменных окружения или встроенных модулей, а не для регулярных переменных внутренней оболочки. Кроме того, вы каждый раз открываете выходной файл через цикл, который будет намного дороже, чем вычисление его имени. –

+0

@CharlesDuffy, это то, что я использовал в своем исходном коде, пока Деннис не указал на это, и я изменил его. – HappyTimeGopher

+0

@CharlesDuffy: хорошие моменты. Я изменю имена переменных; Я буду откладывать ваш код для определения сортировки и открытия файлов, когда это необходимо. – chepner

1

Еще одно взятие:

declare -i i=0 
declare -A names 
while read line; do 
    first=${line:0:1} 
    if [[ -z ${names[$first]} ]]; then 
     case $first in 
      ,) names[$first]="$2/comma.txt" ;; 
      .) names[$first]="$2/period.txt" ;; 
      *) names[$first]="$2/$first.txt" ;; 
     esac 
    fi 
    printf "%s\n" "$line" >> "${names[$first]}" 
    printf "\rLine $((++i))" 
done < "$file" 

и

awk -v dir="$2" ' 
    { 
     first = substr($0,1,1) 
     if (! (first in names)) { 
      if (first == ",")  names[first] = dir "/comma.txt" 
      else if (first == ".") names[first] = dir "/period.txt" 
      else     names[first] = dir "/" first ".txt" 
     } 
     print > names[first] 
     printf("\rLine %d", NR) 
    } 
'