VSCodeで「正規表現マッチ抽出」を実現するTypeScriptマクロ【ログ解析が10倍速くなる】

Development
スポンサーリンク
スポンサーリンク

はじめに

サーバーログやアプリケーションログを解析する時、こんな悩みありませんか?

2024-02-08 10:00:00 INFO: Server started
2024-02-08 10:01:00 ERROR: Connection failed
2024-02-08 10:02:00 WARN: Timeout after 30s
2024-02-08 10:03:00 ERROR: Retry failed
2024-02-08 10:04:00 INFO: Request processed
... (数千行続く)

「ERRORだけ抽出したい」
「複数のパターンを一度に抽出したい」
「マッチした行番号も記録したい」

VSCodeの標準機能では、検索結果をコピペして整形…という非効率な作業になりがち。

今回、複数の正規表現パターンで一括抽出し、行番号付きで別ファイルに出力するTypeScriptマクロを実装しました。

ログ解析が劇的に効率化する、実務で必須のツールです。

なぜこのツールが必要なのか?

VSCode標準機能の限界

標準の検索機能(Ctrl+F)では:

  • 1つのパターンしか検索できない
  • マッチした行をファイルに保存できない
  • 行番号を一緒に記録できない
  • 複数パターンを比較できない

例:エラーと警告を同時に抽出したい場合

標準機能での手順:

  1. ERRORで検索
  2. マッチした行を手動でコピー
  3. 新規ファイルに貼り付け
  4. WARNで検索
  5. マッチした行を手動でコピー
  6. 同じファイルに追記
  7. 行番号は手動で追加…

面倒すぎる!

このマクロで実現できること

項目標準機能このマクロ
複数パターン抽出❌ 不可✅ 一括抽出
行番号記録❌ 手動✅ 自動
ファイル出力❌ 手動✅ 自動
実行履歴❌ なし✅ 日時記録
操作数7手順以上1キー操作

実装した機能

主な機能

ショートカット: Ctrl+Shift+4

複数パターン一括抽出

  • 選択範囲から複数の正規表現を読み取り
  • またはカンマ区切りで入力

行番号付き出力

  • マッチした行の行番号を記録
  • 元ファイルへのジャンプが簡単(Ctrl+G)

実行情報の記録

  • 実行日時(年月日時分秒)
  • 対象ファイル名
  • 各パターンのマッチ件数

別ファイル出力

  • 元ファイルを汚さない
  • ファイル名:元ファイル名_extracted_20240208_123456.txt

対応する入力方法

方法1: 選択範囲から読み取る(推奨)

# パターンリスト
ERROR
WARN.*timeout
Failed to .*
Exception.*

上記を選択してCtrl+Shift+4実行 → 4パターンで一括抽出

方法2: カンマ区切りで入力

選択せずにCtrl+Shift+4実行 → ダイアログ表示

ERROR, WARN.*timeout, Failed to .*

出力例

============================================================
正規表現マッチ抽出結果
============================================================
実行日時: 2024-02-08 15:30:45
対象ファイル: server.log
抽出パターン数: 3

------------------------------------------------------------
パターン1: ERROR
マッチ件数: 2 件
------------------------------------------------------------
[Line 145] 2024-02-08 10:01:00 ERROR: Connection failed
[Line 389] 2024-02-08 10:03:00 ERROR: Retry failed

------------------------------------------------------------
パターン2: WARN.*timeout
マッチ件数: 1 件
------------------------------------------------------------
[Line 267] 2024-02-08 10:02:00 WARN: Timeout after 30s

------------------------------------------------------------
パターン3: Failed to .*
マッチ件数: 1 件
------------------------------------------------------------
[Line 389] 2024-02-08 10:03:00 ERROR: Retry failed

============================================================
抽出完了
============================================================
マッチ件数: パターン1=2件, パターン2=1件, パターン3=1件
合計: 4 件

実務での活用例

例1:エラーログの解析

シーン:本番障害の調査

# パターンリスト
ERROR
FATAL
Exception

結果:

  • エラー系のログだけを抽出
  • 行番号から前後の文脈を確認
  • 障害の原因を特定

例2:特定ユーザーの行動追跡

シーン:不正アクセスの調査

# パターンリスト
user_id=12345
IP: 192.168.1.100
Failed login.*12345

結果:

  • 特定ユーザーの全アクセスログを抽出
  • 不正なアクセスパターンを発見

例3:パフォーマンス問題の調査

シーン:レスポンスタイムの劣化

# パターンリスト
Slow query.*
timeout.*exceeded
Response time: [5-9]\d{3}ms

結果:

  • 遅いクエリやタイムアウトを抽出
  • ボトルネックを特定

例4:複数サーバーのログ比較

シーン:サーバーA・B・Cでエラー率比較

# 各サーバーのログで実行
ERROR
WARN

結果:

  • サーバーA: ERROR=50件, WARN=30件
  • サーバーB: ERROR=120件, WARN=80件 ← 異常
  • サーバーC: ERROR=45件, WARN=35件

例5:定期メンテナンスの記録

シーン:夜間バッチの成否確認

# パターンリスト
Batch.*started
Batch.*completed
Batch.*failed

結果:

  • バッチ処理の開始・完了・失敗を記録
  • 実行時刻も自動記録

TypeScriptコード解説

メイン処理(extractByRegex.ts)

import * as vscode from 'vscode';
import * as path from 'path';

/**
 * 正規表現マッチ抽出ツール
 */
export async function extractByRegex() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('アクティブなエディタがありません');
        return;
    }

    const document = editor.document;
    const selection = editor.selection;
    const fileName = path.basename(document.fileName);

    // 選択範囲からパターンを取得
    let patterns: string[];

    if (!selection.isEmpty) {
        // 選択範囲がある場合
        patterns = getSelectedPatterns(document, selection);
        
        if (patterns.length === 0) {
            vscode.window.showWarningMessage('有効なパターンが選択されていません');
            return;
        }
    } else {
        // ダイアログで入力
        const input = await vscode.window.showInputBox({
            prompt: '抽出する正規表現パターンを入力(複数の場合はカンマ区切り)',
            placeHolder: 'ERROR, WARN.*timeout, Failed to .*',
            value: 'ERROR'
        });

        if (!input) {
            return;
        }

        patterns = input.split(',')
            .map(p => p.trim())
            .filter(p => p.length > 0);
    }

    // 抽出実行
    const results = await extractMatches(document, patterns);

    if (results.totalCount === 0) {
        vscode.window.showInformationMessage('マッチする行が見つかりませんでした');
        return;
    }

    // 結果を新規ファイルに出力
    await createResultFile(fileName, patterns, results);

    vscode.window.showInformationMessage(
        `抽出完了: 合計 ${results.totalCount} 件のマッチ`
    );
}

選択範囲からパターンを取得

/**
 * 選択範囲から正規表現パターンを取得
 */
function getSelectedPatterns(
    document: vscode.TextDocument,
    selection: vscode.Selection
): string[] {
    const patterns: string[] = [];
    const startLine = selection.start.line;
    const endLine = selection.end.line;

    for (let i = startLine; i <= endLine; i++) {
        const lineText = document.lineAt(i).text.trim();
        
        // 空行やコメント行はスキップ
        if (!lineText || lineText.startsWith('#') || lineText.startsWith('//')) {
            continue;
        }

        // 正規表現として有効かチェック
        try {
            new RegExp(lineText);
            patterns.push(lineText);
        } catch (e) {
            vscode.window.showWarningMessage(`不正な正規表現をスキップ: ${lineText}`);
        }
    }

    return patterns;
}

マッチする行を抽出

/**
 * マッチした行の情報
 */
interface MatchedLine {
    lineNumber: number;  // 1-based行番号
    text: string;        // 行のテキスト
}

/**
 * ドキュメントから複数パターンでマッチする行を抽出
 */
async function extractMatches(
    document: vscode.TextDocument,
    patterns: string[]
): Promise {
    const matches: MatchResult[] = [];
    let totalCount = 0;

    // パターンごとに処理
    for (const pattern of patterns) {
        const regex = new RegExp(pattern);
        const matchedLines: MatchedLine[] = [];

        // 全行をチェック
        for (let i = 0; i < document.lineCount; i++) {
            const line = document.lineAt(i);
            if (regex.test(line.text)) {
                matchedLines.push({
                    lineNumber: i + 1,  // 1-based
                    text: line.text
                });
            }
        }

        matches.push({
            pattern,
            lines: matchedLines
        });

        totalCount += matchedLines.length;
    }

    // 現在時刻
    const now = new Date();
    const timestamp = formatDateTime(now);

    return {
        timestamp,
        sourceFile: path.basename(document.fileName),
        matches,
        totalCount
    };
}

出力内容の生成

/**
 * 出力内容を生成
 */
function generateOutput(
    sourceFileName: string,
    patterns: string[],
    results: ExtractResults
): string {
    const lines: string[] = [];

    // ヘッダー
    lines.push('=' .repeat(60));
    lines.push('正規表現マッチ抽出結果');
    lines.push('=' .repeat(60));
    lines.push(`実行日時: ${results.timestamp}`);
    lines.push(`対象ファイル: ${sourceFileName}`);
    lines.push(`抽出パターン数: ${patterns.length}`);
    lines.push('');

    // パターンごとの結果
    for (let i = 0; i < results.matches.length; i++) {
        const match = results.matches[i];
        
        lines.push('-'.repeat(60));
        lines.push(`パターン${i + 1}: ${match.pattern}`);
        lines.push(`マッチ件数: ${match.lines.length} 件`);
        lines.push('-'.repeat(60));

        if (match.lines.length > 0) {
            // 行番号の桁数を計算(揃えるため)
            const maxLineNumber = Math.max(...match.lines.map(l => l.lineNumber));
            const lineNumberWidth = String(maxLineNumber).length;

            match.lines.forEach(matchedLine => {
                const lineNumberStr = String(matchedLine.lineNumber).padStart(lineNumberWidth, ' ');
                lines.push(`[Line ${lineNumberStr}] ${matchedLine.text}`);
            });
        } else {
            lines.push('(マッチなし)');
        }

        lines.push('');
    }

    // サマリー
    lines.push('=' .repeat(60));
    lines.push('抽出完了');
    lines.push('=' .repeat(60));
    
    const summary = results.matches
        .map((m, i) => `パターン${i + 1}=${m.lines.length}件`)
        .join(', ');
    
    lines.push(`マッチ件数: ${summary}`);
    lines.push(`合計: ${results.totalCount} 件`);
    lines.push('');

    return lines.join('\n');
}

ファイル出力

/**
 * 結果を新規ファイルに出力
 */
async function createResultFile(
    sourceFileName: string,
    patterns: string[],
    results: ExtractResults
) {
    // 出力内容を生成
    const output = generateOutput(sourceFileName, patterns, results);

    // 新規ドキュメントを作成
    const doc = await vscode.workspace.openTextDocument({
        content: output,
        language: 'log'
    });

    // エディタで開く(隣に表示)
    await vscode.window.showTextDocument(doc, {
        viewColumn: vscode.ViewColumn.Beside,
        preview: false
    });

    // ファイル名を提案
    const suggestedName = generateFileName(sourceFileName, new Date());
    
    // 保存ダイアログを表示
    const uri = await vscode.window.showSaveDialog({
        defaultUri: vscode.Uri.file(suggestedName),
        filters: {
            'Text files': ['txt', 'log'],
            'All files': ['*']
        }
    });

    if (uri) {
        await vscode.workspace.fs.writeFile(
            uri,
            Buffer.from(output, 'utf8')
        );

        vscode.window.showInformationMessage(`結果を保存しました: ${path.basename(uri.fsPath)}`);
    }
}

/**
 * 出力ファイル名を生成
 */
function generateFileName(sourceFileName: string, date: Date): string {
    const nameWithoutExt = path.parse(sourceFileName).name;
    const timestamp = formatDateTimeForFile(date);
    return `${nameWithoutExt}_extracted_${timestamp}.txt`;
}

使い方

1. 拡張機能のインストール

# GitHubからクローン
git clone https://github.com/xxxxx-sys/my-macros.git
cd my-macros

# 依存関係インストール
npm install --legacy-peer-deps

# パッケージ化
npm run compile
npx vsce package

# インストール
code --install-extension my-macros-0.0.5.vsix

2. 基本的な使い方

方法1: 選択範囲から抽出(推奨)

patterns.txt
---
# エラー系
ERROR
FATAL
Exception.*

# 警告系
WARN
timeout
  1. パターンリストを作成
  2. 抽出したいパターンを選択
  3. 対象ログファイルに切り替え
  4. Ctrl+Shift+4で実行

方法2: カンマ区切りで入力

  1. 対象ログファイルを開く
  2. Ctrl+Shift+4で実行
  3. ダイアログに入力:ERROR, WARN, timeout
  4. Enter

3. 実践例

シーン1: エラーログの集計

server.log(10,000行)
---
2024-02-08 10:00:00 INFO: Started
2024-02-08 10:01:00 ERROR: Connection failed
...

パターン:

ERROR
FATAL

実行: Ctrl+Shift+4

結果:```server_extracted_20240208_150000.txt

パターン1: ERROR → 50件
パターン2: FATAL → 5件
合計: 55件


**シーン2: 特定期間のログ抽出**

**パターン:**

2024-02-08 1[0-2]:.*ERROR


**結果:** 10時~12時台のERRORのみ抽出

**シーン3: 複数条件の組み合わせ**

**パターン:**

user_id=12345.*ERROR
user_id=12345.*WARN
user_id=12345.*login


**結果:** 特定ユーザーの全アクティビティを抽出

### 4. 行番号からのジャンプ

抽出結果:

[Line 145] 2024-02-08 10:01:00 ERROR: Connection failed


**元ファイルへジャンプ:**
1. 元ファイル(server.log)を開く
2. `Ctrl+G`
3. `145`を入力してEnter
4. → 該当行に即ジャンプ!

## package.jsonの設定

```json
{
  "contributes": {
    "commands": [
      {
        "command": "myMacros.extractByRegex",
        "title": "Extract by Regex Pattern",
        "category": "My Macros"
      }
    ],
    "keybindings": [
      {
        "command": "myMacros.extractByRegex",
        "key": "ctrl+shift+4",
        "when": "editorTextFocus"
      }
    ]
  }
}

メリット・デメリット

メリット

✅ 圧倒的な効率化

  • 複数パターンを一度に抽出
  • 手作業なら10分以上かかる作業が数秒

✅ 行番号で前後確認が簡単

  • エラーの発生箇所を即座に特定
  • 前後の文脈を確認してデバッグ

✅ 実行履歴の保存

  • いつ何を抽出したか記録
  • 調査の再現性が高い

✅ 正規表現で柔軟な抽出

  • 複雑な条件も指定可能
  • パターンを保存して再利用

✅ 元ファイルを汚さない

  • 別ファイルに出力
  • 元のログは変更なし

デメリット

❌ 巨大ファイルは処理に時間がかかる

  • 数万行を超えるログは数秒待つ
  • ただし手作業よりは圧倒的に速い

❌ 正規表現の知識が必要

  • 基本的なパターンは簡単
  • 複雑な条件は学習コストあり

❌ リアルタイム抽出は非対応

  • tail -f のようなリアルタイム監視は不可
  • ファイル全体の一括抽出のみ

トラブルシューティング

Q: パターンが認識されない

A: 正規表現の構文をチェック

NG: [未完成の正規表現
OK: \[完成した正規表現\]

NG: (グループ化ミス
OK: (正しいグループ化)

不正な正規表現は自動的にスキップされます。

Q: マッチする行が見つからない

A: パターンを確認

# 大文字小文字を区別
ERROR → "error"はマッチしない

# 解決策:大文字小文字を無視
[Ee][Rr][Rr][Oo][Rr]
または
(?i)error

Q: 行番号がずれる

A: 元ファイルを編集していませんか?

抽出後に元ファイルを編集すると、行番号がずれます。
抽出は編集前に実行してください。

Q: ファイルが保存できない

A: パスに権限があるか確認

# 書き込み権限のあるフォルダに保存
NG: C:\Program Files\...(権限不足)
OK: C:\Users\[ユーザー名]\Documents\...

まとめ

VSCodeで複数の正規表現パターンで一括抽出し、行番号付きで別ファイルに出力する機能を実装しました。

この機能が役立つ人:

  • サーバーログを頻繁に解析する
  • エラー調査をよくする
  • 複数パターンを同時に抽出したい
  • 調査の履歴を残したい
  • テキスト処理の効率化を図りたい

特にログ解析では、標準機能で10分以上かかる作業を数秒で完了できるため、実務での生産性向上に大きく貢献します。

サーバー障害の調査、パフォーマンス問題の特定、不正アクセスの追跡など、様々なシーンで活用できる革新的なツールです。

ぜひ試してみてください!

関連記事

サクラエディタからの移行経緯

サクラエディタからVSCodeへマクロ移行!快適開発環境の構築記録
はじめに長年愛用してきたサクラエディタのマクロ機能。便利なJavaScript/VBSマクロを多数作成して日常業務で活用してきましたが、最近のAWS開発やブログ執筆でVSCodeを使う機会が増えてきました。「VSCodeでもサクラエディタの...

TypeScriptマクロ開発環境の構築方法

VSCode TypeScriptマクロ開発環境の完全ガイド【セットアップから運用まで】
はじめにVSCodeでカスタムマクロを作成したいけど、どうやって開発環境を構築すればいいか分からない。そんな悩みを持つ方に向けて、TypeScriptでVSCode拡張機能を開発する環境の構築から、実際にマクロを作成して使えるようにするまで...

選択行による一括置換マクロ

VSCodeで「選択行による一括置換」を実現するTypeScriptマクロ【テキスト編集が10倍速くなる】
はじめにテキスト編集で「複数の文字列を一度に置換したい」と思ったことはありませんか?例えば、こんなシーン:変数名を複数箇所で一括変更テーブル定義のデータ型を複数列で一括更新ドキュメント内の用語統一を複数ワードで一括実施VSCodeの標準機能...

タグ: #VSCode #TypeScript #マクロ #正規表現 #ログ解析 #生産性向上 #エラー調査

コメント