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

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

## ソースコード
[mototoke/shell-extract-text](https://github.com/mototoke/shell-extract-text)にshell一式まとめてます。

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

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

```shell: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
```

この[記事](https://stackoverflow.com/questions/29253591/generate-large-csv-with-random-content-in-bash)を参考に`100万行`のcsv作ります。

```
sh generate-dummy-data2.sh
```

実行すると↓みたいなデータが作られます。
1000で割り切れる時だけ2列目に`DUMMY`が入ります。

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

```shell: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だと遅い](https://eel3.hatenablog.com/entry/20141026/1414292281)らしく、実際には1000万行以上のデータに対して抽出処理を掛けないといけなかったのでこのままでは使い物になりませんでした。



### そこそこ速い抽出

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

```shell:fast-extract-grep.sh
grep $search_word $input_file > $output_file
```

```shell:fast-extract-sed.sh
sed -ne "/$search_word/p" $input_file > $output_file
```

```shell:fast-extract-awk.sh
awk -v search="$search_word" '$0~search {print}' ${input_file} > $output_file
```

どれもだいたい1秒程度で処理が終わります。
ループ使わないとものすごく速い！

実行結果はどれもこんな感じです。
おびただしい数の`DUMMY`達がそこに。

```csvs
...
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


### おまけ

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

```shell: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`達がそこに。

```csvs
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
```