<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">超巨大JSONの省メモリ処理:POSIX awkによるSAX風ストリーミングパーサーの構築</h1>
<h2 class="wp-block-heading">【導入と前提】</h2>
<p>数十GBを超える巨大なJSONファイルを、メモリを消費せずに高速処理するパイプラインを構築します。SAX風のイベント駆動型処理により、低リソース環境でのバッチ自動化を実現します。</p>
<ul class="wp-block-list">
<li><p><strong>OS</strong>: Linux / Unix (POSIX準拠)</p></li>
<li><p><strong>必須ツール</strong>: <code>jq</code> (v1.6以上推奨), <code>awk</code> (mawk, gawk, nawk等), <code>curl</code></p></li>
</ul>
<h2 class="wp-block-heading">【処理フローと設計】</h2>
<p><code>jq --stream</code> をレキシカルアナライザ(字句解析器)として利用し、その出力を <code>awk</code> で作成したステートマシンに流し込むことで、メモリ使用量を一定(O/1)に保ちます。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["Remote/Local JSON Source"] -->|curl -s| B["jq --stream -c"]
B -->|Path/Value Stream| C["POSIX awk State Machine"]
C -->|Match Logic| D["Structured Log / DB Insert"]
C -->|Error| E["systemd-cat / stderr"]
</pre></div>
<ol class="wp-block-list">
<li><p><strong>Input</strong>: HTTP/S または ローカルファイルからのストリーム。</p></li>
<li><p><strong>Tokenizer</strong>: <code>jq --stream</code> が JSON を <code>[["path"], value]</code> 形式のフラットなストリームに変換。</p></li>
<li><p><strong>Processor</strong>: <code>awk</code> が現在のパスを監視し、特定の条件に合致した際にアクションを実行。</p></li>
</ol>
<h2 class="wp-block-heading">【実装:堅牢な自動化スクリプト】</h2>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/bin/sh
# ==============================================================================
# SAX-style JSON Stream Processor
# Description: Processes massive JSON streams using jq-stream and POSIX awk.
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# 一時ファイルのクリーンアップ設定
TMP_FILE=$(mktemp /tmp/json_proc.XXXXXX)
trap 'rm -f "$TMP_FILE"' EXIT
# 設定項目
TARGET_URL="${1:-http://localhost:8080/large_metrics.json}"
LOG_TAG="json-sax-parser"
# ログ出力関数(systemd-journal連携)
log_info() { echo "[INFO] $1" | systemd-cat -t "$LOG_TAG" -p info; }
log_err() { echo "[ERROR] $1" | systemd-cat -t "$LOG_TAG" -p err; }
log_info "Starting JSON stream processing..."
# メインパイプライン
# 1. curl: リトライ(-L: 追跡, --retry: 再試行)を有効にしてデータを取得
# 2. jq --stream: JSONをメモリに展開せず、パスと値のペアとして出力
# 3. awk: 状態を保持し、特定のパス(例: users[].id)が現れた時に処理
curl -sL --retry 3 "$TARGET_URL" | \
jq --stream -c 'select(length > 1)' | \
awk -F'[,:]' '
# POSIX awk によるステートマシン実装
BEGIN {
target_path = "\"id\"" # 抽出したいキー
}
{
# jq --stream の出力形式: [["users",0,"id"],"12345"]
# パス部分に target_path が含まれるか簡易チェック
if ($0 ~ target_path) {
# 値を抽出 (簡易的な文字列操作)
val = $NF
gsub(/[\[\]\"]/, "", val)
print "Processed ID: " val
}
}
END {
print "--- Stream Processing Finished ---"
}
' >> "$TMP_FILE"
log_info "Successfully processed records to $TMP_FILE"
</pre>
</div>
<h3 class="wp-block-heading">systemd タイマー設定例 (<code>/etc/systemd/system/json-stream.timer</code>)</h3>
<p>定期実行が必要な場合、Cronよりも詳細なログ管理が可能なsystemdタイマーを推奨します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">[Unit]
Description=Run JSON Stream Processor every hour
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
</pre>
</div>
<h2 class="wp-block-heading">【検証と運用】</h2>
<h3 class="wp-block-heading">正常系の確認</h3>
<p>実行後、journalctlでログを確認し、パイプラインの各ステップが正常終了しているかチェックします。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -t json-sax-parser -f
</pre>
</div>
<h3 class="wp-block-heading">パフォーマンス確認</h3>
<p>処理中のメモリ使用量がほぼ一定であることを <code>top</code> や <code>ps</code> で確認します。<code>jq</code> の通常実行と異なり、ファイルサイズに比例したメモリ増幅が抑えられていれば成功です。</p>
<h2 class="wp-block-heading">【トラブルシューティングと落とし穴】</h2>
<ul class="wp-block-list">
<li><p><strong>権限問題</strong>: <code>systemd-cat</code> を使用する場合、実行ユーザーにログ書き込み権限が必要です。コンテナ環境等では、単純な <code>logger</code> や <code>stderr</code> へのリダイレクトに切り替えてください。</p></li>
<li><p><strong>エスケープ文字</strong>: <code>awk</code> での文字列抽出時、JSON内のエスケープされたダブルクォート (<code>\"</code>) の扱いに注意が必要です。複雑なデコードが必要な場合は、<code>awk</code> 内で <code>gsub</code> を多用せず、<code>jq</code> 側でフィルタリングを完結させてください。</p></li>
<li><p><strong>パイプの切断</strong>: <code>set -o pipefail</code> を指定しているため、パイプラインのどこか一つがエラー(404 Not Found 等)を返すと、スクリプト全体が即座に異常終了します。</p></li>
</ul>
<h2 class="wp-block-heading">【まとめ】</h2>
<p>運用の冪等性と堅牢性を維持するための3つのポイント:</p>
<ol class="wp-block-list">
<li><p><strong>State-free Logic</strong>: <code>awk</code> 内に複雑な計算状態を持たせず、抽出と変換に特化させる。</p></li>
<li><p><strong>Stream Tokenization</strong>: 巨大データは絶対に一度にデコードせず、<code>jq --stream</code> 等でトークン化する。</p></li>
<li><p><strong>Observability</strong>: 実行ログを標準エラーではなく <code>systemd-cat</code> 等の構造化ログ基盤に流す。</p></li>
</ol>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
超巨大JSONの省メモリ処理:POSIX awkによるSAX風ストリーミングパーサーの構築
【導入と前提】
数十GBを超える巨大なJSONファイルを、メモリを消費せずに高速処理するパイプラインを構築します。SAX風のイベント駆動型処理により、低リソース環境でのバッチ自動化を実現します。
OS: Linux / Unix (POSIX準拠)
必須ツール: jq (v1.6以上推奨), awk (mawk, gawk, nawk等), curl
【処理フローと設計】
jq --stream をレキシカルアナライザ(字句解析器)として利用し、その出力を awk で作成したステートマシンに流し込むことで、メモリ使用量を一定(O/1)に保ちます。
graph TD
A["Remote/Local JSON Source"] -->|curl -s| B["jq --stream -c"]
B -->|Path/Value Stream| C["POSIX awk State Machine"]
C -->|Match Logic| D["Structured Log / DB Insert"]
C -->|Error| E["systemd-cat / stderr"]
Input: HTTP/S または ローカルファイルからのストリーム。
Tokenizer: jq --stream が JSON を [["path"], value] 形式のフラットなストリームに変換。
Processor: awk が現在のパスを監視し、特定の条件に合致した際にアクションを実行。
【実装:堅牢な自動化スクリプト】
#!/bin/sh
# ==============================================================================
# SAX-style JSON Stream Processor
# Description: Processes massive JSON streams using jq-stream and POSIX awk.
# ==============================================================================
set -euo pipefail
IFS=$'\n\t'
# 一時ファイルのクリーンアップ設定
TMP_FILE=$(mktemp /tmp/json_proc.XXXXXX)
trap 'rm -f "$TMP_FILE"' EXIT
# 設定項目
TARGET_URL="${1:-http://localhost:8080/large_metrics.json}"
LOG_TAG="json-sax-parser"
# ログ出力関数(systemd-journal連携)
log_info() { echo "[INFO] $1" | systemd-cat -t "$LOG_TAG" -p info; }
log_err() { echo "[ERROR] $1" | systemd-cat -t "$LOG_TAG" -p err; }
log_info "Starting JSON stream processing..."
# メインパイプライン
# 1. curl: リトライ(-L: 追跡, --retry: 再試行)を有効にしてデータを取得
# 2. jq --stream: JSONをメモリに展開せず、パスと値のペアとして出力
# 3. awk: 状態を保持し、特定のパス(例: users[].id)が現れた時に処理
curl -sL --retry 3 "$TARGET_URL" | \
jq --stream -c 'select(length > 1)' | \
awk -F'[,:]' '
# POSIX awk によるステートマシン実装
BEGIN {
target_path = "\"id\"" # 抽出したいキー
}
{
# jq --stream の出力形式: [["users",0,"id"],"12345"]
# パス部分に target_path が含まれるか簡易チェック
if ($0 ~ target_path) {
# 値を抽出 (簡易的な文字列操作)
val = $NF
gsub(/[\[\]\"]/, "", val)
print "Processed ID: " val
}
}
END {
print "--- Stream Processing Finished ---"
}
' >> "$TMP_FILE"
log_info "Successfully processed records to $TMP_FILE"
systemd タイマー設定例 (/etc/systemd/system/json-stream.timer)
定期実行が必要な場合、Cronよりも詳細なログ管理が可能なsystemdタイマーを推奨します。
[Unit]
Description=Run JSON Stream Processor every hour
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
【検証と運用】
正常系の確認
実行後、journalctlでログを確認し、パイプラインの各ステップが正常終了しているかチェックします。
journalctl -t json-sax-parser -f
パフォーマンス確認
処理中のメモリ使用量がほぼ一定であることを top や ps で確認します。jq の通常実行と異なり、ファイルサイズに比例したメモリ増幅が抑えられていれば成功です。
【トラブルシューティングと落とし穴】
権限問題: systemd-cat を使用する場合、実行ユーザーにログ書き込み権限が必要です。コンテナ環境等では、単純な logger や stderr へのリダイレクトに切り替えてください。
エスケープ文字: awk での文字列抽出時、JSON内のエスケープされたダブルクォート (\") の扱いに注意が必要です。複雑なデコードが必要な場合は、awk 内で gsub を多用せず、jq 側でフィルタリングを完結させてください。
パイプの切断: set -o pipefail を指定しているため、パイプラインのどこか一つがエラー(404 Not Found 等)を返すと、スクリプト全体が即座に異常終了します。
【まとめ】
運用の冪等性と堅牢性を維持するための3つのポイント:
State-free Logic: awk 内に複雑な計算状態を持たせず、抽出と変換に特化させる。
Stream Tokenization: 巨大データは絶対に一度にデコードせず、jq --stream 等でトークン化する。
Observability: 実行ログを標準エラーではなく systemd-cat 等の構造化ログ基盤に流す。
ライセンス:本記事のテキスト/コードは特記なき限り
CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。
コメント