2
2

More than 1 year has passed since last update.

シェルスクリプトでテキストの文字列抽出

Posted at

はじめに

思い出した時に投稿する私がもととけです。
業務でAWS触ってる最中、突如舞い降りたshellによるテキスト解析作業(全然別件)の為にちょっと試したことを記事に残しておきます。
やりたいことはそこそこ大きいテキストファイルから特定の文字列を含む行を抽出して別ファイルに吐き出すという事です。

ソースコード

mototoke/shell-extract-textにshell一式まとめてます。

環境

# 環境用意するの面倒だったのでdocker使ってます shellが動く環境なら何でもOK
docker
centos:8
使用コマンド:sed, grep, awk

ダミーデータ生成

ひとまずダミーデータを作ります。

generate-dummy-data2.sh
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がある行番号取得して、その行番号分ループ回して出力すればいいやと思って書いたのが下のコードです。

slow-extract.sh
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万行以上のデータに対して抽出処理を掛けないといけなかったのでこのままでは使い物になりませんでした。

そこそこ速い抽出

以下のようなコマンドを試しました。

fast-extract-grep.sh
grep $search_word $input_file > $output_file
fast-extract-sed.sh
sed -ne "/$search_word/p" $input_file > $output_file
fast-extract-awk.sh
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

おまけ

折角なので文字列置換も考えてみました。

replace.sh
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
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2