-1

シェルスクリプトを使用して、一連の大きなファイルをループしています。

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

4 に答える 4

3

これは確かに、bash でより効率的に実行できます。

例を挙げるとecho foo | headfork()呼び出しを行い、サブシェルを作成し、パイプラインをセットアップし、外部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

(行が、たとえば、-eまたはの場合、望ましくない動作をする可能性があるため、エコーを使用しないでください-n)。

あるいは、可能な出力ファイルの数が少ない場合は、事前に異なる FD でそれらをすべて開き (選択した他のより大きな数字に置き換えます4)、事前に開いたファイルの 1 つに条件付きで出力することができます。ファイルを開いたり閉じたりするのはコストがかかります -- それぞれclose()強制的にディスクにフラッシュされます -- ですから、これはかなりの助けになるはずです。

于 2012-05-15T16:31:49.117 に答える
2

それをスピードアップするためのいくつかのこと:

  1. 最初の文字を取得するために echo/head を使用しないでください。行ごとに少なくとも 2 つの追加プロセスを生成しています。代わりに、bash のパラメーター展開機能を使用して最初の文字を取得します。

  2. if-elif を使用$firstして、毎回すべての可能性をチェックしないようにします。$firstさらに良いことに、bash 4.0 以降を使用している場合は、各行の大きな if ステートメントでチェックするのではなく、連想配列を使用して出力ファイル名を保存します 。

  3. 連想配列をサポートするバージョンの bash がない場合は、if ステートメントを次のように置き換えます。

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

しかし、次のことをお勧めします。名前が指定されていない場合$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
于 2012-05-15T16:32:14.403 に答える
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 では>、最初にファイルを切り捨て、その後追加します。切り詰めたくない既存のファイルがある場合は、それを変更します>>(ただし、それ以外の場合は使用しないでください>>)。

于 2012-05-15T16:31:55.997 に答える
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)
    }
'
于 2012-05-15T17:13:28.690 に答える