はじめに
思い出した時に投稿する私がもととけです。
業務でAWS触ってる最中、突如舞い降りたshellによるテキスト解析作業(全然別件)の為にちょっと試したことを記事に残しておきます。
やりたいことはそこそこ大きいテキストファイルから特定の文字列を含む行を抽出して別ファイルに吐き出す
という事です。
ソースコード
mototoke/shell-extract-textにshell一式まとめてます。
環境
# 環境用意するの面倒だったのでdocker使ってます shellが動く環境なら何でもOK
docker
centos:8
使用コマンド:sed, grep, awk
ダミーデータ生成
ひとまずダミーデータを作ります。
num=${1:-1000000}
hexdump -v -e '5/1 "%02x""\n"' /dev/urandom |
awk -v OFS=',' '
{
if(NR % 1000 == 0) { print substr($0, 1, 8), "DUMMY", int(NR * 32768 * rand()) }
else { print substr($0, 1, 8), substr($0, 9, 2), int(NR * 32768 * rand()) }
}
' |
head -n "$num" > ./input/input-data.csv
この記事を参考に100万行
のcsv作ります。
sh generate-dummy-data2.sh
実行すると↓みたいなデータが作られます。
1000で割り切れる時だけ2列目にDUMMY
が入ります。
...
998f63ae,12,3071765
516018d6,45,12062390
9851e74a,2f,14441821
774c8a79,ff,3155943
98430c8b,6e,13258884
+ 569d4156,DUMMY,17739939
cbfcbea3,ed,1097453
1d002b2b,b5,4841533
4b907af2,9d,21845406
4f5e7e92,20,21717524
...
抽出
ダミーデータ内のDUMMY
を抜き出して別ファイルに出力するshellを考えます。
遅い抽出
普段、shellを書かないのでDUMMY
がある行番号取得して、その行番号分ループ回して出力すればいいやと思って書いたのが下のコードです。
input_file="./input/input-data.csv"
output_file="./output/output-data.csv"
search_word="DUMMY"
# 検索文字列の行を出力
input_file_search_result=`grep -n "${search_word}" "${input_file}" | sed -e 's/:.*//g'`
for i in ${input_file_search_result[@]}
do
echo `sed -n "${i}P" "${input_file}"` >> $output_file
done
↓でDUMMY
の行番号を配列で取得して
input_file_search_result=`grep -n "${search_word}" "${input_file}" | sed -e 's/:.*//g'`
行番号抜き出して$output_file
に出力してます。
for i in ${input_file_search_result[@]}
do
echo `sed -n "${i}P" "${input_file}"` >> $output_file
done
他の言語ならそこまで気にならないんですがshellでやるとめたんこ遅かったです。
私の環境でだいたい1分くらいかかります。
どうもループ回して外部コマンド(sed)を呼び出すのがshellだと遅いらしく、実際には1000万行以上のデータに対して抽出処理を掛けないといけなかったのでこのままでは使い物になりませんでした。
そこそこ速い抽出
以下のようなコマンドを試しました。
grep $search_word $input_file > $output_file
sed -ne "/$search_word/p" $input_file > $output_file
awk -v search="$search_word" '$0~search {print}' ${input_file} > $output_file
どれもだいたい1秒程度で処理が終わります。
ループ使わないとものすごく速い!
実行結果はどれもこんな感じです。
おびただしい数のDUMMY
達がそこに。
...
569d4156,DUMMY,17739939
53506cc7,DUMMY,37314822
e4daf517,DUMMY,48645248
e4758e55,DUMMY,73777394
a89f7b1f,DUMMY,72139820
e095fdad,DUMMY,68996781
341ce0dc,DUMMY,189298041
c7c16c35,DUMMY,202945408
21fab736,DUMMY,145991807
...
終わりに
実行時間はだいたいこんな感じでした。
超高速化を考えているわけではないのでだいたいこれくらい速くなるんだなぁというのが分かったのでOKです。
コマンド | 実行時間 |
---|---|
sed(ループあり) | 58s |
grep(ループなし) | 1s未満 |
sed(ループなし) | 1s未満 |
awk(ループなし) | 1s未満 |
結論:shellではループしないやり方を考えよう。
参考URL
以下のURLを参考にしました。感謝感謝。
https://stackoverflow.com/questions/29253591/generate-large-csv-with-random-content-in-bash
https://eel3.hatenablog.com/entry/20141026/1414292281
https://qiita.com/hirohiro77/items/7fe2f68781c41777e507
https://www.jh4vaj.com/archives/24778#%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%8C%E3%81%AA%E3%81%84%E8%A1%8C%E3%81%A0%E3%81%91%E3%82%92%E5%87%A6%E7%90%86
https://stackoverflow.com/questions/23360469/variable-pattern-matching-awk
https://bi.biopapyrus.jp/os/linux/grep.html
おまけ
折角なので文字列置換も考えてみました。
input_file="./output/output-data.csv"
output_file="./output/replace-data.csv"
search_word=",DUMMY,"
replace_word=",NODUMMY,"
# 置換結果を別ファイルに出力
awk -v search=${search_word} -v replace=${replace_word} '{ sub( search, replace ,$0);print $0 }' ${input_file} > $output_file
おびただしい数のNODUMMY
達がそこに。
569d4156,NODUMMY,17739939
53506cc7,NODUMMY,37314822
e4daf517,NODUMMY,48645248
e4758e55,NODUMMY,73777394
a89f7b1f,NODUMMY,72139820
e095fdad,NODUMMY,68996781
341ce0dc,NODUMMY,189298041
c7c16c35,NODUMMY,202945408
21fab736,NODUMMY,145991807
ba60881c,NODUMMY,177764255