2026年9月19日

2026年9月19日

awkプログラミング入門|フィールド操作と集計をログ解析で身につける

awk は、テキストを1行ずつ読み、空白などで区切った「フィールド」を取り出して加工・集計するための小さなプログラミング言語です。この記事では、パターン { アクション } という基本の形から、フィールド変数・区切り文字の指定・連想配列による集計までを、Webサーバーのアクセスログを題材に順に説明します。読み終えるころには、「ステータスコード別の件数」「IPアドレス別のアクセス数」「URLごとの転送量合計」を自分で書けるようになります。

awk の基本構造:パターンとアクション

awk のプログラムは「どの行に(パターン)」「何をするか(アクション)」の組み合わせでできています。

awk 'パターン { アクション }' ファイル名

パターンを省略するとすべての行が対象になり、アクションを省略すると該当行をそのまま表示します。たとえば次の2つは、どちらも「error を含む行を表示する」という意味です。

awk '/error/' /var/log/syslog
awk '/error/ { print }' /var/log/syslog

grep と似ていますが、awk はこの後「行の中の何列目を取り出すか」「条件に合った行の値を足し上げるか」まで1つのコマンドで書けるのが強みです。

プログラムはシェルに解釈されないよう、シングルクォートで囲みます。ダブルクォートで囲むと $1 がシェル変数として展開されてしまい、意図しない結果になります。最初につまずきやすい点なので覚えておいてください。

フィールドと組み込み変数

awk は各行を区切り文字(既定では連続する空白とタブ)で分割し、先頭から $1、$2、$3…という変数に入れます。$0 は行全体です。よく使う組み込み変数は次のとおりです。

  • NF:その行のフィールド数。$NF で最後のフィールド、$(NF-1) で最後から2番目を参照できます
  • NR:これまでに読んだ行数(複数ファイルを通した通し番号)
  • FNR:現在のファイル内での行番号
  • FS / OFS:入力の区切り文字 / 出力の区切り文字
# 1列目と3列目を表示
awk '{ print $1, $3 }' data.txt

# 行番号を付けて、最後の列を表示
awk '{ print NR ": " $NF }' data.txt

print $1, $3 のようにカンマで区切ると、間に OFS(既定は空白1つ)が入ります。カンマを付けずに print $1 $3 と書くと、文字列がそのまま連結されて区切りが消えます。

区切り文字を変える

CSV や /etc/passwd のように空白以外で区切られたファイルは、-F オプションで区切り文字を指定します。

# /etc/passwd からユーザー名とログインシェルを取り出す
awk -F: '{ print $1, $NF }' /etc/passwd

# 出力をタブ区切りにする
awk -F: 'BEGIN { OFS = "\t" } { print $1, $7 }' /etc/passwd

注意したいのは、カンマ区切りでも値の中にカンマを含み、引用符で囲まれた本物の CSV は -F, では正しく分割できない点です。GNU awk(gawk)には 5.3 以降で CSV 用の --csv オプションが加わっていますが、実行環境の awk が gawk かどうか、どのバージョンかはサーバーごとに違います。引用符付きの CSV を扱うときは、まず awk --version で確認し、対応していなければ Python などの CSV ライブラリを使う方が安全です。

条件で絞り込む

パターンには正規表現だけでなく、比較式や論理演算も書けます。

# 3列目が 100 より大きい行
awk '$3 > 100' data.txt

# 2列目が "POST" で、かつ 5列目が "/wp-login.php" を含む行
awk '$2 == "POST" && $5 ~ /wp-login\.php/' data.txt

# 特定のフィールドが正規表現に一致しない行
awk '$1 !~ /^#/' config.txt

~ は「正規表現に一致する」、!~ は「一致しない」を表します。数値のように見えるフィールドは数値として比較されるため、$3 > 100 は文字列比較ではなく数値比較になります。ただし "100KB" のように単位が付いた値は数値として扱われないので、集計前にデータの形を目で確かめておくと誤集計を防げます。

BEGIN と END:前処理と後処理

BEGIN ブロックは最初の行を読む前に、END ブロックは最後の行を読み終えた後に1回だけ実行されます。見出しの出力や合計値の表示に使います。

# 3列目の合計と平均
awk '{ sum += $3 } END { if (NR > 0) printf "合計 %d / 平均 %.2f\n", sum, sum / NR }' data.txt

awk の変数は宣言不要で、初めて使うときは数値なら 0、文字列なら空文字として扱われます。そのため sum += $3 をいきなり書いても動きます。一方、空ファイルを渡すと NR が 0 のまま END に入り、平均の計算で0除算エラーになります。上の例のように if (NR > 0) で守っておくと、cron で定期実行するスクリプトでも止まりにくくなります。

printf は C 言語と同じ書式指定ができ、%d(整数)、%.2f(小数2桁)、%-20s(左詰め20桁の文字列)などで表の形に整えられます。print と違い改行は自動で付かないため、\n を忘れないようにします。

連想配列で集計する

awk の集計の中心は連想配列です。添字に文字列をそのまま使えるので、「キーごとに件数や合計を数える」処理が1行で書けます。ここからは Nginx や Apache の標準的な combined 形式のアクセスログを例にします。この形式では、空白で区切ると $1 がクライアントIP、$7 がリクエストパス、$9 がステータスコード、$10 が転送バイト数になります(ログ形式をカスタマイズしている場合は列番号が変わるので、先に1行を awk '{ for (i = 1; i <= NF; i++) print i, $i }' で分解して確認してください)。

# ステータスコード別の件数
awk '{ count[$9]++ } END { for (code in count) print code, count[code] }' access.log

# IPアドレス別のアクセス数を多い順に10件
awk '{ ip[$1]++ } END { for (a in ip) print ip[a], a }' access.log | sort -rn | head -10

# パスごとの転送量合計("-" は 0 として扱う)
awk '$10 ~ /^[0-9]+$/ { bytes[$7] += $10 } END { for (p in bytes) printf "%12d %s\n", bytes[p], p }' access.log | sort -rn | head -20

for (key in array) の取り出し順は決まっていません。順位を付けたいときは、上のように出力を sort -rn(数値の降順)に渡すのが手軽で、どの awk 実装でも同じ結果になります。gawk には配列を並べ替える asort などの関数もありますが、mawk や BusyBox の awk では使えないため、複数のサーバーで使い回すスクリプトでは避けた方が無難です。

複数の条件を組み合わせた集計

パターンと連想配列を組み合わせると、調査に役立つ集計がすぐに作れます。

# 5xx エラーが出たパスを件数順に
awk '$9 ~ /^5/ { err[$7]++ } END { for (p in err) print err[p], p }' access.log | sort -rn | head

# wp-login.php への POST を送ってきた IP を件数順に
awk '$6 == "\"POST" && $7 ~ /^\/wp-login\.php/ { ip[$1]++ } END { for (a in ip) print ip[a], a }' access.log | sort -rn | head

combined 形式ではリクエスト行が "POST /wp-login.php HTTP/1.1" のように引用符で囲まれているため、$6 は "POST と引用符付きになります。条件に合わないときは、このように引用符がフィールドに含まれていないかを疑ってください。

文字列関数で整形する

よく使う組み込みの文字列関数は次のとおりです。いずれも POSIX で定められていて、主要な awk 実装で使えます。

  • length(s):文字列の長さ
  • substr(s, 開始位置, 長さ):部分文字列(位置は1始まり)
  • split(s, arr, 区切り):文字列を分割して配列に入れ、要素数を返す
  • sub(正規表現, 置換文字列, 対象) / gsub(...):最初の1か所 / すべてを置換
  • tolower(s) / toupper(s):小文字 / 大文字に変換
# クエリ文字列を除いたパス単位で集計する
awk '{ split($7, part, "?"); path[part[1]]++ } END { for (p in path) print path[p], p }' access.log | sort -rn | head

# 時刻 [28/Oct/2026:10:15:32 から「時」だけ取り出して時間帯別に数える
awk '{ split($4, t, ":"); hour[t[2]]++ } END { for (h in hour) print h, hour[h] }' access.log | sort

日本語を含むファイルでは、length や substr が文字数で数えるかバイト数で数えるかが、awk の実装とロケール設定によって変わります。集計結果の文字数がずれるときは、locale コマンドで UTF-8 のロケールになっているかを確認してください。逆に、ASCII だけのログを大量に処理するときは LC_ALL=C awk ... とすると速くなることがあります。

スクリプトファイルにまとめる

1行が長くなってきたら、ファイルに分けると読みやすく、レビューもしやすくなります。

#!/usr/bin/awk -f
# status_report.awk : ステータスコード別の件数と割合を表示する
{
    count[$9]++
    total++
}
END {
    if (total == 0) {
        print "対象行がありません"
        exit
    }
    for (code in count) {
        printf "%s %8d %6.2f%%\n", code, count[code], count[code] * 100 / total
    }
}
awk -f status_report.awk /var/log/nginx/access.log | sort

ローテーション済みの圧縮ログは zcat access.log.2.gz | awk -f status_report.awk のようにパイプで渡せます。本番サーバーで巨大なログを処理するときは、CPU と I/O を使うことを意識し、まずは head -n 10000 などで一部を切り出して動作を確かめてから全量に流すと安心です。

実装の違いを把握しておく

Linux で awk と打ったときに動くのは、ディストリビューションによって gawk、mawk、BusyBox awk などさまざまです。Debian 系では mawk が既定で入っている環境が多く見られます(2026年10月現在)。この記事で紹介した $n、NF、NR、-F、BEGIN/END、連想配列、split・gsub などの基本機能は POSIX で定められた範囲なので、どの実装でもおおむね同じように動きます。一方、--csv、asort、strftime などは gawk の拡張です。スクリプトを複数のサーバーで共有する場合は、awk --version(mawk では awk -W version)で実装を確認し、拡張機能に頼るときは gawk と明示して呼び出すと、環境差によるトラブルを避けられます。

まとめ

  • awk は パターン { アクション } の組み合わせで、行の絞り込みと加工を1つのコマンドで書けます
  • $1〜$NF でフィールドを、NR・NF で行数・列数を参照し、-F と OFS で区切り文字を調整します
  • BEGIN/END で前処理と合計表示を行い、空ファイルでの0除算には条件で備えます
  • 連想配列 count[キー]++ が集計の基本で、並べ替えは sort -rn に任せると実装差を気にせずに済みます
  • 引用符付きの CSV や gawk 独自の関数は環境によって動かないため、awk --version で実装を確認してから使います

お気軽にご相談ください

お見積りへ お問い合わせへ
▲