csvファイルを解析して、列値の一致する文字セットに基づいて行をフィルタリングします。

csvファイルを解析して、列値の一致する文字セットに基づいて行をフィルタリングします。

次のcsvファイルを検討してください。

A,3300   
B,8440   
B,8443   
B,8444 
C,304
C,404  
M,5502   
M,5511

実際のcsvファイルはかなり大きい(約60,000行)。問題を説明するために小さなバージョンのみを提供しました。

2番目のフィールドに基づいて行をフィルタリングして、一致する文字セットを持つ行を単一の行にグループ化するスクリプトを作成する必要があります(2番目のフィールドを一致する文字セットに置き換えます)。

つまり、上記のcsvファイルでは、次の出力が期待されます。

A,3300   
B,844  
C,304
C,404 
M,55   

2番目のcsvフィールドの内容だけがスクリプトの目的に関連しています。したがって、他のフィールドで発生するすべての一致/不一致は、ファイル内のままでなければなりません。

これにawkは役に立ちますか?それとも別の組み込み機能がありますか?どんな助けでも大変感謝します。

答え1

awk2つの文字列間の共通の開始文字を見つけるために小さな関数を作成しました。

awk '
BEGIN{OFS=FS=","}
function common_chars(a,b, o){
    split(a,asplit,"")
    split(b,bsplit,"")
    n=1
    while (asplit[n]==bsplit[n]){
        o=o""asplit[n]
        n++
    }
    return o
}
s[$1] {v[$1]=common_chars(v[$1],$2)}
!s[$1] {v[$1]=$2;s[$1]=1 }
END {for(a in v){print a,v[a]}}
' file

表示されない場合$1(状態はに保存されます)、配列にs[$1]保存されます。表示されている場合は、関数の戻り値をそれ自体と間に設定してください。この関数は、最初の文字で不一致が見つかるまで個々の文字に対してwhileループを実行します。$2v[$1]=$2v[$1]$2

forC,404そしてC,304それは印刷されますC,

出力:

A,3300   
B,844
C,
M,55

答え2

60,000行の場合は少し遅くなりますが、実行可能に見えます。するいいえここに引用符を入れてください$line

スクリプトのどこかに処理するデータがより多く表示されるバグがあるという奇妙な感じがまだあります。

$ sort -u testfile | datamash -t, -g1 collapse 2  \
| tr ',' ' ' | while read line ; do ./my_filter $line ; done
A,3300
B,844
C,304
C,404
M,55

データを前処理しdatamashてソートされたデータを取得するには、my_filter1行ずつ入力するだけです。

$ sort -u testfile | datamash -t, -g1 collapse 2 
A,3300
B,8440,8443,8444
C,304,404
M,5502,5511

それはmy_filter

$ cat my_filter
#!/bin/bash
_longest_match () {
  if ((${#1}>${#2})); then
    long="$1" short="$2"
  else
    long="$2" short="$1"
  fi

  lshort=${#short}
  score=0
  for ((l=score+1;l<=lshort;++l)); do
    sub="${short:0:l}"

    [[ $long != $sub* ]] && break
    subfound="$sub" score="$l"
  done

  if ((score)); then
    printf '%s\n' "$subfound"
  fi
} # ----------  end of function _longest_match  ----------


_output () {
  for item in $(echo "$@"|tr ' ' '\n' | sort -u) ; do
    printf '%s,%s\n' "$key" "$item"
  done
} # ----------  end of function _output  ----------

declare -A matches
declare -A no_matches

key=$1
shift

for item in $( printf '%s\n' "$@"| sort -nr ); do
  if [ -z "$one" ]; then
    one=$1
    two=${2:-$1}
    shift 2
  else
    two=$1
    shift
  fi

  three=$(_longest_match $one $two)

  [ ${#three} -gt 0 ] && matches[$key]+="$three " || no_matches[$key]+="$one $two "
  [ ${#three} -gt 0 ] && one="$three" || one="$two"
done

  _output "${matches[@]} ${no_matches[@]}" | sort -u

_longest_matchインスピレーションを見つけました。https://stackoverflow.com/a/23297950

テストファイルのデュアルエントリを使用していくつかの追加テストを実行しました。

$ cat testfile.new 
A,3300
B,8440
B,8440
U,3
U,7
U,7
U,73
B,8440
B,8443
B,8444
B,976
C,304
C,404
M,5502
M,5511

結果:

$ sort -u testfile | datamash -t, -g1 collapse 2  \
| tr ',' ' ' | while read line ; do ./my_filter $line ; done
A,3300
B,844
B,976
C,304
C,404
M,55
U,3
U,7

予想した結果と同じですか?

答え3

使用awk:

BEGIN { OFS=FS="," }

prefixlength[$1] == "" {
        # First time seeing this label.
        # Remember the full string and its length.

        prefix[$1] = $2
        prefixlength[$1] = length($2)
        next
}

{
        # Compare the current string to the (current) longest
        # prefix related to this label. Update the prefix length
        # to the longest common prefix length.

        for (i = 1; i <= prefixlength[$1]; ++i)
                if (substr(prefix[$1],i,1) != substr($2,i,1)) {
                        prefixlength[$1] = i-1
                        break
                }
}

END {
        # Output labels and their longest prefix.

        for (i in prefix)
                print i, substr(prefix[i],1,prefixlength[i])
}

指定された入力に対して以下を行います。

$ awk -f script file
A,3300
B,844
C,
M,55

計算された最長のプレフィックス長がゼロの場合は空のプレフィックスが表示されるため、この特別な場合にすべての文字列を表示する必要がある場合は、コードを少し変更する必要があります。

BEGIN { OFS=FS="," }

prefixlength[$1] == "" {
        # First time seeing this label.
        # Remember the full string and its length.

        prefix[$1] = $2
        prefixlength[$1] = length($2)
        next
}

{
        # Remember all found strings in a sort of "array". The
        # strings added after the first will only ever be used
        # if the prefix length ends up as zero.

        prefix[$1] = prefix[$1] SUBSEP $2
}

{
        # Compare the current string to the (current) longest
        # prefix related to this label. Update the prefix length
        # to the longest common prefix length.

        for (i = 1; i <= prefixlength[$1]; ++i)
                if (substr(prefix[$1],i,1) != substr($2,i,1)) {
                        prefixlength[$1] = i-1
                        break
                }
}

END {
        # Output labels and their longest prefix. If the prefix length
        # is zero for a label, output all collected strings as separate
        # lines.

        for (i in prefix)
                if (prefixlength[i] > 0)
                        print i, substr(prefix[i],1,prefixlength[i])
                else {
                        n = split(prefix[i],a,SUBSEP)
                        for (j = 1; j <= n; ++j)
                                print i, a[j]
                }
}

テスト:

$ awk -f script file
A,3300
B,844
C,304
C,404
M,55

関連情報