Bash 入門

Linux awk コマンド

awk は、パターンスキャン(Pattern Scanning)およびデータ処理(Data Processing)において極めて優れた性能を発揮する強力なテキスト処理ツール(Text Processing Tool)です。入力ファイル(Input File)を1行ずつ読み込み、セパレータ(Separator)に基づいて各行を複数のフィールド(Fields)に分割し、それらのフィールドに対して操作を実行します。これにより、特定のデータの抽出、出力の再フォーマット(Reformatting)、および構造化されたテキストファイルからのレポート生成において非常に効果的です。

1. awk の基礎:構造と基本構文

awk コマンドの基本構造は awk 'パターン{ アクション}' 入力ファイル です。

awk は 入力ファイル(ファイルが指定されていない場合は標準入力 / Standard Input)の各行を処理します。各行に対して、指定されたパターン(Pattern)にマッチするかどうかをチェックします。その行がマッチした場合、awk はそのパターンに関連付けられたアクション(Action)を実行します。

  • パターンが提供されていない場合、アクションはすべての行に対して実行されます。
  • アクションが提供されていない場合、awk は行全体をプリント(Print)します。

1.1 フィールドとフィールドセパレータ

awk は自動的に各入力行を複数のフィールドに分割します。デフォルト(Default)では、空白文字(スペースまたはタブ)がフィールドセパレータ(Field Separator)として機能します。

  • 最初のフィールドは $1 として参照され、2番目は $2 となり、以降も同様に続きます。
  • 行全体のデータは $0 で表されます。
  • 現在のレコード(行)内のフィールド数は、組み込み変数(Built-in Variable)の NF (Number of Fields) に格納されます。
  • これまでに処理されたレコードの総数(通常は行番号)は、NR (Number of Records) に格納されます。

シンプルな data.txt ファイルを考慮してみましょう:

Name Age City
Alice 30 NewYork
Bob 24 London
Charlie 35 Paris

data.txt 内の名前(1番目のフィールド)のみをプリントします:

awk '{ print $1 }' data.txt

このコマンドは各行を処理し、1番目のフィールド ($1) の内容をプリントします。

名前と都市(1番目と3番目のフィールド)をプリントします:

awk '{ print $1, $3 }' data.txt

print ステートメント内の $1$3 の間にあるカンマは、デフォルトで awk が出力時にフィールド間にスペースを追加するように指示します。

行全体およびフィールド数をプリントします:

awk '{ print "Line:", $0, "Fields:", NF }' data.txt

これは、$0(行全体)および NF(フィールド数)にどのようにアクセスするかを示しています。

カスタムフィールドセパレータ(-F)

場合によっては、データがCSVファイルにおけるカンマや /etc/passwd におけるコロンなど、空白文字以外のキャラクターで区切られていることがあります。-F オプション(または内部の FS 変数)を使用することで、代替のフィールドセパレータを指定できます。

例となる grades.csv

Student,Math,Science,English
Alice,90,85,92
Bob,78,88,80
Charlie,95,90,88

grades.csv から学生の名前と数学の成績をプリントします:

awk -F',' '{ print $1, $2 }' grades.csv

-F',' はフィールドセパレータをカンマに設定します。

1.2 パターン:行のフィルタリング

awk におけるパターンは、正規表現(Regular Expression)、関係式(Relational Expression)、またはそれらの組み合わせです。パターンが提供された場合、そのパターンにマッチする行に対してのみアクションが実行されます。

正規表現パターン:
これらは grep と似た動作をしますが、awk のコンテキスト(Context)内で機能します。スラッシュで囲まれたパターン /pattern/ は、その正規表現を含む行にマッチします。

data.txt から都市が "NewYork" である行をプリントします:

awk '/NewYork/ { print $0 }' data.txt

行内の任意の場所に "NewYork" が見つかった場合、その行全体をプリントします。

名前が 'A' で始まる人の名前をプリントします:

awk '/^A/ { print $1 }' data.txt

ここでは正規表現 ^A を使用して、最初の文字が 'A' である行にマッチさせています。

関係式パターン:
これらのパターンは関係演算子(Relational Operators: ==, !=, <, <=, >, >=)を使用して、フィールド値または変数を比較します。

grades.csv から数学の成績が80より大きい学生の名前と成績をプリントします:

awk -F',' '$2 > 80 { print $1, $2 }' grades.csv

ここで、$2 > 80 がパターンになります。これは2番目のフィールド(数学の成績)が数値的に80より大きいかどうかをチェック(Check)します。

数学の成績が正確に90である学生の名前をプリントします:

awk -F',' '$2 == 90 { print $1, $2 }' grades.csv

== 演算子は厳密な比較(Exact Comparison)を実行します。

1.3 アクション:何を実行するか

アクションは中括弧 {} で囲まれ、1つ以上の awk ステートメントで構成されます。一般的なアクションには以下のものが含まれます:

  • print ステートメント:前述の通り、フィールドまたは行全体をプリントします。
  • 変数:awk はユーザー定義変数(User-defined Variables)をサポートしています。
  • 算術演算:数学的な計算を実行します。
  • 条件付きステートメント(if-else):アクション内のコードフローを制御します。
  • ループ(for, while):アクション内でイテレーション(Iteration / 反復)を行います。

grades.csv における数学の平均点を計算します:

awk -F',' '
NR==1 { next } # ヘッダー行をスキップ
{
  total_math_score += $2 # スコアを累算
  count++ # 学生数をカウント
}
END {
  print "総人数:", count
  print "数学の平均点:", total_math_score / count
}' grades.csv

この例では、いくつかの新しい概念が導入されています:

  • NR==1 { next } : 最初の行(NR はレコード番号)にマッチするパターンです。next は、後続のコードを実行せずに次の入力行に直接スキップ(Skip)するよう awk に指示します。
  • total_math_score += $2 : 現在の学生の数学のスコアを合計変数(Sum Variable)に加算します。awk の変数は動的型付け(Dynamically Typed)であり、初期値はデフォルトで 0 または空のストリングになります。
  • count++ : 学生のカウンター(Counter)をインクリメントさせます。
  • END { ... } : 特殊なパターンであり、すべての入力行が処理された後にそのアクションが実行されます。これはデータの集計(Summarization)に非常に適しています。

2. awk の高度な機能

awk は、複雑なデータ操作を実行するためのより高度な機能(Advanced Features)を提供しています。

2.1 BEGIN と END ブロック

BEGIN および END は特殊なパターンであり、任意の入力行を読み込む前 (BEGIN)、またはすべての入力行の処理が完了した後 (END) に特定のアクションを実行できるようにします。

  • BEGIN { アクション} : awk が最初の入力行を処理し始める前に一度だけ実行されます。通常、変数の初期化、ヘッダーのプリント、またはフィールドセパレータ (FS) の設定に使用されます。
  • END { アクション} : awk がすべての入力行を処理した後に一度だけ実行されます。通常、集計された統計情報、合計、またはフッターのプリントに使用されます。

BEGINEND を使用して grades.csv のフォーマット済みレポートをプリントする例:

awk -F',' '
BEGIN {
  print "--- 学生成績レポート ---"
  print "名前\t数学\t科学\t英語"
  sum_math = 0
  sum_science = 0
  sum_english = 0
  count_students = 0
}
NR==1 { next } # ヘッダーをスキップ
{
  print $1,"\t",$2,"\t",$3,"\t",$4
  sum_math += $2
  sum_science += $3
  sum_english += $4
  count_students++
}
END {
  print "-----------------------------"
  print "成績集計:"
  print "数学の平均点:", sum_math / count_students
  print "科学の平均点:", sum_science / count_students
  print "英語の平均点:", sum_english / count_students
  print "-----------------------------"
}' grades.csv

BEGIN ブロック(Block)では、レポートのヘッダーを設定し、合計変数を初期化しています。メインブロックでデータを処理し、合計を累算します。最後に END ブロックで平均値を計算し、プリントします。

2.2 awk のコア組み込み変数

NF(フィールド数)、NR(レコード/行番号)、および $0(行全体)に加えて、awk には非常に有用な組み込み変数がいくつかあります:

  • FS (Field Separator): 入力フィールドセパレータ。BEGIN ブロック内で設定するか、-F コマンドラインオプションを通じて設定できます。
  • OFS (Output Field Separator): プリント時に使用される出力フィールドセパレータ。デフォルトはスペース1つです。
  • RS (Record Separator): 入力レコードセパレータ(デフォルトは改行文字 / Newline)。
  • ORS (Output Record Separator): 出力レコードセパレータ(デフォルトは改行文字)。
  • FILENAME : 現在処理中の入力ファイル名。

出力フィールドセパレータをカンマに変更し、data.txt をプリントします:

awk 'BEGIN { OFS="," } { print $1, $2, $3 }' data.txt

これにより、スペースの代わりにカンマで区切られたフィールドがプリントされます。

各レコードのファイル名をプリントします:

awk '{ print FILENAME, NR, $0 }' data.txt

2.3 条件ロジックとループ

awk はアクションブロック内で if-else ステートメントおよび for/while ループをサポートしており、より複雑なロジックを実装できます。

if-else ステートメント:

awk -F',' '
NR==1 { next }
{
  if ($2 > 90) {
    status = "優秀"
  } else if ($2 > 80) {
    status = "良好"
  } else {
    status = "普通"
  }
  print $1, $2, status
}' grades.csv

このスクリプトは、数学のスコアに基づいて「ステータス(Status)」評価カラムを追加します。

for ループ:

data.txt の各行にあるすべてのフィールドをプリントし、それらのフィールド番号をプレフィックス(Prefix / 前置)として追加します:

awk '{
  for (i=1; i<=NF; i++) {
    print "フィールド", i, ":", $i
  }
  print "---"
}' data.txt

ループは1から NF(フィールドの総数)までイテレーションし、各フィールドを1つずつプリントします。

2.4 ユーザー定義関数

コードをより組織的かつ再利用可能(Reusable)にするために、awk は関数の定義を許可しています。

awk '
function calculate_average(score1, score2, score3) {
  return (score1 + score2 + score3) / 3
}
NR==1 { next }
{
  avg = calculate_average($2, $3, $4)
  print $1, avg
}' grades.csv

この例では、3つのスコアの平均値を計算する calculate_average 関数を定義し、それを使用して各学生の名前と全体的な平均成績をプリントしています。

3. 実用的なユースケースとデモ

3.1 ログデータの抽出と集計(ケーススタディ)

access.log という名前のWebサーバーログファイルを考慮してみましょう(問題特定のためにログファイルを解析する実践的なケーススタディに似ています)。

192.168.1.1 - [10/Nov/2023:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 1234
192.168.1.2 - [10/Nov/2023:10:00:05 +0000] "GET /about.html HTTP/1.1" 200 567
192.168.1.1 - [10/Nov/2023:10:00:10 +0000] "POST /login HTTP/1.1" 401 200
192.168.1.3 - [10/Nov/2023:10:00:15 +0000] "GET /images/logo.png HTTP/1.1" 200 890
192.168.1.2 - [10/Nov/2023:10:00:20 +0000] "GET /nonexistent HTTP/1.1" 404 150
192.168.1.1 - [10/Nov/2023:10:00:25 +0000] "GET /index.html HTTP/1.1" 200 1234

各行には、IPアドレス、タイムスタンプ、リクエストメソッド/パス/プロトコル、ステータスコード、および送信されたバイト数が含まれます。フィールドは主にスペースで区切られています。

タスク1:ステータスコードが404であるすべてのリクエストと、それに対応するIPアドレスを抽出します。

(スペースをセパレータとして)慎重に数えると、ステータスコードは9番目のフィールドになります。IPアドレスは1番目のフィールドです。

awk '$9 == 404 { print "IP:", $1, "リクエストパス:", $7, "ステータス:", $9 }' access.log
  • $9 == 404 は、9番目のフィールドが 404 である行にマッチします。
  • print ... はIP、リクエストパス(7番目のフィールド、引用符の後)、およびステータスコードをプリントします。

タスク2:総リクエスト数、およびステータスコードごとのリクエスト数をカウント(統計)します。

awk '
{
  total_requests++
  status_counts[$9]++ # 連想配列を使用
}
END {
  print "総リクエスト数:", total_requests
  print "--- ステータスコード統計 ---"
  for (status_code in status_counts) {
    print "ステータス", status_code, ":", status_counts[status_code]
  }
}' access.log
  • total_requests++ は、各行に対してカウンターをインクリメントします。
  • status_counts[$9]++ は、status_counts という名前の連想配列(Associative Array:別名ハッシュテーブル / 辞書)を使用しています。ステータスコード ($9) がキー (Key) として機能し、その対応する値が出現するたびにインクリメントされます。
  • END ブロックは status_counts 配列をトラバース(Traverse / ループ処理)し、最終的なサマリー(Summary)をプリントします。

3.2 データ変換:データのリフォーマット

前述の data.txt の並べ替え(Reordering)を行うと仮定します:

Name Age City
Alice 30 NewYork
Bob 24 London
Charlie 35 Paris

タスク:data.txt を "City: <City>, Name: <Name>, Age: <Age>" の形式にリフォーマット(再フォーマット)します。

awk '
NR==1 { print "City: City, Name: Name, Age: Age"; next } # ヘッダーを処理
{
  print "City:", $3 ", Name:", $1 ", Age:", $2
}' data.txt

この例では、カスタムのヘッダーをプリントし、その後続の各行のフィールドの順序を再配置(Reorder)し、プレフィックスを追加しています。