はじめに
2014年ごろ、組み込み機器の開発で使うログを、確認しやすい形へ変換するための小さなPHPスクリプトを何本か作りました。
この記事は、PHPの基本構文を読める初心者向けの振り返りです。ログ解析全般ではなく、決まった形式のテキストをCLIで整形する狭い範囲に絞り、一部の実装詳細は省略します。
いま見返すと、1本あたり約30行です。フレームワークも、クラスも、設定ファイルもありません。入力ファイルを1行ずつ読み、区切り文字で分解し、必要な形で標準出力へ出すだけです。
いわゆる「誰得ツール」です。
何分短縮できたか、何回使ったかは、もう覚えていません。特定の大きな不具合を見つけた主役でもありませんでした。目的はもっと素朴で、日々の作業を少し便利にすることでした。
立派なアプリケーションを作るほどではないものの、目視とコピー&ペーストを続けるのは面倒です。その狭い隙間を埋めるための、便利Tipsに近いコードでした。
この記事では、当時のスクリプトが何をしていたのかを振り返り、公開できる形に抽象化して作り直します。
この記事の入力例、名前、メッセージは説明用に新しく作った架空データです。製品の実ログ、製品名、ソースコード上の識別子は掲載していません。
この記事の前提
- 対象読者: 手作業のテキスト整形を小さなCLIで自動化したい方
- 前提知識: PHPの変数、配列、条件分岐、ファイル読み込みの基礎
- 経験時点: 元のスクリプトは2014年ごろ
- 2026年時点の補足: 書き直した例はPHP 8以降を対象とし、元の実行環境を再現するものではありません
- 対象外: 汎用ログ収集基盤、巨大ファイルの高速処理、複数行にまたがるレコード
なぜPHPだったのか
現在なら、この種のテキスト処理を見てPythonを思い浮かべる方も多いと思います。しかし、当時はまだPythonが社内で一般的ではなく、PHPのほうが身近で、社内にも根付いていました。
最適な言語を比較して選んだというより、必要な処理を、その場で使える言語ですぐ形にしたという判断です。
過去のツールを現在の定番だけで評価すると、「なぜPythonを使わなかったのか」という話になりがちです。しかし、道具の選択は、言語機能だけでなく、当時の社内環境、既存資産、実行できるOS、周囲が読めるかどうかにも左右されます。
先に結論
この小さなツールから持ち帰れるのは、次の3点です。
- 一度しか使わないように見えても、同じ整形を繰り返すなら自動化の候補になる
- 使い捨てツールほど、入力形式と異常行の扱いを決めておく
- 過去の業務コードを公開するときは、伏せ字にせず、架空データと新しい実装で再構成する
当時のコードは目的を果たしました。一方で、入力チェック、文字コード、エラー終了、テストという点では、公開サンプルにそのまま使える品質ではありません。
「昔のコードをきれいに見せる」のではなく、当時なぜ必要だったかと、いまならどこを直すかをセットで残すのが、今回の記事化の方針です。
何を自動化していたのか
残っていたPHPスクリプトは、大きく2種類に分かれます。
定義一覧から確認用のメッセージを作る
入力は、呼び出し元、項目名、値の種類などをタブで区切った一覧です。
Sensor::setThreshold() temperature number
Network::open() socket system_error
Storage::write() path text
これを、レビューや確認で読みやすいメッセージへ変換します。
*ERR* [Sensor::setThreshold()] temperature is out of range => [値]
*ERR* [Network::open()] socket => [システムエラーメッセージ]
元の一覧を見ながら1行ずつメッセージを組み立てる代わりに、機械的な部分をスクリプトへ任せていました。
ログらしき1行から必要な列を抜き出す
もう一方は、時刻や識別子、関数の引数などを区切り文字で取り出し、カンマ区切りで出力するスクリプトです。
表計算ソフトへ貼り付けたり、差分を見たりする前処理として使う発想でした。
機能は小さいのですが、入力と出力がファイルではなく標準入出力に寄っていた点は、今見ても悪くありません。シェルのリダイレクトと組み合わせられるからです。
php format_log.php definitions.tsv > expected.log
当時の実装
当時のコードは、ほぼ次の流れでした。
<?php
$filePath = $argv[1];
$fp = fopen($filePath, 'r');
while (!feof($fp)) {
$line = fgets($fp);
$line = str_replace(["\n", '"'], '', $line);
$columns = explode("\t", $line);
print('*ERR* [' . $columns[0] . '] ');
print($columns[1] . PHP_EOL);
}
fclose($fp);
短く、やりたいことはすぐ分かります。一方、入力が想定どおりでなければ簡単に崩れます。
- 引数がない場合を処理していない
- ファイルを開けなかった場合の終了コードがない
-
while (!feof(...))により、終端で空行を1回処理する可能性がある - 列数を確認せず、配列の要素を参照している
- 改行や引用符を一律に削除している
- スキップした行や変換できなかった行が分からない
- Shift_JISとUTF-8が混在すると、コメントや出力が文字化けする
自分だけが、決まった入力へ使うなら動きます。しかし、公開リポジトリに置くと、利用者は別の改行、空行、不足列、余分なタブを持ち込んできます。
使い捨てであっても、壊れた入力を黙って正常出力に混ぜないことが重要です。
公開用に最小限作り直す
当時の処理を、製品固有の情報を使わずに書き直すと、次のようになります。
<?php
declare(strict_types=1);
if ($argc !== 2) {
fwrite(STDERR, "Usage: php format_log.php <definitions.tsv>\n");
exit(2);
}
$handle = fopen($argv[1], 'rb');
if ($handle === false) {
fwrite(STDERR, "Cannot open input file: {$argv[1]}\n");
exit(1);
}
$lineNumber = 0;
$errorCount = 0;
while (($line = fgets($handle)) !== false) {
$lineNumber++;
$line = rtrim($line, "\r\n");
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
$columns = str_getcsv(
string: $line,
separator: "\t",
enclosure: '"',
escape: ''
);
if (count($columns) !== 3) {
fwrite(
STDERR,
"Line {$lineNumber}: expected 3 columns, got "
. count($columns)
. "\n"
);
$errorCount++;
continue;
}
[$context, $item, $kind] = $columns;
$detail = match ($kind) {
'number' => '[VALUE]',
'system_error' => '[SYSTEM ERROR]',
'text' => '[TEXT]',
default => '[UNKNOWN]',
};
printf("*ERR* [%s] %s => %s\n", $context, $item, $detail);
}
fclose($handle);
exit($errorCount === 0 ? 0 : 1);
機能は大きく増やしていません。変えたのは、主に境界の扱いです。
- 引数の数を確認する
- 読めないファイルをエラーにする
-
fgets()の戻り値でループを制御する - 空行とコメント行を明示的に扱う
- TSVとして解析する
- 列数が違う行を標準エラーへ出す
- 1行でも変換に失敗したら、終了コードを0にしない
この例は2026年時点のPHPで説明するために書き直したコードです。str_starts_with()とmatchを使うため、PHP 8以降を前提にしています。当時のPHP 5系へそのまま戻すサンプルではありません。また、str_getcsv()のescapeは省略せず、空文字列を明示しています。
「動いた行」と「捨てた行」を分ける
ログ整形ツールで怖いのは、プログラムが停止することだけではありません。変換できなかった行を黙って捨て、欠けた出力を完全な結果だと思ってしまうことです。
そのため、公開用の実装では次の役割を分けます。
| 出力先 | 内容 |
|---|---|
| 標準出力 | 正常に変換できた行 |
| 標準エラー | 行番号と変換できなかった理由 |
| 終了コード | すべて変換できたかどうか |
これなら、正常出力だけをファイルへ保存しつつ、問題のあった行を画面で確認できます。
php format_log.php definitions.tsv > expected.log
自動テストやバッチ処理から呼ぶ場合も、終了コードで成否を判断できます。
1本へまとめすぎない
残っていたコードには、用途が少しずつ違う複数のスクリプトがありました。
- 通常エラーの整形
- 範囲外エラーの整形
- システムエラーの整形
- ログから特定の引数を抽出
現在なら、共通のパーサーと出力形式を用意して1本へ寄せる設計もできます。ただし、設定項目を増やしすぎると、今度は使うたびにオプションを思い出す必要があります。
この規模では、「1ファイルを渡すと1種類の結果が出る」という単純さにも価値があります。
共通化するなら、次の条件がそろってからで十分です。
- 入力形式が本当に同じ
- 異常行の扱いが同じ
- 出力を使う相手が同じ
- 修正時に複数ファイルへ同じ変更が発生している
似ているだけで早くまとめると、用途ごとの小さな差が条件分岐として残ります。
公開リポジトリには実ログを入れない
今回、最も注意が必要なのはコードよりデータです。
実ログや変換元一覧には、次の情報が入り得ます。
- 製品名や機種名
- 内部クラス名、関数名、ファイル名、行番号
- ホスト名、ユーザー名、IPアドレス
- 起動時刻、障害時刻、ソフトウェアのバージョン
- チャンネル数やアラーム名などの製品仕様
- 実際に発生した障害と、その再現条件
名称だけをDEVICE_Aへ置換しても、項目の組み合わせや件数から元の仕様が見えることがあります。
そこで公開用リポジトリは、元の作業フォルダをコピーして不要ファイルを削るのではなく、空のリポジトリから作ります。
php-log-helper/
├─ README.md
├─ LICENSE
├─ src/
│ └─ format_log.php
├─ samples/
│ └─ definitions.tsv
└─ tests/
└─ format_log_test.php
サンプルログも、実ログの一部を切り出さず、説明用に新しく作ります。これなら、削除し忘れたファイルや過去のGit履歴から、元データが公開される事故を避けやすくなります。
今振り返って残したいこと
このPHPスクリプトは、汎用ログ解析基盤ではありませんでした。
決まった形式を読み、決まった形式へ変えるだけです。それでも、同じ確認を何度も手で行うよりは確実で、作業の入口をそろえる役には立ちました。
過去の小さなツールを記事にするとき、コードの古さを隠す必要はないと思っています。
- 当時は何に困っていたのか
- その短いコードで、どの手作業を消したのか
- どの前提に強く依存していたのか
- 公開するなら、何を作り直す必要があるのか
ここまで書けば、30行の誰得ツールにも、別の現場へ持ち帰れるものが残ります。
覚えていない成果を作らない
このツールについて、「作業時間を何分減らした」「何件の不具合を発見した」といった数字は残っておらず、記憶にもありません。
そのため、この記事では効果を後から作らず、次の事実だけを残します。
- 手作業を便利にする目的で作った
- 当時の社内で根付いていたPHPを使った
- 製品データを整形する小さな専用スクリプトだった
- コードは自分でゼロベースから作った
- 実データは機微情報なので公開しない
劇的な成功談がなくても、「この程度の面倒なら30行ほどの専用ツールで片付ける」という判断自体が、当時の便利Tipsとして残せる内容だと思っています。
