コンテンツにスキップ

AWKハンドブック

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

AWKは強力なテキスト処理言語です。ファイルやデータストリームの読み込み、フィールドの抽出、計算、出力など、さまざまな処理を記述できます。AWKスクリプトは簡潔で効率的な方法でデータ操作を行うことができます。

AWKの特徴

[編集]
  • 行指向のテキスト処理
  • パターンマッチングと正規表現
  • フィールド分割による列データの処理
  • 組み込み変数による柔軟な制御
  • 豊富な文字列操作関数

AWKの基本構造

[編集]

基本的な構文

[編集]

AWKスクリプトは、パターンとアクションのペアから構成されています:

パターン { action }
BEGIN { action }
END { action }

重要な組み込み変数

[編集]
  • FS: フィールドセパレータ(デフォルトは空白)
  • RS: レコードセパレータ(デフォルトは改行)
  • OFS: 出力フィールドセパレータ
  • ORS: 出力レコードセパレータ
  • NF: 現在の行のフィールド数
  • NR: 現在の行番号
  • FILENAME: 現在処理中のファイル名

AWKの組み込み関数

[編集]

文字列関数

[編集]
  • length(str): 文字列の長さを返す
  • substr(str, start, length): 部分文字列を取り出す
  • index(str, substr): 部分文字列の位置を検索
  • match(str, regexp): 正規表現でマッチング
  • split(str, array, separator): 文字列を配列に分割
  • sub(regexp, replacement): 最初のマッチを置換
  • gsub(regexp, replacement): すべてのマッチを置換

数値関数

[編集]
  • int(x): 整数部分を取得
  • rand(): 乱数生成
  • sin(x), cos(x), exp(x), log(x): 数学関数

実践的なAWKスクリプト例

[編集]

/etc/passwdからアカウント一覧を作成

[編集]
# ユーザー名とシェルの一覧を表示
BEGIN {
    FS = ":"
    print "Username\tShell"
    print "--------\t-----"
}
{   # パターンを省略した場合全ての行にマッチします
    # システムアカウントを除外(UIDが1000以上)
    if ($3 >= 1000) {
        printf "%-15s\t%s\n", $1, $7
    }
}

CSVファイルの処理(クォート文字列対応)

[編集]
# CSVファイルを処理し、クォートされた文字列も適切に扱う
BEGIN {
    FPAT = "([^,]+)|(\"[^\"]+\")"
    OFS = ","
}
{
    # クォート文字列から引用符を除去
    for (i=1; i<=NF; i++) {
        if ($i ~ /^".*"$/) {
            $i = substr($i, 2, length($i)-2)
        }
    }
    print
}

ログファイル解析

[編集]
# Apacheアクセスログから404エラーを抽出し集計
/HTTP\/[0-9.]+ 404/ {
    urls[$7]++
}
END {
    print "404 Not Found URLs and counts:"
    for (url in urls) {
        printf "%-50s %d\n", url, urls[url]
    }
}

正規表現を使った高度な処理

[編集]
# メールアドレスを抽出して検証
{
    email_pattern = "([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})"
    line = $0
    while (match(line, email_pattern)) {
        email = substr(line, RSTART, RLENGTH)
        # ドメイン部分を取り出す
        split(email, parts, "@")
        printf "Found email: %s (Domain: %s)\n", email, parts[2]
        line = substr(line, RSTART + RLENGTH)
    }
}

高度なテクニック

[編集]

マルチデリミタの処理

[編集]
# 空白とカンマの両方をデリミタとして使用
BEGIN {
    FS = "[ ,]+"  # 空白またはカンマの連続をデリミタとする
}
{
    for (i=1; i<=NF; i++) {
        print "Field", i, ":", $i
    }
}

レコードセパレータの活用

[編集]
# 段落単位での処理(空行で区切られたテキスト)
BEGIN {
    RS = "\n\n+"  # 1つ以上の空行をレコードセパレータとする
    FS = "\n"     # 各行をフィールドとして扱う
}
{
    print "Paragraph #" NR ":"
    for (i=1; i<=NF; i++) {
        print "  Line" i ":" $i
    }
}

配列とソート

[編集]
# データの集計とソート出力
{
    count[$1]++
}
END {
    # 配列のインデックスを取得してソート
    n = asorti(count, sorted)
    print "Sorted unique items and their counts:"
    for (i=1; i<=n; i++) {
        key = sorted[i]
        printf "%-20s %d\n", key, count[key]
    }
}

AWKのベストプラクティス

[編集]

スクリプトの構造化

[編集]
  • 明確なBEGINブロックでの初期化
  • 適切な変数名の使用
  • コメントによるドキュメント化
  • 複雑な処理の関数化

効率的な処理

[編集]
  • 不要な正規表現の最小化
  • 大きなファイル処理時のメモリ使用考慮
  • 適切なパターンマッチングの選択

デバッグテクニック

[編集]
# デバッグ情報の出力
function debug(msg) {
    printf "DEBUG: %s (Line %d)\n", msg, NR > "/dev/stderr"
}

AWK実践イディオム集

[編集]

ファイル処理の基本イディオム

[編集]

最終行を除く全行に区切り文字を付加

[編集]
# カンマ区切りの出力で、最後の要素の後ろにカンマを付けない
{
    printf "%s%s", $0, (NR == FNR ? "" : ",")
}

ヘッダー行のスキップ

[編集]
# 最初の1行をスキップする3つの方法
NR > 1 { process() }  # 方法1
FNR > 1 { process() } # 方法2:複数ファイル対応
!/^Header/ { process() } # 方法3:パターンマッチ

末尾n行の処理

[編集]
{
    # 最後の10行を保持する循環バッファ
    buffer[NR % 10] = $0
}
END {
    # 最後の10行を出力
    for (i = NR - 9; i <= NR; i++) {
        print buffer[i % 10]
    }
}

データ加工のイディオム

[編集]

列の入れ替え

[編集]
# 1列目と2列目を入れ替える
{
    temp = $1
    $1 = $2
    $2 = temp
    print
}

特定列の抽出(cut相当)

[編集]
# カンマ区切りファイルから3,5,7列目を抽出
BEGIN { FS = OFS = "," }
{
    print $3, $5, $7
}

重複行の除去(uniq相当)

[編集]
# より効率的なuniq実装
!seen[$0]++ # これだけで重複除去が実現できる

# キー列での重複除去
!seen[$1,$2]++ # 1,2列の組み合わせでの重複除去

テキスト処理のイディオム

[編集]

行の前後に文字列追加

[編集]
# 各行を<li>タグで囲む
{ print "<li>" $0 "</li>" }

# インデント追加
{ print "    " $0 }

文字列の結合

[編集]
# 全行を連結(区切り文字なし)
{ str = str $0 }
END { print str }

# カンマ区切りで連結
{ 
    if (NR > 1) str = str "," 
    str = str $0
}
END { print str }

文字列の分割と結合

[編集]
# カンマ区切り文字列をスペース区切りに変換
BEGIN { FS = ","; OFS = " " }
{
    $1 = $1  # この代入で自動的にOFSで結合される
    print
}

計算処理のイディオム

[編集]

列の合計と平均

[編集]
# 複数列の合計と平均を同時計算
{
    for (i = 1; i <= NF; i++) {
        sum[i] += $i
        count[i]++
    }
}
END {
    for (i = 1; i <= length(sum); i++) {
        printf "Column %d: Sum = %f, Average = %f\n", 
               i, sum[i], sum[i]/count[i]
    }
}

最大値・最小値の検出

[編集]
# 初期化と更新を簡潔に記述
NR == 1 { 
    min = max = $1 
}
{
    if ($1 < min) min = $1
    if ($1 > max) max = $1
}

高度な処理イディオム

[編集]

2つのファイルの結合(join相当)

[編集]
# ファイル1から key-valueをハッシュに格納
FNR == NR {
    data[$1] = $2
    next
}
# ファイル2を処理しながら結合
{
    if ($1 in data) {
        print $0, data[$1]
    }
}

グループ集計

[編集]
# キーでグループ化して集計
{
    count[$1]++
    sum[$1] += $2
}
END {
    for (key in count) {
        printf "%s: count=%d, sum=%f, avg=%f\n", 
               key, count[key], sum[key], sum[key]/count[key]
    }
}

複数区切り文字の処理

[編集]
# カンマまたはセミコロンで区切られたデータの処理
BEGIN {
    # 正規表現をFSに設定
    FS = "[,;]"
    # または FPAT で値のパターンを指定
    FPAT = "([^,;]+)|(\"[^\"]+\")"
}

ファイル操作のイディオム

[編集]

複数ファイルへの出力

[編集]
# 条件に応じて異なるファイルに出力
{
    if ($3 > 100) {
        print > "high.txt"
    } else if ($3 > 50) {
        print > "medium.txt"
    } else {
        print > "low.txt"
    }
}

ファイル名をキーにした処理

[編集]
# ファイルごとの統計
{
    count[FILENAME]++
    sum[FILENAME] += $1
}
END {
    for (file in count) {
        printf "%s: records=%d, sum=%f\n", 
               file, count[file], sum[file]
    }
}

デバッグとトレースのイディオム

[編集]

デバッグ出力

[編集]
# 条件付きデバッグ出力
function debug(msg) {
    if (DEBUG) {
        printf "[DEBUG] %s (File=%s, Line=%d)\n", 
               msg, FILENAME, FNR > "/dev/stderr"
    }
}

実行時間計測

[編集]
# 処理時間の計測
BEGIN {
    start = systime()
}
END {
    printf "Processing time: %d seconds\n", 
           systime() - start > "/dev/stderr"
}

メモリ使用量の監視

[編集]
# 配列サイズの監視
function check_memory(arr, msg) {
    printf "Array size (%s): %d\n", 
           msg, length(arr) > "/dev/stderr"
}

エラー処理のイディオム

[編集]

入力検証

[編集]
# データ型と範囲のチェック
{
    if ($1 !~ /^[0-9]+$/) {
        printf "Error: Invalid number at line %d\n", 
               NR > "/dev/stderr"
        next
    }
    if ($2 < 0 || $2 > 100) {
        printf "Error: Value out of range at line %d\n", 
               NR > "/dev/stderr"
        next
    }
}

終了状態の設定

[編集]
# エラー発生時の終了コード設定
{
    if (error_condition) {
        print "Error message" > "/dev/stderr"
        exit 1
    }
}

これらのイディオムは、実際の開発現場で頻繁に使用される定型的なパターンです。状況に応じて適切に組み合わせることで、効率的なAWKプログラミングが可能になります。

AWKベストプラクティスガイド

[編集]

スクリプト構造とスタイル

[編集]

基本的なスクリプト構造

[編集]
#!/usr/bin/awk -f

# メタ情報
# Author: name
# Date: YYYY-MM-DD
# Description: スクリプトの目的

BEGIN {
    # 初期化
    initialize()
}

# メイン処理
function initialize() {
    # 変数の初期化
    FS = ","
    OFS = ","
    TOTAL = 0
}

# パターンとアクションを明確に分離
/pattern/ {
    process_record()
}

END {
    # 後処理
    finalize()
}

命名規則

[編集]
# 定数は大文字
BEGIN {
    MAX_RECORDS = 1000
    DEFAULT_VALUE = -1
}

# 関数名は動詞から始める
function calculate_average(values, length) {
    # ローカル変数は小文字
    local_sum = 0
    for (i = 1; i <= length; i++) {
        local_sum += values[i]
    }
    return local_sum / length
}

エラー処理とバリデーション

[編集]

入力データの検証

[編集]
{
    if (!validate_record($0)) {
        report_error("Invalid record format at line " NR)
        next
    }
    process_valid_record()
}

function validate_record(line) {
    if (NF != EXPECTED_FIELDS) {
        return 0
    }
    if ($1 !~ /^[0-9]+$/) {
        return 0
    }
    return 1
}

function report_error(message) {
    printf("ERROR: %s\n", message) > "/dev/stderr"
}

優雅なエラー処理

[編集]
function process_file(filename) {
    if ((getline < filename) < 0) {
        printf("ERROR: Cannot open file %s\n", filename) > "/dev/stderr"
        exit 1
    }
    close(filename)
}

function safe_divide(numerator, denominator,    result) {
    # 局所変数としてresultを使用
    if (denominator == 0) {
        return DEFAULT_VALUE
    }
    result = numerator / denominator
    return result
}

パフォーマンス最適化

[編集]

効率的なパターンマッチング

[編集]
# 悪い例
{
    if ($0 ~ /pattern1/) process1()
    if ($0 ~ /pattern2/) process2()
    if ($0 ~ /pattern3/) process3()
}

# 良い例
/pattern1/ { process1() }
/pattern2/ { process2() }
/pattern3/ { process3() }

メモリ使用の最適化

[編集]
# 大きなファイル処理時のメモリ管理
{
    # 必要なデータのみを保持
    key = $1
    if (key in cache) {
        if (length(cache) > MAX_CACHE_SIZE) {
            delete cache[get_oldest_key()]
        }
        cache[key] = $2
    }
}

function get_oldest_key(    k) {
    # 局所変数kを使用
    for (k in cache) {
        return k  # 最初のキーを返す
    }
}

モジュール化と再利用性

[編集]

関数の適切な分割

[編集]
# メイン処理を小さな関数に分割
{
    if (validate_input()) {
        process_record()
        update_statistics()
        generate_output()
    }
}

function validate_input() {
    return NF == EXPECTED_FIELDS
}

function process_record() {
    calculate_values()
    update_totals()
}

function update_statistics() {
    update_min_max()
    update_average()
}

設定の外部化

[編集]
# 設定を初期化ブロックにまとめる
BEGIN {
    # 設定値の集中管理
    Config["field_separator"] = ","
    Config["date_format"] = "%Y-%m-%d"
    Config["max_records"] = 1000
    
    # 設定の適用
    apply_configuration()
}

function apply_configuration() {
    FS = Config["field_separator"]
    OFS = Config["field_separator"]
}

デバッグとトレーサビリティ

[編集]

包括的なロギング

[編集]
function log_debug(message) {
    if (DEBUG) {
        printf("[DEBUG] %s:%d - %s\n", 
               FILENAME, FNR, message) > "/dev/stderr"
    }
}

function log_error(message) {
    printf("[ERROR] %s:%d - %s\n", 
           FILENAME, FNR, message) > "/dev/stderr"
}

function log_info(message) {
    printf("[INFO] %s\n", message) > "/dev/stderr"
}

デバッグ支援機能

[編集]
function dump_array(arr, prefix,    i) {
    # 配列の内容をデバッグ出力
    for (i in arr) {
        log_debug(sprintf("%s[%s] = %s", 
                 prefix, i, arr[i]))
    }
}

function trace_call(func_name) {
    # 関数呼び出しのトレース
    TRACE_DEPTH++
    printf("%*s-> %s\n", TRACE_DEPTH * 2, "", 
           func_name) > "/dev/stderr"
}

セキュリティ考慮事項

[編集]

入力のサニタイズ

[編集]
function sanitize_input(str) {
    # 危険な文字を除去
    gsub(/[;&|]/, "", str)
    return str
}

function validate_filename(filename) {
    # ファイル名の検証
    if (filename ~ /[^[:alnum:]._-]/) {
        return 0
    }
    return 1
}

安全なファイル操作

[編集]
function safe_open_file(filename,    status) {
    if (!validate_filename(filename)) {
        log_error("Invalid filename: " filename)
        return 0
    }
    status = (getline < filename) >= 0
    close(filename)
    return status
}

テスト可能性

[編集]

ユニットテスト用の構造

[編集]
# テスト用の関数
function test_calculate_average() {
    # テストデータの準備
    test_values[1] = 10
    test_values[2] = 20
    test_values[3] = 30
    
    # テストの実行
    result = calculate_average(test_values, 3)
    
    # 結果の検証
    if (result != 20) {
        log_error("Test failed: expected 20, got " result)
        return 0
    }
    return 1
}

統合テストのサポート

[編集]
# テストモードの実行
BEGIN {
    if (TEST_MODE) {
        run_tests()
        exit
    }
}

function run_tests(    test_count, passed) {
    test_count = passed = 0
    
    # テストの実行
    test_count++
    if (test_calculate_average()) passed++
    
    # テスト結果の報告
    printf("%d/%d tests passed\n", 
           passed, test_count) > "/dev/stderr"
}

ドキュメント化

[編集]

コードドキュメント

[編集]
# 関数のドキュメント
# Parameters:
#   values - 数値の配列
#   length - 配列の長さ
# Returns:
#   計算された平均値、エラー時は-1
function calculate_average(values, length) {
    # 実装
}

使用方法の説明

[編集]
function print_usage() {
    print "Usage: awk -f script.awk [options] file" > "/dev/stderr"
    print "Options:" > "/dev/stderr"
    print "  -v DEBUG=1     Enable debug output" > "/dev/stderr"
    print "  -v TEST_MODE=1 Run tests" > "/dev/stderr"
}

これらのベストプラクティスを適用することで、より保守性が高く、信頼性のあるAWKスクリプトを作成することができます。状況に応じて適切なプラクティスを選択し、組み合わせることが重要です。

さいごに

[編集]

AWKは単純なテキスト処理から複雑なデータ解析まで、幅広いタスクをこなすことができます。本ハンドブックで紹介した例や技術を基に、実際の問題解決に活用してください。

参考文献

[編集]
  • "The AWK Programming Language" (Aho, Weinberger & Kernighan)
  • "GAWK: Effective AWK Programming"
  • "Sed & Awk" (Dale Dougherty)