巨大JSONストリームを低メモリで高速処理するPOSIX awkベースのSAX風パーサー実装

Tech

POSIX準拠awkによるSAX風JSONストリーミングパーサーの実装 SRE/DevOpsエンジニア Linux (POSIX sh/awk, jq, curl, systemd)

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

巨大JSONストリームを低メモリで高速処理するPOSIX awkベースのSAX風パーサー実装

【導入と前提】

メモリ制約のある環境で数GB規模の巨大JSONログを低フットプリントかつストリーミングで安全に抽出・パースする。(52文字)

  • 前提条件

    • OS: 汎用Linux (Alpine Linux, Ubuntu, RHEL等)

    • 必須ツール: POSIX準拠の sh, awk, curl, jq (動作比較・検証用)

    • 権限: ログ収集・systemdタイマー設定時の root または sudo 権限

【処理フローと設計】

入力されるJSONストリームを1文字ずつ状態遷移マシン(FSM)で評価し、メモリに巨大なAST(抽象構文木)を構築することなく、SAX(Simple API for XML)スタイルのイベント(START_OBJECT, KEY, VALUE, END_OBJECT等)として即座に出力・フィルタリングします。

graph TD
A["Input Stream"] -->|curl / cat| B["POSIX awk Parser FSM"]
B -->|State: IN_STRING| C{Escape?}
C -->|Yes| B
C -->|No| D["Emit Token Event"]
B -->|State: STRUCTURAL| E["Emit Control Event"]
D -->|Pipeline| F["jq / Processing Engine"]
E -->|Pipeline| F

【実装:堅牢な自動化スクリプト】

以下のスクリプトは、POSIX awk を利用してメモリ消費を一定(O(1))に保ちながらJSONストリームをパースするラッパーおよび実行処理です。

#!/bin/sh


# -----------------------------------------------------------------------------


# JSON SAX-style Streaming Parser in POSIX Shell & AWK


# -----------------------------------------------------------------------------

set -eu

# エラーハンドリングとクリーンアップ設定

TMP_LOG=$(mktemp /tmp/json_parser.XXXXXX)
trap 'rm -f "$TMP_LOG"' EXIT INT TERM

log_info() {
    printf '[INFO] %s\n' "$1"
}

log_error() {
    printf '[ERROR] %s\n' "$1" >&2
}

# 1. ネットワーク経由でのストリーム処理およびSAXパース

fetch_and_parse_json() {
    ENDPOINT_URL="$1"

    # curl オプション説明:


    # -s: 進捗メーターを非表示 (silent)


    # -S: エラー時はエラーメッセージを表示 (show-error)


    # -f: HTTPエラー (4xx/5xx) 発生時に正常終了させず失敗扱いにする (fail)


    # -L: リダイレクトを自動追跡 (location)

    curl -sSfL "$ENDPOINT_URL" | awk '
    BEGIN {
        state = "OUTSIDE"
        escaped = 0
        depth = 0
        key_buffer = ""
        val_buffer = ""
    }
    {
        len = length($0)
        for (i = 1; i <= len; i++) {
            c = substr($0, i, 1)

            if (state == "IN_STRING") {
                if (escaped) {
                    buffer = buffer c
                    escaped = 0
                } else if (c == "\\") {
                    escaped = 1
                } else if (c == "\"") {
                    state = "OUTSIDE"
                    print "STRING:" buffer
                    buffer = ""
                } else {
                    buffer = buffer c
                }
                continue
            }

            if (c == "\"") {
                state = "IN_STRING"
                buffer = ""
            } else if (c == "{") {
                depth++
                print "START_OBJECT:" depth
            } else if (c == "}") {
                print "END_OBJECT:" depth
                depth--
            } else if (c == "[") {
                depth++
                print "START_ARRAY:" depth
            } else if (c == "]") {
                print "END_ARRAY:" depth
                depth--
            } else if (c == ":") {
                print "COLON"
            } else if (c == ",") {
                print "COMMA"
            }
        }
    }'
}

# メイン処理の実行

TARGET_URL="https://api.github.com/zen"
log_info "Processing stream from ${TARGET_URL}..."

if fetch_and_parse_json "$TARGET_URL" > "$TMP_LOG"; then
    log_info "Parsing completed successfully."

    # 検証用に結果の一部を表示

    head -n 20 "$TMP_LOG"
else
    log_error "Failed to parse JSON stream."
    exit 1
fi

自動実行用 systemd 設定例

定期的なバッチログ解析を想定した systemd サービスおよびタイマー設定です。

/etc/systemd/system/json-stream-parser.service:

[Unit]
Description=POSIX AWK JSON Streaming Parser Service
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/json_stream_parser.sh
User=nobody
Group=nogroup
PrivateTmp=true
ProtectSystem=full

/etc/systemd/system/json-stream-parser.timer:

[Unit]
Description=Run JSON Streaming Parser hourly

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

【検証と運用】

正常系検証

作成したパーサーの出力形式と、標準ツール jq の構文チェックを組み合わせて動作を確認します。

# テストデータの生成とストリーミングパース検証

echo '{"key": "value", "nested": {"array": [1, 2]}}' | ./json_stream_parser.sh

# jq による構造検証の並列チェック(jqのオプション: -e はパース失敗時に終了ステータス1を返す)

curl -sSfL "https://api.github.com/zen" | jq -e . > /dev/null && echo "Valid JSON"

エラーログの確認 (systemd運用時)

# サービスの実行状態とログの出力確認 (journalctl オプション: -u でユニット指定、-n で行数指定)

journalctl -u json-stream-parser.service -n 50 --no-pager

【トラブルシューティングと落とし穴】

  1. POSIX awk における FS=""(空フィールド分離)の非標準挙動

    • 問題: GNU awk や一部の mawk では FS="" で1文字ずつ自動分割できますが、POSIX標準仕様では未定義動作です。

    • 対策: 上記コードの通り、length()substr() を用いて1文字ずつ走査することで、あらゆる環境(BusyBox awk, BSD awk 等)でポータブルに動作させます。

  2. 環境変数および資格情報の漏洩防止

    • 問題: HTTPヘッダーやAPIトークンをスクリプト引数として渡すと、ps コマンド等で他のプロセスから参照される危険性があります。

    • 対策: curl利用時は -H @file または環境変数から読み込む構成にし、trap を用いて一時設定ファイルを確実に削除 (rm -f) してください。

  3. Unicodeエスケープ(\uXXXX)および改行文字の扱い

    • 問題: JSON標準のエスケープシーケンスや文字列内の改行コードにより、awk の行指向処理(デフォルトの改行区切り)が崩れる場合があります。

    • 対策: 文字列状態(IN_STRING)のフラグ管理を厳密に行い、改行文字も状態機械の内部で蓄積・処理するように維持します。

【まとめ】

運用の冪等性と堅牢性を維持するための3つのポイント:

  1. メモリ消費の定数化 (O(1)): 巨大ファイルであっても一括ロードせず、状態遷移(FSM)によるストリーム処理を徹底する。

  2. ポータビリティの担保: POSIX準拠の標準コマンド仕様(sh / substrによる文字分解)を守り、実行環境への依存を最小化する。

  3. 安全な後始末(シグナルハンドリング): trap を利用して一時ファイルやパイプラインの残存プロセスを確実に解放する構造にする。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました