20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【shellチューニング】覚えておきたいshellを爆速にするレシピ集

Last updated at Posted at 2022-06-11

そのshellまだまだ早くなりますよ。

Linuxコマンドは沢山あります。

ぶっちゃけ大抵のことはforループとgrep,sedでできます。

しかし、それが災いの元。大災害を引き起こすことも。
何気なく書いていたwhile read line×grep
いつの間にか処理対象が増えて数十時間終わらないことも。。。

というか1000万件ほどのデータでもwhile read line×grepなんて終わらない。。
バッチジョブは速攻で終わらせたいですし。お寿司。

リファクタリングがてら、そのshellが早くなるかもしれないTipsを

shellを爆速3分クッキングに近づけるお品書き

Tailは諦めて逆から読み込め! 【tac コマンド】

tacはcatを逆から呼んだコマンドで、名前の通りcatを反対から実施してくれる。

$ cat a.txt
1
2
3

だとすると、

$ tac a.txt
3
2
1

というような使い方になる。
逆順表示っていつ使うの。。。とか思うかもしれないけれど、

例えば1億件とかの大容量ファイルの処理中に処理が中断したとする。
7000万件は処理完了したから、残りの3000万件だけ抜き出したい。。というケースがあれば、

tailコマンドを使うと、tail -30000000 a.txt > b.txtで抜き出すのかもしれない。
しかし、tailコマンドは、その特性上、1億件のファイルを頭からreadして3000万件をwriteする処理だ。
ホットコーヒーがアイスコーヒーになるくらいの時間がかかる。

しかし、tacを使えば、tac | head -30000000 | tac > b.txtと書くことができる。
headコマンドはいい奴で、3000万件ファイルをreadすると直前のtacコマンドにkillメッセージを送ってくれる。その後、tacコマンドで3000万件ファイルをreadして書き出せばよいので、単純計算で4000万件のreadが不要になったということになる。(順序を気にしなくていいファイルなら、後半のtacは不要になる)

※headが直前の処理にkillを送るため、linux上ではエラーメッセージが表示されるかもしれない。それが受け入れられない場合は、tac | head -30000000 | tac > b.txt 2>/dev/nullとかでエラーメッセージを抑制するか、headをawkに変えて実施するといい。

同じ処理を2回流す?そんなのはナンセンス。 【teeコマンド】

teeは標準入力で受け取った内容を、標準出力とファイルに出力してくれるリダイレクトとパイプ処理を両立させてくれるコマンドだ。

$ grep "タナカ" a.txt | tee b.txt | grep "タロウ"  > c.txt

AというファイルをBに加工したものと、AというファイルをBに加工して、それをCに加工したい。。。
A→Bの処理を1行目書いて、B→Cを2行目に書く。。みたいなケースで、

A→B→Cをどうしてもワンライナーで書きたい。。とか、すこしでも処理速度上げたい。。みたいなときに使えるコマンドだ。( -aを使うとファイル追記とか、ファイルを複数引数に入れたりもできる)

ファイル名を変更するなら! 【rename コマンド】

同一ディレクトリ内でファイル名を変更したい時に使うコマンドだ。

$ rename a.txt b.txt

rename {変更したいファイル名} {変更後のファイル名}という感じで使う。(複数ファイルを指定したり、*をつかったりもできる。)

大抵のことコマンドには、mvコマンドがあるから不要じゃん?と思うかもしれないが、
大量にファイル名をリネームしなければいけない時の実行速度が段違いだ。

mvコマンドは内部的にrename処理を行って、失敗したら別ディレクトリにcopy処理でファイルを移すような動きになっている。同一ディレクトリ内のリネームならばrenameコマンドで十分だ。5000件のファイルを一括で移動させるときの実行速度はrenameコマンドの方が100倍以上速い。。。

特定の項目抽出したい!! 【awkコマンド】

"1","りんご","赤い"

例えば、上のようなcsv形式のデータから「りんご」だけ抜き出したいとします。
sed 's/^".*","\(.*\)",".*"$/\1/'とかgrep -o -E '"[0-9]*","'[^\"]*とかでも抽出できますが、
パターンマッチは時間がかかりすぎます。awkを使うと便利です。

awk -F '","' '{print $2}'

これで「りんご」が抜き出せます。
解説を加えると、-Fオプションで文字の区切り文字をしていします。
要は1項目目を"1、2項目目をりんご、3項目目を赤い"と認識させるわけです。
その後、'{print $2}'は2項目目を出力しますよという意味です。
awk -F '","' '{print $3}'は「赤い"」が出力されます。

ちょっとした応用

<number>1</number><fruits>りんご</fruits><color>赤い</color>

この-Fオプションを使うと、上記のようなxml形式のデータから「りんご」だけ抽出できます。

awk -F '<fruits>' '{print $2}' | awk -F '</fruits>' {print $1}'

""を区切り文字にして、2項目目を出力します。
そうすると りんご</fruits><color>赤い</color>が残ります。
その後のパイプで""を区切り文字にして1項目目を出力すると、「りんご」だけ抽出できます。
このパターンマッチはsedよりも圧倒的に早いです。。。おすすめ。。。

特定の項目を置換したい!! 【gsub関数】

"1","りんご","赤いりんご"

上記の3項目目から、「りんご」だけ消したい。。。
sed 's/りんご//g'を使うと2項目目も消えてしまうし、sed 's/りんご"$/"/'もいけてない。。。

そんな時は、gsub関数を使いましょう!

awk -F '","' -v 'OFS=","' '{gsub("りんご","",$3); print}'

-Fは区切り文字ですね。
そして、-v 'OFS=","'は出力時の区切り文字の指定です。
これ指定しないと、区切り文字はデフォルトで半角スペースになります。
「"1 りんご 赤い"」という出力にならないように、区切り文字指定をしましょう。
そして肝心なのはgsub関数です。
gsub("①","②",③);という書き方で、③項目目の①を②に置換するという意味です。
つまり今回は、「"1","りんご","赤い"」が出力されるわけです。

ちょっとした応用

"1","りんご","赤いりんご"
"2","ぶどう,"青いぶどう"

上記のようなデータの3項目目を以下のようにしたい!というとき、

"1","りんご","赤い"
"2","ぶどう","青い"

awk -F '","' -v 'OFS=","' '{gsub("りんご","",$3); print}'
awk -F '","' -v 'OFS=","' '{gsub("ぶどう","",$3); print}'

とそれぞれ投げてもいいんですが、
これが1億件に対しての処理とすると、2回投げると2億件処理するのに等しいです。
やっぱり一回で済ませたい!そんな時は、if文を使いましょう!

awk -F '","' -v 'OFS=","' '{if($3 ~ /りんご/){gsub("りんご","",$3); print} else if ($3 ~ /ぶどう/){gsub("ぶどう","",$3); print}}'

これは3項目目がりんごとパターンマッチしたら3項目目のりんごを削除、
もしくは、3項目目がぶどうとパターンマッチしたら3項目目のぶどうを削除するという意味です。
ワンライナーで書いたので処理も1回で済みます。

ちなみに、if文のなかはパターンマッチなら~ /(文字列)/も使えますし、== (文字列)という感じで文字列一致も行けますよ。

もっと応用してみる

<number>1</number><fruits>りんご</fruits><color>赤いりんご</color>

上記のような文字列があったとき、
awk -F '<color>' -v 'OFS=<color>' '{gsub("りんご","",$2); print}'
と書いてももちろんいいんですが、以下のように区切り文字指定で置換する手もあります。

awk -F '<color>赤いりんご' -v '<color>赤い' '{print $1,$2}'

これが意外に便利です。1項目目の「1りんご」と2項目目の「」の間の区切り文字を「赤いりんご」から「赤い」に変えるというイメージですね。
他の行に影響があるとまずいので、if文を混ぜて以下のようにすると完璧です。

awk -F '<color>赤いりんご' -v '<color>赤い' '{if ($2 != ""){print $1,$2}' else {print $1}'

これは、区切り文字で指定したものがあれば変換しますが、
区切り文字で指定したものがなければ何も変換せずに出力するような意味です。
実用さでいうとこれの方が使いますかね。。。

大量にデータを抜き出したい! 【Joinコマンド】

#fruits.csv
1 りんご
2 ぶどう
3 いちご
4 めろん
5 すいか

#number.csv
1
2
5

のようなデータからNamber 1,2,4のデータだけ抜き出したいとなったとき、
正直、3項目なら3回grepすればいいです。笑
自動化するなら、以下のshをつくることになるかと思います。

cat number.csv | while read line
do
    grep ${line} fruits.csv
done

短いデータならこれでいいんですが、1億件というデータ量になると
for文回した数×1億回データみなきゃなんで、100万回for文回すとかの処理だと1か月消えます。

そこで使いたいのが、joinコマンドです!
sortしたデータじゃないとjoinできないとか制限はありますが、以下のような書き方でいけます。

LANG=C join number.csv fruits.csv

1項目目の数字が一致しているものだけ出力します。LANG=Cはおまじないみたいなものです。
逆に-vオプションを使うと一致していないものだけ出力とか
-aオプションで一致しなくても出力とかもできます。
joinは1億件でも数分で処理してくれるのでまじで便利です。おすすめ。

もはやgrepを加速しよう。 【LC_ALL=C grep -iコマンド】

grepするときに文頭にLC_ALL=C-iオプションをつけてみて下さい。
大文字小文字を無視することにはなりますが、1.2~1.3倍になります。

(おまけ) 便利コマンド

複数コマンドの実行制御

ワンライナーでコマンドを実行したい際に、便利な記法がある。

$ コマンド1 ; コマンド2       ---コマンド1に続いてコマンド2を実行する
$ コマンド1 && コマンド2      ---コマンド1が正常に終了したときのみコマンド2を実行する
$ コマンド1 || コマンド2      ---コマンド1が正常に終了しなかった場合のみコマンド2を実行する

linuxのコマンドを続けて実行したい。。。けど、処理時間が長くて待ち時間が。。。というときに、使えることがあると思う。便利。

nice コマンド

Linux上のコマンドの実行優先度を変更するコマンド。
Cpuあまり使用したくないけど、ずりずり進めたい。。みたいなジョブがあればniceコマンドを使うとよい。

$ nice -n 19 grep "タナカ" a.txt

(-20から19で優先度が指定出来て、数値が大きいほど優先度は低くなる)

参考文献
シェルスクリプトを何万倍も遅くしないためには
シェルスクリプト リファクタリング ~遅いシェルスクリプトが供養されてたので蘇生して256倍に高速化させました~

20
13
1

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
20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?