TIL

Did you learn anything today?

Webフロントエンドハイパフォーマンスチューニングの読書メモ

Webフロントエンド ハイパフォーマンス チューニングの読書メモ。

Webブラウザレンダリングエンジン、JavaScriptエンジンについて

ブラウザ レンダリングエンジン JavaScriptエンジン
Chromium (Chrome, Opera, 次世代 Edge) Blink V8
Safari WebKit JavaScript Coer
Firefox Gecko SpiderMonkey
Edge EdgeHTML Chakra
IE Trident Chakra

リソースの取得に用いるネットワークプロトコル

  1. URL に含まれるホスト名の解決
  2. HTTP による取得
    1. TCP 接続の確立
    2. (HTTPS であれば)TLS 接続の確立
    3. HTTP リクエストの送信と HTTP レスポンスの受け取り

IP

ネットワークのノード間のパケットのやり取りを中継するプロトコル

ホスト名の解決に DNS、リソースの取得には HTTP が用いられます。これらはどちらも IP 上で利用されます。

TCP

TCP はコネクションとい概念を持ちます。相手先の IP アドレスとポート番号を指定して接続を確立し、一度接続を確立すると接続が終了するまで、IP と比べると信頼性のあるデータ転送を行えます。

ウェブにおいて、HTTP はこの TCP の上で利用されます。したがって、HTTP でリソースを取得する前に必ず TCP 接続を確立します。

DNS

ホスト名を IP アドレスを変換する作業は、クライアント内の DNSゾルバを通じて行います。DNSゾルバは自身のキャッシュや DNS サーバに問い合わせることでホスト名に紐づく IP アドレスを検索します。


JavaScript による計測

  • Navigation Timing API による計測
  • User Timing API による計測
  • Resource Timing API による計測
  • Frame Timing APIによる計測
  • Server Timing APIによる計測
  • High Resolution Time APIによる計測
  • Perfomance Observer による継続的な計測

Navigation Timing API による計測

ブラウザがページをロードするのに要した処理時間の内訳を知ることができる。

  • window.performance.timing オブジェクト
  • window.performance.navigation オブジェクト

User Timing API による計測

開発者が任意の処理にかかる時間を計測することができる。

Resource Timing API による計測

リソースの取得にかかっている時間の統計情報を得られる。

Navigation Timing API では、ユーザーがどれぐらいブラウザのナビゲーション処理に待たされているかを詳細に知ることができる。ただし、個別のリソースの取得にどう時間がかかっているかについては分からない。

※ Network パネルで見ることができるリソース取得のパフォーマンスの詳細情報とほとんど同じもの。

Frame Timing API による計測

Frame Timing APIはページ描画のフレーム情報を取得するAPI

Navigation Timing APIResource Timing APIはネットワーク処理に関するパフォーマンスの情報だが、これはレンダリングパフォーマンスの情報ということになる。

Server Timing API による計測

Server Timing APIは、レスポンスヘッダのServer-Timingフィールドにサーバーの各処理にかかった時間を含めて、クライアントでそれを参照できるようにしようというAPI

High Resolution Time API による計測

High Resolution Time APIはマイクロ秒単位でパフォーマンスを計測するためのAPI

Date.now()のミリ秒では不十分ということで、細かな計測にはperformance.now()を使うと良い。

Perdoemance Observerによる継続的な計測

ブラウザパフォーマンスを計測するAPIは以下の5つがある。

  • Navigation Timing API
  • User Timing API
  • Resource Timing API
  • Frame Timing API
  • Server Timing API

Performance Observerはこれらの変更を監視するAPI

Javascript の実行のチューニング

  • Page Visibility API


高度なチューニング

ブラウザのレンダリングは4つの工程から成る。

① Loading -> ② Scripting -> ③ Rendering -> ④ Painting

さらに、各工程はそれぞれ以下の処理に細分化される。

  • ① Loading
    • Download
    • Parse
  • ② Scripting
  • ③ Rendering
    • CalculateStyle
    • Layout
  • ④ Painting
    • Paint
    • Rasterize
    • CompositeLayers

① Loading

省略。

② Scripting

省略。

③ Rendering

Calculate style

DOM 要素と CSS ルールを突き合わせて要素ごとのスタイル情報を計算する。

DOM ツリーの構造を変化させたり、DOM 要素の属性を変化させると Calculate Style が引き起こされる。

  1. DOM ツリーの構造の変化ってなに?
  2. appendChild()メソッドや removeChild()メソッド、innerHTML プロパティや textContent プロパティなどを使って DOM ツリーに変化を加えること。

  3. DOM 要素の属性ってなに?

  4. class 属性や id 属性などを変更すること。

アニメーション中にこういうった変更を行う理由はないので避けてください。ある要素の CSS プロパティを変化させたい場合には、 Javascript で次のようにDOM要素をstyle プロパティから直接変更することで DOM 要素と CSS ルールのマッチング処理を避けることができます。

element.style.opacity = '0.8';

Layout

視覚的要素のレイアウト情報を計算する

DOM ツリーの構造を変化させたり、コンテンツを変化させると Layout 処理が引き起こされる。
また、レイアウトの更新を必要とする CSS プロパティを変更すると、Layout 処理が引き起こされます。
Layout 処理が引き起こされると、後続の Paint 処理や Composite 処理もすべて引き起こされます。

Layout を引き起こさずに要素の位置や大きさを変えたい場合には、transform CSS プロパティの値に translate()や scale()を使ってください。

// だめ
element.style.marginTop = '10px';
element.style.left = '100px';

// よい
element.style.transform = 'translate(100px, 10px)';

④ Painting

視覚的要素をレイヤーごとにビットマップ化する。

Paint は、内部で生成されたレイヤーごとに描画命令を生成する処理です。 Paint が引き起こされると、皇族の Resterize と Composite Kayers も実行される。

Layout 処理が引き起こされた場合や、再度 Resterize が必要となる CSS プロパティを変更すると引き起こされます。

ペイントを引き起こす CSS プロパティ

  • color
  • border-color
  • backgorund-color
  • backgorund-image
  • z-index

ペイントを引き起こさない CSS プロパティ

  • opacity
  • transform

原理的にそれが難しい場合、たとえば要素の色を変更したり背景画像を動かしたりするようなアニメーションの場合はここで最適化をストップさせることになる。

Composite Layers

描画されたレイヤーを合成して最終的なレンダリング結果を生成する。

translateZ ハックを使用してレイヤーの合成に GPU を使うと最適化できる。

.target {
  transform: translateZ(0);
}

このハックを適用した上でアニメーションを行うとほとんんど GPU でのみ描画される高速なアニメーションを実装できる

var element = document.getElementByID('animation-target');

// アニメーションを始める前にGPUで合成されるレイヤーを生成する、
element.style.transform = 'translateZ(0)';

var i = 0;
var loop = function() {
  // アニメーション中にもGPUでの合成が解除されないようにtranslateZ(0)をつける。
  requestAnimationFrame(function() {
    element.style.transform = 'translateZ(0) translate2d(' + (i++) + 'px,0px');
    loop();
  });
};
loop();

これらの再レンダリング時のフェーズを減らすことは非常に重要。

参考文献

ブラウザレンダリングを理解するため簡単にまとめてみた - Qiita

パフォーマンスに関する各種ブラウザAPI - EagleLand

知っておきたいブラウザについての基礎入門 - Speaker Deck

シェルスクリプトまとめ2

定期的に行う作業を自動実行

crontab・・・「crond に実行してほしいコマンドとその実行日時のセット(cronjob)」を管理するコマンド

cronjob を一覧表示する

$ sudo crontab -l

シェルスクリプトを実行するときは、ホームディレクトリからの相対パスか、絶対パスを記述する

$ crontab -e

MAILTO=chino@gmail.com # cronjobの実行結果が指定のメールアドレスに悪られる.(MTAが設定済みの場合)
45 11 * * 1 /home/chino/scripts/analyze_log.sh > /tmp/result_$(date +%Y-%m-%d).txt

公開鍵認証

公開鍵の登録

公開鍵の登録 = 公開鍵のファイルを所定の位置に置くこと

公開鍵のファイルをサーバ上にコピーして、その鍵ペアで認証したいユーザのホームの直下の.sshディレクトリにauthorized_keysという名前で置く。

$ ssh-keygen
$ scp id_rsa.pub chino@192.168.1.2:/tmp/
$ ssh chino@192.168.1.2

$ sudo mkdir -p ~chiya/.ssh/
$ sudo mv /tmp/id_rsa ~chiya/.ssh/authorized_keys # 認証したユーザ以外では読み書きできないように「.ssh」ディレクトリと「authorized_keys」のパーミッションを設定すれば、公開鍵の登録は完了。
$ sudo chown -R chiya:chiya ~chiya/.ssh/ # 所有者をそのユーザに、グループもそのユーザのグループに設定する。
$ sudo chmod 700 ~chiya/.ssh # 所有者のみに読み書き・閲覧を許可
$ sudo chmod 600 ~chiya/.ssh/authorized_keys # 所有者のみに読み書きを許可

公開鍵を簡単に登録したい

$ ssh-copy-id -i ~/.ssh/id_rsa.pub chino@192.168.1.2 # 公開鍵を登録するサーバ側のユーザを明示的に指定する。

-i: 登録したい公開鍵のパスを指定する。

ログインに成功すると、サーバ上のユーザのauthorized_keysに公開鍵が登録される。

※ パスワード認証化、登録済みの鍵ペアでそのユーザとしてログインできることが前提。

定時処理で自動的に scp したい

パスフレーズを設定しない。その代わりに実行に制限を与える。

$ scp ~/.ssh/logdl.pub chino@192.168.1.2:/tmp/
$ ssh chino@192.168.1.2
$ cat /tmp/logdl.pub >> ~/.ssh/authorized_keys

登録が完了したら設定を変更する。

$ vim ~/.ssh/authorized_keys

公開鍵の行の先頭に関連付けるコマンド列を追記する。

command="scp -f /var/log/nginx/access.log"

ファイルをダウンロードするとき

$ scp -f サーバ上のファイルのパス

ファイルをアップロードするとき

$ scp -t サーバ上のファイルのパス

試す。

$ scp -i ~/.ssh/logdl chino@192.168.1.2:/var/log/nginx/access.log /tmp/
#!/bin/bash

server_log=/var/log/nginx/access.log
key=~/.ssh/logdl
servers="192.168.1.51 192.168.1.52 192.168.1.53"
for server in servers
do
  log=/tmp/${server}-access.log
  result=/tmp/${server}-result.csv
  scp -i $key chino@${server}:${server_log} $log
  ~/scripts/analyze_access.sh $log $result
done

※ この方法は、簡単に設定できるが融通が利かない。scp の実行内容を変えるときはその都度、authorized_keys を修正しないといけない。パスが異なるファイルをいくつもダウンロードしたいときも、その分だけ鍵ペアを用意しないといけない。

複数のサーバのファイルを効率よく収集したい

よくわからん。

条件に当てはまるログの行数を収集したい

ページごとのアクセス数ではなく合計が求めたい。→ wcを使おう

行数

$ cat /tmp/access.log | wc -l

単語数

$ cat /tmp/access.log | wc -w

文字数

$ cat /tmp/access.log | wc -m

grep で見つかった業の数

$ zcat /oldlog/2018/accesslog.log.1.gz | grep "/campaign" | wc -l

複数 ver

#!/bin/bash

for lof in /oldlog/2018/accesslog.*.gz
do
    zcat $log | grep "/campaign" | wc -l
    # zcat $log | grep "Jan/2018" | wc -l # 1月のログだけ
    # zcat $log | grep -E "([23][0-9]/Jan|0[0-9]|10/Feb)/2018" | wc -l # 2018年1月20から2月10日まで。複数の月をまたがるような日付の範囲は正規表現を使う。
done
#!/bin/bash

total_count=0
for lof in /oldlog/2018/accesslog.*.gz
do
    pattern="([23][0-9]/Jan|0[0-9]|10/Feb)/2018"
    count=$(zcat $log | grep "/campaign" | grep -E $pattern | wc -l)
    total_count=$(($total_count + $count)) # 算術展開。bashで計算機能を使うときは$(())と書く
done
echo "合計:${total_count}"

複数のテキストファイルを一括編集したい

sedを使う。→ テキストを編集するツールの一種。ストリームエディタ。

$ sed -e "s/置換前の文字列/置換後の文字列/"

$ cat rabbithouse-coffee.csv | sed -e "s/購入済み/使用済み" > rabbithouse-coffee.csv.updated

※ 1 つのコマンド列の中で「ファイルを開く操作」と「そのファイルに書き込む操作」を同時にやると、ファイルが消失する。

#!/bin/bash
source=/tmp/rabbithouse-coffee.csv
backup=${source}.$(date +%Y-%m-%d).bak
mv $source $backup
cat $backup | sed -e "s/購入済み/使用済み/" > $source

正規表現を使って複数のテキストファイルを一括編集したい。

なるほど、分からん。

数字をまとめて

"s/category([0-9]+/category-\1-/"

category100a → category-100-a

category200c → category-200-c

否定形

"s#(://[^:]+)3000/#\1:13000/#"

スラッシュを正規表現の中に含めるので区切り文字にはスラッシュ以外の文字を使うとよい。

http://192.168.1.15:3000/http://192.168.1.15:13000/

http://rabbit-house:3000/http://rabbit-house:13000/

先頭だけ

字下げの深さが違う場合もまとめて置換

"s/^( +)* /\1!! /"

* コーヒー
  * キリマンジャロ
  * ブルーマウンテン
  * カプチーノ
* 軽食
  * サンドイッチ
  * オムライス
  * ハンバーグセット \* ライス/パン

!! コーヒー
!! キリマンジャロ
!! ブルーマウンテン
!! カプチーノ
!! 軽食
!! サンドイッチ
!! オムライス
!! ハンバーグセット \* ライス/パン

prettier のせいで勝手に整形される・・・

末尾

句点があったら、その後に半角スペースを 2 つ追加する。

"s/。$/。 /"

古い日付のファイルを探して消したい

findコマンドを使えばいい。ファイルを探すときはfind

1 ヶ月以上前のファイルの一覧を得る

$ find /backup/daily -ctime +30

-ctime: 最終変更日(changed time)でファイルを探す。名前やパーミッションディレクトリの移動といった、あらゆる変更についての最終変更日時。

-atime: 最終アクセス日時。誰からも長いこと参照されていないファイルを探す時に使う。

-mtime: 最終変更日時。ファイルの内容が変更された日時。ファイルの内容の変更についてだけの最終変更日時。

大抵の場合は、-ctimeで事足りる。

#!/bin/bash

remove_files="$(find /backup/daily -ctime +30)"
for file in $remove_files
do
  rm "$file"
done

+-の考え方

今日から出発して、まず指定の日数分だけ時間を遡るイメージ。

+なら過去方向に進みながら古いファイルを探していく

-なら今日(未来の方向)に向かって戻りながら新しいファイルを探していく

最終変更日が 1 年以上前の古いファイル

$ find ./ -ctime +365

最終変更日が 1 週間以内である(この一週間以内に変更された)新しいファイル

$ find ./ -ctime -8

最後に実行してから 1 週間前から 2 週間前までのファイル

$ find /logs/ -ctime +7 and -ctime -15

最後に実行してから半年前の、名前に「access」を含んだファイル

$ find /logs/ -ctime +180 and -name "*access*"

最後に実行してから 30 日前の、名前に「access」または「error」を含んだファイル

$ find /logs/ -ctime +30 and \( -name "*access*" -or -name "*error*" \)

ディスクが満杯になる前にファイルを削除したい

#!/bin/bash

# 見出し行を除いた数値の部分だけ取り出す
free_size=$(df /data/backup | sed -r -e "s/[^ ]+ +[^ ]+ +[^ ]+ +([^ ]+).+/\1/" | tail -n 1)
required_size=$((10 * 1000 * 1000)) # 10GB

if [ $free_size -lt $required_size]
then
  files=$(find /data/backup -ctime +30)
  for file in $files
  do
    rm "$file"
  done
fi

前のコマンドが完了したら次のコマンドを実行する

prepare-data && process-data && report-result

プログラムをソースからビルドしてインストールするとき

$ ./configure --prefix=$HOME/local/ && make && make install

最新の情報に基づいてパッケージを更新するとき

$ sudo apt-get install update && sudo apt-get upgrade && sudo apt-get clean

前のコマンドが失敗したら次のコマンドを実行する

$ download-data --server=primary.datastore || download-data --server=secondary.datastore || exit 1

サブシェル

サブシェル・・・シェルの分身を作って、そっちでコマンドを実行する。

#!/bin/bash

cd /shared/
for dir in logs data users
do
  (cd $dir;
  files=$(find ./ -name "*.bak");
  for file in files;
  do
    rm "$file";
  done)
done

※ ;を末尾に付与してかっこの中を「コマンドを列挙したリスト」にする

※ 複数行に渡る構文を含んだ処理は、関数としてくくりだしておけばサブシェルの中でも簡単に使える。

#!/bin/bash

cd /shared/
remove_files() {
  files=$(find ./ -name "*.bak")
  for file in files
  do
    rm "$file"
  done
}

for dir in logs data users
do
  (cd $dir && remove_files)
done

1 つ前の作業ディレクトリに戻る

$ cd -

case

#!/bin/bash
do_at_rabbithouse() {
  #
}
# (ry

case "$(hostname)" in
  rabbithouse)
    do_at_rabbithouse
    ;;
  flulu_de_rapan)
    do_at_flulu_de_rapan
    ;;
  amausaan)
    do_at_amausaan
    ;;
  starbucks*) # 正規表現も使える e.g. starbucks_01, starbucks_02
    do_at_starbucks
    ;;
  doutor*|exe*) # 複数のパターンを列挙できる
    do_at_coffee_chain
    ;;
esac

一定時間繰り返したい

#!/bin/bash

count=0
while [ $count != 3600 ]
do
  curl "http://..."
  sleep 1 # 1秒待つ。 sleep 0.5 なら0.5秒、sleep 30d なら30日間待つ
  count=$(($count + 1))
done

コマンドのすべての出力をログファイルに保存したい

入力は標準入力だけだけど、出力は 2 つある。

標準出力・・・全般的な出力用(1 番)。1>と書く。>は 1>の省略形。

標準エラー出力・・・エラー情報の出力用(2 番)。2>と書く。

ファイルへのリダイレクトや別のコマンドへのパイプラインは、このうちの標準出力の方だけをつなぐ。

標準エラー出力から出てきたエラーメッセージは、リダイレクト先のファイルには保存されない。

解決策

既存のファイル書き込み処理の口にリダイレクトして、そこに相乗りする。

$ ./test-status.sh -u a001 1>/tmp/log.txt 2>&1

>%1: その後に指定した番号の出力と同じ出力先にリダイレクトする。

1 番(=標準出力)から出てくる情報が/tmp/log.txtというファイルに書き込まれるように口がつなぎ替わって、
2 番(=標準エラー出力)から出てくる情報が 1 番出口の接続先になっているファイル書き込み用の口に流れるように口がつなぎ替わる


シェルスクリプトまとめ1

リモート操作

X を飛ばす

$ ssh -Y -C chino@192.168.1.1

-Y: X の転送指定

-C: 通信内容の圧縮。付けると応答性がよくなる。

SSH 越しにコマンドを 1 つだけ実行する

$ ssh chino@server /scripts/do_backup.sh

検索

$ grep -r -i -E "(香風|かふう|カフウ) ?(智乃|ちの|チノ) | Kafu Chino" /var/share/

-i: 大文字小文字の違いを無視 -E: 正規表現を使う

仮想端末

インストール

$ sudo apt-get install tmux

有効化

$ tmux

再接続

$ tmux attach

切断

Ctrl + BD

※ 切断してから exit しよう

画面の生成

Ctrl + BC

画面の切り替え(次へ)

Ctrl + BN

画面の切り替え(前へ)

Ctrl + BP

画面の分割(縦)

Ctrl + B"

画面の分割(横)

Ctrl + B%

画面の移動

Ctrl + B, ,

画面サイズの変更

Ctrl + BCtrl + , , ,

画面のスクロールの有効

Ctrl + B[

Qで元の状態に戻る。

履歴

検索機能(後方検索)

Ctrl + R

逆方向(前方検索)にもコマンド履歴の検索をするには・・・

.bashrcstty stop endefを追記する。

検索機能(前方検索)

Ctrl + S

端末間でコマンド履歴を共有する

.bashrcに追記

function share_history {
    history -a
    history -c
    history -r
}
PROMPT_COMMAND='share_history'
shopt -u histappend
export HISTSIZE=9999

iandeth. - bash にて複数端末間でコマンド履歴(history)を共有する方法

scp

ローカルのfile.txtを 192.168.1.1 の/tmp配下にコピーする。

$ scp ./file.txt chino@192.168.1.1:/tmp/

ログインに使ったユーザのホーム

chino@192.168.1.1:~/

他のユーザのホーム

chino@192.168.1.1:~Cococa/

相対パス・・・ログイン後のホームディレクトリが起点になる

chino@192.168.1.1:../../tmp/

リモートのfile.txtを ローカルにコピーする。

$ scp chino@192.168.1.1:/tmp/file.txt ~/

ディレクトリごとコピーする

$ scp chino@192.168.1.1:/tmp/*.log /tmp/

$ scp -r chino@192.168.1.1:/tmp/results/ /tmp/

サーバからサーバにファイルをコピーする

$ scp chino@rabbithouse:/data/file chiya@amausaan:/backup/

システムの過負荷

top

システムの負荷に関する情報を、数秒間隔でリアルタイムに更新しにながら表示

load average: 負荷の指標。CPU に処理されるのを待ってる仕事の数。1 分あたり平均で何個分の仕事が積み上がっているのかを表す値。CPU のコア数以下ならよい。

%CPU: CPU 使用率。

TIME+: CPU 時間。実際に CPU を使ってる時間。

過負荷の原因を探るときは「CPU 使用率が高い」、「CPU 時間も長い」プロセスがあるかどうかを見る。

Cキーを押すとコマンドの詳細表示が切り替わる。

PID: プロセス ID

$ sudo kill プロセスID

Shift + P・・・CPU 使用率でソート

Shift + T・・・CPU 時間順でソート

Shift + M・・・メモリー使用量順でソート

ログファイルの閲覧

$ grep "/redmine" access.log | grep -v "/live" |  less

-v: 指定の文字列を含む行を除外する

圧縮されたログファイルを grep

zcat: 圧縮ファイルを読み込んで内容を展開しながら出力する(gzip 形式専用)

$ zcat access.log.gz | grep "/redmine" access.log | grep -v "/live" |  less

zcat: .gz, .tgz 形式

xzcat: .xz 形式

unzip -p: zip 形式

リアルタイム出力

$ tail -F access.log | grep "/redmine" access.log | grep -v "/live"

シェルスクリプト

  1. シェルスクリプトを作成する。

    $ vim setup.sh

  2. 実行権限を変更する。(新しく作ったシェルスクリプトはまだ実行権限が付与されていない)

    $ chmod +x setup.sh

  3. シェルスクリプトを実行する

ファイル名だけでコマンドとして実行できるのは、ファイルが/bin//usr/bin/などの特別な場所にあるときのみ

$ /home/chino/setup.sh
$ ./setup.sh
$ ~chino/setup.sh

変数

#!/bin/bash

base=/var/log/nginx
latest=${base}/access.log
prev=${base}/access.log.1.gz

cat $latest | analyze_latest.sh max
zcat $prev | analyze_prev.sh max
tar xvf log_result_max

tar xfv <path>: ファイルを展開

tar cfv <path>: ファイルを圧縮

環境変数

自分で定義しなくても$変数名と書くと値を参照できる特殊な変数。

$HOME: ホームディレクトリのパス $PWD: 現在のディレクトリのパス $EDITOR: 既存のテキストエディタのパス $PAGER: 既存のページャー(less, lv など)のパス $USER: 現在のユーザのユーザ名 $GROUP: 現在のユーザののグループ名 $HOSTNAME: そのマシンのホスト名

コマンド置換

$(コマンド列)または`コマンド列`

$ mv result.txt result-$(date +%Y-%m-%d).txt

必要な情報を取り出す

cut・・・パイプで渡された内容の各行から必要な部分を切り取って返す

$ cat /var/log/nginx/access.log | grep -v "/live" | cut -d " " -f 7 | less

"-d": 区切り文字。(一文字だけしか指定できない)

"-f": 取り出す位置

アクセスされた日付だけを取り出す。

$ cat /var/log/nginx/access.log | grep -v "/live" | cut -d "[" -f 2 | cut -d "]" -f 1 | less

同じ内容の行を数えたい

uniq・・・連続した重複を取り除く

sortしておけば、すべての重複をきちんと取り除ける。

uniq -c・・・それぞれの内容が何回登場したか、が出力される。

アクセスされたページのパスの一覧に対して、同じパスの登場回数を数える

$ cat /var/log/nginx/access.log | grep -v "/live" | cut -d " " -f 7 | sort -r | uniq -c | cat

-r: 降順ソート

上位と下位の項目を抽出する

上位

$ cat /var/log/nginx/access.log | grep -v "/live" | cut -d " " -f 7 | sort -r | uniq -c | head -n 20

-n: 出力する行数を変える

下位

$ cat /var/log/nginx/access.log | grep -v "/live" | cut -d " " -f 7 | sort -r | uniq -c | tail -n +6

-n +: 指定行数以降を出力する。

-n -: 指定行数以外を除いた全ての行を出力する。

CSV ファイルの並び替え

1,2,3,4,6 列目を取り出す。

$ cat test.csv | cut -d "," -f 1-3,4,6 | sort -t "," -k 3 -n | cat

-t: 区切り文字の指定

-k: 指定した列の内容で並び替え

-n: 数の部分を数値として解釈して数の大小で並べ替える

※ cut と sort でオプションの名前が違う。

結果を上書き出力

$ cat test.csv | cut -d "," -f 1-3,4,6 | sort -t "," -k 3 -n > sorted.csv

>>: 追記

オプション引数

#!/bin/bash
source=$1
target=$2
$ ./script.sh first second third

first・・・$1

second・・・$2

third・・・$3

名前付き引数

#!/bin/bash

while getopts b: OPT
do
    case $OPT in
        b) base="$OPTARG" ;;
        n) next="$OPTARG" ;;
        p) previous="$OPTARG" ;;
        o) output="$OPTARG" ;;
    esac
done
$ ./opt_script.sh -b /var/log/nginx/access.log2.gz -n /var/log/nginx/access.log.1.gz

処理分岐

$ script.sh foo bar
if [ $# = 2 ] # 引数の個数が2に等しい
then
    echo "Hello!"
else
    echo "Good afteroon"
fi
target=$1

if [ "$target" != "" ] # 引数が渡された
then
    echo "Hello!"
else
    echo "Good afteroon"
fi

繰り返し

#!/bin/bash

for filename in rabbithouse.log fleur_du_lapin.log amausaan.log
do
    ./create-report.sh $filename
done
#!/bin/bash

for filename in `cd /var/log/nginx; ls *.log | grep -v error.log`
do
    ./create-report.sh $filename
done

シェル関数

#!/bin/bash

# 主要な処理をmain関数として先に定義しておいて最後にそれを呼び出すのが定石
main() {
    report rabbithouse.log rabbithouse-${today}.csv /shared/rabbithouse/reports
    report fleur_du_lapin.log ${today}.csv /shared/fleur_du_lapin/reports
    report amausaan.log ${today}.txt /shared/amausaan/reports
}

today() {
    date +%Y-%m-%d
}

report() {
    source=$1
    report=$2
    outdir=$3
    ./analyze_mail_log.sh $source $report
    mkdir -p $outdir
    mv /tmp/${report} ${outdir}/
    echo "$source を処理しました"
}

main

exit 1: スクリプトの実行を中断して終了ステータスを 1 にする

return 1: 関数の実行を中断して終了ステータスを 1 にする

※ 関数のなかで$1と書くと、シェルスクリプトの引数ではなく、関数の引数を参照する。ので、関数内で引数を参照するときは関数外でいったん別の名前の変数として定義しておく。