awk配列とグループ化による集計
フィールド値をキーとする連想配列を使い、合計、件数、グループ別の集計結果を計算します。
「awk配列とグループ化による集計」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
awkの連想配列とは
awkでは、連想配列はキーと値を格納するデータ構造で、キーには任意の文字列または数値を使えます。多くの言語にあるインデックス配列とは異なり、awkの配列は内部的にはハッシュマップです。そのため、フィールドの値ごとのデータのグループ化や集計に適しています。
- 暗黙的に宣言できます。
arr[key] = valueのように代入するだけです - キーはデフォルトで文字列として扱われます(数値は変換されます)
- サイズ制限はありません。必要に応じてawkが配列を拡張します
- 1回の走査で合計、件数、グループ別集計を計算するのに適しています
インクリメントする前にキーを初期化する必要はありません。awkは未設定のキーを自動的に0または空文字列として扱います。
グループごとの行数を数える
最も一般的な集計パターンは、フィールド内の各一意値が何回現れるかを数えることです。フィールド $1(または任意のフィールド)を配列のキーとして使い、一致する行ごとにカウンターをインクリメントします。
ENDブロックは、すべての入力を読み終えた後に実行されます。蓄積した結果を出力するのはここです。
count[$1]++処理後、countには$1の各一意値に対応する行数の合計が格納されます。
#!/usr/bin/env bash
# Count how many log entries exist per HTTP status code
# Input format: <ip> <date> <method> <path> <status> <bytes>
printf '10.0.0.1 2024-01-01 GET /index 200 512
10.0.0.2 2024-01-01 POST /api 404 128
10.0.0.3 2024-01-01 GET /img 200 2048
10.0.0.4 2024-01-01 GET /api 500 64
10.0.0.5 2024-01-01 DELETE /api 404 32
' | awk '
{
count[$5]++
}
END {
for (status in count)
print status, count[status]
}'
グループごとに数値フィールドを合計する
カウントは集計方法の一つにすぎません。あるフィールドごとにグループ化して数値フィールドを合計するには、グループ化に使うフィールドをキーとする配列に数値を累積します。
パターンは次のとおりです。
- キー = グループ化に使うフィールド(例:ユーザー名なら
$1) - 値 = 数値フィールドの累計(例:バイト数なら
$2)
これにより、awkを1回実行するだけでグループ別の合計を計算できます。ソートや前処理は必要ありません。
#!/usr/bin/env bash
# Sum bytes transferred per user from an access log
printf 'alice 1024
bob 512
alice 2048
charlie 256
bob 768
alice 128
' | awk '
{
bytes[$1] += $2
}
END {
for (user in bytes)
printf "%-10s %d bytes\n", user, bytes[user]
}'
1回の走査で件数と合計を組み合わせる
awkでは複数の配列を同時に管理できるため、ファイルを1回走査するだけで、グループごとの件数と合計(したがって平均も)を求められます。パイプラインを2回実行するよりも、はるかに効率的です。
count[key]++— 出現回数を記録しますtotal[key] += $N— 累計を記録しますENDでtotal[k] / count[k]を計算すると、グループごとの平均になります
#!/usr/bin/env bash
# Compute per-department headcount and average salary
printf 'Engineering Alice 95000
Engineering Bob 88000
Marketing Carol 72000
Marketing Dave 68000
Engineering Eve 102000
Marketing Frank 75000
' | awk '
{
dept = $1
sal = $3
count[dept]++
total[dept] += sal
}
END {
printf "%-15s %5s %10s\n", "Department", "Count", "Avg Salary"
for (d in count)
printf "%-15s %5d %10.0f\n", d, count[d], total[d]/count[d]
}'
2次元グループ化のための複数フィールドキー
複数のフィールドを区切り文字で連結すると、複合キーを作成できます。これにより、特別な構文を使わずに2次元のグループ化を実現できます。
データ中に現れない区切り文字を選んでください(例:SUBSEP、awk組み込みのレコード区切り文字 \034、またはリテラルのパイプ文字 |)。
- 複合キー:
arr[$1 SUBSEP $2]またはarr[$1"|"$2] - 出力時にキーを分割して戻す:
split(key, parts, "|")
#!/usr/bin/env bash
# Count requests grouped by HTTP method AND status code
printf 'GET 200
POST 201
GET 404
GET 200
DELETE 204
POST 201
GET 404
POST 500
' | awk '
{
key = $1 "|" $2
count[key]++
}
END {
printf "%-8s %-6s %s\n", "Method", "Status", "Count"
for (k in count) {
split(k, parts, "|")
printf "%-8s %-6s %d\n", parts[1], parts[2], count[k]
}
}'
グループごとの最小値と最大値を追跡する
連想配列を使うと、グループごとの最小値と最大値を簡単に追跡できます。ポイントは、!(key in arr)という特殊な条件を使い、各キーが最初に現れたときだけ初期化することです。
!(key in min_arr)は、キーが初めて現れたときに真になります- 初期化後は、標準的な小なり/大なり比較で値を比較して上書きします
このパターンにより、最小値を壊してしまう不要な0を避けられます。
#!/usr/bin/env bash
# Find min and max response time per endpoint
printf '/api/users 120
/api/orders 340
/api/users 98
/api/orders 512
/api/users 210
/api/orders 280
/health 5
/health 7
' | awk '
{
ep = $1
rt = $2
if (!(ep in mn)) { mn[ep] = rt; mx[ep] = rt }
if (rt < mn[ep]) mn[ep] = rt
if (rt > mx[ep]) mx[ep] = rt
}
END {
printf "%-15s %6s %6s\n", "Endpoint", "Min", "Max"
for (ep in mn)
printf "%-15s %6d %6d\n", ep, mn[ep], mx[ep]
}'
度数分布 — 上位Nグループ
実務でよくある作業の一つが、出現頻度が最も高い上位N個の値を見つけることです。awkでカウントし、sortとheadにパイプして順位付けと絞り込みを行います。
このパイプラインパターンは、シェルでの処理における慣用的な方法です。
- awkで配列に出現回数を集計し、
ENDでcount keyを出力します sort -rnで数値を降順にソートしますhead -n 5で上位5件だけを残します
awkを集計に集中させ、ソートをsortに任せる方法は、Unixの哲学に沿っています。
#!/usr/bin/env bash
# Top 3 most active IP addresses from an access log
printf '192.168.1.10 GET /index
10.0.0.5 POST /api
192.168.1.10 GET /css
172.16.0.3 GET /index
10.0.0.5 GET /api
192.168.1.10 DELETE /api
172.16.0.3 POST /login
10.0.0.5 GET /index
10.0.0.5 POST /logout
' | awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' \
| sort -rn \
| head -n 3
複数ファイルの集計にNRとFNRを使う
複数のファイルを処理するとき、awk組み込みのNR(読み込んだレコードの総数)とFNR(現在のファイル内のレコード番号)を使うと、FILENAMEによって各レコードがどのファイルに由来するかを識別できます。
FILENAME— 現在読み込んでいるファイルの名前FNR == 1— 新しいファイルの先頭で成立します(ヘッダーのスキップに便利です)
これにより、1回のawk実行でディレクトリ全体のログを対象に、ファイルごとのグループ別集計を行えます。
awk配列からソート済みの出力を表示する
awkのfor (key in array)ループは、順序を保証しません。決定的な出力が必要な場合は、awkのENDでの出力をsortにパイプするか、値を集めてasorti/asort関数(GNU awkのみ)でawk内でソートします。
クロスプラットフォームのスクリプトでは、移植性のあるシェルパイプラインを使う方法が一般的に推奨されます。
sort -k1,1— 1番目のフィールド(グループキー)をアルファベット順にソートしますsort -k2,2rn— 2番目のフィールド(件数または合計)を数値の降順にソートします
#!/usr/bin/env bash
# Bytes per user, sorted alphabetically by username
printf 'zara 800
alice 1500
bob 300
alice 200
zara 400
bob 1100
' | awk '
{
total[$1] += $2
}
END {
for (u in total)
print u, total[u]
}' | sort -k1,1
配列キーを削除して状態をクリアする
ストリームの途中で集計状態をリセットしたい場合があります。たとえば、ログファイル内の各セクションが空行や特定の区切り値で分けられている場合です。
delete arr[key]— 1つのエントリを削除しますdelete arr— 配列全体をクリアします(GNU awk)- 存在しないキーを作成してしまわないよう、アクセスする前に
(key in arr)で存在を確認します
この手法により、awkを再起動せずにスライディングウィンドウやセクションごとの集計を行えます。
#!/usr/bin/env bash
# Sum values within each section delimited by "---"
printf 'alice 100
bob 200
---
alice 50
bob 75
charlie 300
---
' | awk '
/^---/ {
print "-- Section totals --"
for (u in total) printf " %-10s %d\n", u, total[u]
delete total
next
}
{
total[$1] += $2
}'
実践的なパイプライン:Nginxログの要約
ここまでの内容を組み合わせて、Nginxのcombinedログ形式をawkで1回走査して集計する、本番環境向けの例を見てみましょう。1回の走査で、仮想ホストごとのリクエスト数、合計バイト数、エラー率を計算します。
使用する主なテクニック:
- 複合キー:フィールドから抽出した
vhost - 並列配列:
reqs[]、bytes[]、errors[] - 条件付き累積:エラーレスポンスだけを数える
$9 >= 400 ENDで整形したprintfの表を出力します
#!/usr/bin/env bash
# Simulated Nginx combined log: host ip - - [date] "METHOD /path HTTP/1.1" status bytes
printf 'api.example.com 10.0.0.1 - - [01/Jan/2024] "GET /v1" 200 1024
www.example.com 10.0.0.2 - - [01/Jan/2024] "GET /" 200 4096
api.example.com 10.0.0.3 - - [01/Jan/2024] "POST /v1" 500 128
www.example.com 10.0.0.4 - - [01/Jan/2024] "GET /img" 404 64
api.example.com 10.0.0.5 - - [01/Jan/2024] "GET /v1" 200 2048
www.example.com 10.0.0.6 - - [01/Jan/2024] "GET /" 200 4096
api.example.com 10.0.0.7 - - [01/Jan/2024] "DELETE /v1" 403 32
' | awk '
{
host = $1; status = $9; b = $10
reqs[host]++
bytes[host] += b
if (status + 0 >= 400) errors[host]++
}
END {
printf "%-20s %6s %10s %6s\n", "Host", "Reqs", "Bytes", "Errors"
for (h in reqs)
printf "%-20s %6d %10d %6d\n", h, reqs[h], bytes[h], errors[h]+0
}'
理解度チェック:最小値を正しく初期化する
awkで集計するときによくある落とし穴について、理解度を確認しましょう。
レッスンのまとめ:awk配列による集計
awkだけでグループ別集計を計算するための基本パターンを学びました。
- カウント:
count[$key]++— 行ごとにインクリメントし、ENDで出力します - 合計:
total[$key] += $N— グループごとに数値フィールドを累積します - 平均:
count[]とtotal[]の両方を管理し、ENDで割り算します - 最小値/最大値:
!(key in arr)を使って初回に初期値を設定し、その後で比較します - 複合キー: フィールドを区切り文字で連結し、多次元のグループ化に使います
- ソート済みの出力: awkの
ENDでの出力をsortにパイプし、決定的な順序にします - セクションのリセット:
delete arrを使い、入力の論理的なセクション間で状態をクリアします
これらのパターンを使えば、負荷の高いデータベースクエリや複数回のパイプライン実行を、効率的なawkの1回の実行に置き換えられます。これは本番環境でシェルを扱うエンジニアにとって重要なスキルです。
よくある質問
「awk配列とグループ化による集計」レッスンは無料ですか?
はい。「awk配列とグループ化による集計」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
「awk配列とグループ化による集計」で何を学びますか?
フィールド値をキーとする連想配列を使い、合計、件数、グループ別の集計結果を計算します。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「awk配列とグループ化による集計」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?
はい。すべてのLinux Command Line & Bash Scripting Masteryレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。