Awk:loop&将不同的行保存到不同的文件?

时间:2012-05-15 15:55:41

标签: linux bash shell awk

我正在使用shell脚本循环一系列大文件:

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?

4 个答案:

答案 0 :(得分:3)

这肯定可以在bash中更有效地完成。

举个例子:echo foo | head进行fork()调用,创建子shell,设置管道,启动外部head程序......并且没有理由一点都不。

如果你想要一行的第一个字符,而没有任何低效的子进程错误,它就像这样简单:

c=${line:0:1}

我也会认真考虑对您的输入进行排序,因此您只能在看到新的第一个字符时重新打开输出文件,而不是每次都通过循环。

即 - 使用sort进行预处理(将<$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

(不使用echo,因为如果你的行是-e-n,它会表现得不合理。

或者,如果可能的输出文件数量很少,您可以在前面的不同FD上打开它们(替换我选择4的其他更高的数字),并有条件地输出到其中一个预先打开的文件。打开和关闭文件很昂贵 - 每个close()强制刷新到磁盘 - 所以这应该是一个重要的帮助。

答案 1 :(得分: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中,>第一次截断文件并随后附加。如果您有不想截断的现有文件,请将其更改为>>(但不要使用>>。)

答案 2 :(得分:2)

要加快速度的一些事情:

  1. 不要使用echo / head来获取第一个字符。你是     每行产生至少两个额外的进程。代替,     使用bash的参数扩展工具来获取第一个字符。

  2. 使用if-elif避免检查$first所有内容 可能性     每一次。更好的是,如果您使用的是bash 4.0或更高版本,请使用关联数组     存储输出文件名,而不是检查     $first在每行的if语句中。{/ p>

  3. 如果您没有支持关联的bash版本 数组,用以下代码替换你的if语句。

    if [[ "$first" = "," ]]; then
        name='comma'
    elif [[ "$first" = "." ]]; then
        name='period'
    else
        name="$first"
    fi 
    
  4. 但建议如下。如果没有给出名称(仅限FYI),请注意使用$REPLY作为read使用的默认变量。

    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
    

答案 3 :(得分: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)
    }
'