简单工具中心

tutorial

正規表現テスターの完全実装ガイド - パターンマッチングを極める高度な文字列処理技術

プロ級正規表現テスターの包括的実装ガイド。パターン設計、性能最適化、リアルタイム検証、可視化機能まで詳細解説。

32分钟阅读
正規表現テスターの完全実装ガイド - パターンマッチングを極める高度な文字列処理技術

正規表現テスターの完全実装ガイド

正規表現(Regular Expression)は、文字列パターンマッチングの最も強力なツールの一つです。適切に活用することで、複雑なテキスト処理、データ検証、ログ解析などを効率的に実行できます。

本ガイドでは、プロダクションレベルの正規表現テスターの完全実装について、基礎理論から実践的な最適化技術まで包括的に解説します。

正規表現エンジンの基本原理

1. 正規表現パーサーとコンパイラ

高性能な正規表現テストエンジン

// 包括的な正規表現テストシステムの実装
class RegexTester {
    constructor() {
        this.testResults = [];
        this.performanceMetrics = {
            totalTests: 0,
            averageExecutionTime: 0,
            maxExecutionTime: 0,
            minExecutionTime: Infinity
        };
        this.cache = new Map();
        this.compiledPatterns = new Map();
        this.validationRules = new Map();
        this.flagDescriptions = {
            'g': '全体マッチング(Global)',
            'i': '大文字小文字を無視(Case-insensitive)',
            'm': '複数行モード(Multiline)',
            's': 'ドットが改行にもマッチ(Dotall)',
            'u': 'Unicode対応(Unicode)',
            'y': '粘着性マッチング(Sticky)'
        };
        this.metaCharacters = {
            '.': '任意の一文字(改行以外)',
            '^': '行の開始',
            '$': '行の終了',
            '*': '0回以上の繰り返し',
            '+': '1回以上の繰り返し',
            '?': '0回または1回',
            '[]': '文字クラス',
            '()': 'グループ化',
            '|': '論理OR',
            '\\': 'エスケープ文字',
            '{}': '量詞(回数指定)'
        };
    }

    // メイン正規表現テスト機能
    test(pattern, testString, flags = '', options = {}) {
        const startTime = performance.now();
        const testId = this.generateTestId();

        const result = {
            id: testId,
            pattern,
            flags,
            testString,
            timestamp: new Date(),
            matches: [],
            isValid: false,
            executionTime: 0,
            groups: [],
            namedGroups: {},
            globalMatches: [],
            errors: [],
            warnings: [],
            analysis: {},
            suggestions: []
        };

        try {
            // パターンの妥当性チェック
            const validationResult = this.validatePattern(pattern, flags);
            if (!validationResult.isValid) {
                result.errors = validationResult.errors;
                result.warnings = validationResult.warnings;
                return this.finalizeResult(result, startTime);
            }

            // 正規表現オブジェクト作成
            const regex = this.createRegexObject(pattern, flags);
            result.isValid = true;

            // マッチング実行
            if (flags.includes('g')) {
                result.globalMatches = this.executeGlobalMatch(regex, testString);
                result.matches = result.globalMatches.map(match => match.match);
            } else {
                const match = this.executeSingleMatch(regex, testString);
                if (match) {
                    result.matches = [match.match];
                    result.groups = match.groups;
                    result.namedGroups = match.namedGroups;
                }
            }

            // パフォーマンス分析
            result.analysis = this.analyzePattern(pattern, flags, testString);

            // 改善提案生成
            result.suggestions = this.generateSuggestions(pattern, flags, result);

        } catch (error) {
            result.errors.push({
                type: 'EXECUTION_ERROR',
                message: error.message,
                stack: error.stack
            });
        }

        return this.finalizeResult(result, startTime);
    }

    // パターン妥当性検証
    validatePattern(pattern, flags) {
        const result = {
            isValid: true,
            errors: [],
            warnings: []
        };

        // 基本構文チェック
        try {
            new RegExp(pattern, flags);
        } catch (error) {
            result.isValid = false;
            result.errors.push({
                type: 'SYNTAX_ERROR',
                message: error.message,
                position: this.findErrorPosition(error.message, pattern)
            });
            return result;
        }

        // パフォーマンスに関する警告
        const performanceIssues = this.analyzePerformanceIssues(pattern);
        result.warnings.push(...performanceIssues);

        // セキュリティ検査
        const securityIssues = this.analyzeSecurityIssues(pattern);
        result.warnings.push(...securityIssues);

        // 最適化提案
        const optimizationSuggestions = this.analyzeOptimizationOpportunities(pattern);
        result.warnings.push(...optimizationSuggestions);

        return result;
    }

    analyzePerformanceIssues(pattern) {
        const issues = [];

        // ネストした量詞チェック
        if (/(\*|\+|\?|\{\d+,\}).*(\*|\+|\?|\{\d+,\})/.test(pattern)) {
            issues.push({
                type: 'PERFORMANCE_WARNING',
                message: 'ネストした量詞は指数的バックトラッキングを引き起こす可能性があります',
                severity: 'medium',
                recommendation: '量詞の組み合わせを見直してください'
            });
        }

        // 選択肢の長いパターン
        const alternations = pattern.split('|').length;
        if (alternations > 10) {
            issues.push({
                type: 'PERFORMANCE_WARNING',
                message: '選択肢が多すぎるとパフォーマンスが低下します',
                severity: 'low',
                recommendation: 'パターンを分割するか文字クラスを使用してください'
            });
        }

        // 非効率的な文字クラス
        if (/\[a-zA-Z0-9\]/.test(pattern)) {
            issues.push({
                type: 'OPTIMIZATION_WARNING',
                message: '\\w を使用してより効率的に書けます',
                severity: 'info',
                recommendation: '[a-zA-Z0-9_] の代わりに \\w を使用'
            });
        }

        return issues;
    }

    analyzeSecurityIssues(pattern) {
        const issues = [];

        // ReDoS攻撃の可能性
        const redosPatterns = [
            /\(.*\*.*\)\*/, // (a*)*
            /\(.*\+.*\)\+/, // (a+)+
            /\(.*\*.*\)\+/, // (a*)+
            /\(.*\+.*\)\*/ // (a+)*
        ];

        redosPatterns.forEach(redosPattern => {
            if (redosPattern.test(pattern)) {
                issues.push({
                    type: 'SECURITY_WARNING',
                    message: 'ReDoS(Regular expression Denial of Service)攻撃の脆弱性があります',
                    severity: 'high',
                    recommendation: 'ネストした量詞を避け、原子的グループや所有的量詞を検討してください'
                });
            }
        });

        return issues;
    }

    analyzeOptimizationOpportunities(pattern) {
        const opportunities = [];

        // アンカーの使用推奨
        if (!pattern.startsWith('^') && !pattern.endsWith('$') && pattern.length > 10) {
            opportunities.push({
                type: 'OPTIMIZATION_SUGGESTION',
                message: 'アンカー(^ または $)の使用を検討してください',
                severity: 'info',
                recommendation: '不要な文字列の探索を避けるためアンカーを使用'
            });
        }

        // 非キャプチャグループの推奨
        const captureGroups = (pattern.match(/\([^?]/g) || []).length;
        if (captureGroups > 3) {
            opportunities.push({
                type: 'OPTIMIZATION_SUGGESTION',
                message: '非キャプチャグループ (?:) の使用を検討してください',
                severity: 'info',
                recommendation: 'キャプチャが不要な場合は (?:) を使用'
            });
        }

        return opportunities;
    }

    // 正規表現オブジェクト作成(キャッシュ付き)
    createRegexObject(pattern, flags) {
        const cacheKey = `${pattern}::${flags}`;

        if (this.compiledPatterns.has(cacheKey)) {
            return this.compiledPatterns.get(cacheKey);
        }

        const regex = new RegExp(pattern, flags);
        this.compiledPatterns.set(cacheKey, regex);

        // キャッシュサイズ管理
        if (this.compiledPatterns.size > 1000) {
            const firstKey = this.compiledPatterns.keys().next().value;
            this.compiledPatterns.delete(firstKey);
        }

        return regex;
    }

    // グローバルマッチ実行
    executeGlobalMatch(regex, testString) {
        const matches = [];
        let match;

        while ((match = regex.exec(testString)) !== null) {
            const matchInfo = {
                match: match[0],
                index: match.index,
                lastIndex: regex.lastIndex,
                groups: [...match].slice(1),
                namedGroups: match.groups || {},
                length: match[0].length
            };

            matches.push(matchInfo);

            // 無限ループ防止
            if (match.index === regex.lastIndex) {
                regex.lastIndex++;
            }

            // 安全のための制限
            if (matches.length > 10000) {
                break;
            }
        }

        return matches;
    }

    // 単一マッチ実行
    executeSingleMatch(regex, testString) {
        const match = regex.exec(testString);

        if (match) {
            return {
                match: match[0],
                index: match.index,
                groups: [...match].slice(1),
                namedGroups: match.groups || {},
                length: match[0].length,
                input: match.input
            };
        }

        return null;
    }

    // パターン分析
    analyzePattern(pattern, flags, testString) {
        return {
            complexity: this.calculatePatternComplexity(pattern),
            characterCount: pattern.length,
            groupCount: this.countGroups(pattern),
            quantifierCount: this.countQuantifiers(pattern),
            characterClassCount: this.countCharacterClasses(pattern),
            alternationCount: this.countAlternations(pattern),
            anchorCount: this.countAnchors(pattern),
            backtrackingRisk: this.assessBacktrackingRisk(pattern),
            readabilityScore: this.calculateReadabilityScore(pattern),
            estimatedComplexity: this.estimateTimeComplexity(pattern, testString.length)
        };
    }

    calculatePatternComplexity(pattern) {
        let complexity = 0;

        // 基本文字は1ポイント
        complexity += pattern.length;

        // 特殊文字は追加ポイント
        const specialChars = pattern.match(/[.*+?^${}()|[\]\\]/g) || [];
        complexity += specialChars.length * 2;

        // ネストしたグループは指数的に複雑
        const groupDepth = this.calculateGroupDepth(pattern);
        complexity += Math.pow(2, groupDepth);

        // 量詞は複雑度を上げる
        const quantifiers = pattern.match(/[*+?]|\{\d+,?\d*\}/g) || [];
        complexity += quantifiers.length * 3;

        return Math.min(complexity, 1000); // 上限設定
    }

    calculateGroupDepth(pattern) {
        let maxDepth = 0;
        let currentDepth = 0;

        for (let i = 0; i < pattern.length; i++) {
            if (pattern[i] === '(' && (i === 0 || pattern[i-1] !== '\\')) {
                currentDepth++;
                maxDepth = Math.max(maxDepth, currentDepth);
            } else if (pattern[i] === ')' && (i === 0 || pattern[i-1] !== '\\')) {
                currentDepth--;
            }
        }

        return maxDepth;
    }

    countGroups(pattern) {
        return (pattern.match(/\(/g) || []).length;
    }

    countQuantifiers(pattern) {
        return (pattern.match(/[*+?]|\{\d+,?\d*\}/g) || []).length;
    }

    countCharacterClasses(pattern) {
        return (pattern.match(/\[[^\]]*\]/g) || []).length;
    }

    countAlternations(pattern) {
        return (pattern.match(/\|/g) || []).length;
    }

    countAnchors(pattern) {
        return (pattern.match(/[\^$]/g) || []).length;
    }

    assessBacktrackingRisk(pattern) {
        const riskFactors = [];

        // ネストした量詞
        if (/(\*|\+|\?)\s*(\*|\+|\?)/.test(pattern)) {
            riskFactors.push('nested_quantifiers');
        }

        // 選択肢を含む量詞
        if (/\([^)]*\|[^)]*\)[\*\+]/.test(pattern)) {
            riskFactors.push('quantified_alternation');
        }

        // 重複する文字クラス
        if (/\[.*[a-z].*\].*\[.*[a-z].*\]/.test(pattern)) {
            riskFactors.push('overlapping_character_classes');
        }

        return {
            level: riskFactors.length > 0 ? 'high' : 'low',
            factors: riskFactors
        };
    }

    calculateReadabilityScore(pattern) {
        let score = 100;

        // 長すぎるパターン
        if (pattern.length > 50) score -= 20;
        if (pattern.length > 100) score -= 30;

        // エスケープが多い
        const escapeCount = (pattern.match(/\\/g) || []).length;
        if (escapeCount > 10) score -= 15;

        // ネストが深い
        const depth = this.calculateGroupDepth(pattern);
        if (depth > 3) score -= depth * 5;

        // 選択肢が多い
        const alternationCount = this.countAlternations(pattern);
        if (alternationCount > 5) score -= alternationCount * 3;

        return Math.max(0, score);
    }

    estimateTimeComplexity(pattern, inputLength) {
        const complexity = this.calculatePatternComplexity(pattern);
        const backtrackingRisk = this.assessBacktrackingRisk(pattern);

        if (backtrackingRisk.level === 'high') {
            return `O(2^${inputLength})`;
        } else if (complexity > 100) {
            return `O(${inputLength}^2)`;
        } else {
            return `O(${inputLength})`;
        }
    }

    // 改善提案生成
    generateSuggestions(pattern, flags, result) {
        const suggestions = [];

        // パフォーマンス改善提案
        if (result.analysis.complexity > 100) {
            suggestions.push({
                type: 'performance',
                message: 'パターンが複雑すぎます。シンプルな形に分解することを検討してください',
                priority: 'high'
            });
        }

        if (result.analysis.backtrackingRisk.level === 'high') {
            suggestions.push({
                type: 'performance',
                message: 'バックトラッキングリスクが高いです。原子的グループの使用を検討してください',
                priority: 'high',
                example: '(?>pattern) または (?:pattern)+ の使用'
            });
        }

        // 可読性改善提案
        if (result.analysis.readabilityScore < 50) {
            suggestions.push({
                type: 'readability',
                message: 'パターンの可読性が低いです。コメントや分割を検討してください',
                priority: 'medium'
            });
        }

        // 機能改善提案
        if (!flags.includes('u') && /[\u{10000}-\u{10FFFF}]/.test(pattern)) {
            suggestions.push({
                type: 'functionality',
                message: 'Unicodeフラグ(u)の使用を推奨します',
                priority: 'medium'
            });
        }

        // セキュリティ提案
        if (result.warnings.some(w => w.type === 'SECURITY_WARNING')) {
            suggestions.push({
                type: 'security',
                message: 'セキュリティリスクがあります。入力サイズの制限を検討してください',
                priority: 'high'
            });
        }

        return suggestions;
    }

    // 結果の最終化
    finalizeResult(result, startTime) {
        result.executionTime = performance.now() - startTime;

        // パフォーマンスメトリクス更新
        this.updatePerformanceMetrics(result.executionTime);

        // 結果保存
        this.testResults.push(result);

        // 履歴サイズ管理
        if (this.testResults.length > 1000) {
            this.testResults.shift();
        }

        return result;
    }

    updatePerformanceMetrics(executionTime) {
        this.performanceMetrics.totalTests++;
        this.performanceMetrics.maxExecutionTime = Math.max(
            this.performanceMetrics.maxExecutionTime,
            executionTime
        );
        this.performanceMetrics.minExecutionTime = Math.min(
            this.performanceMetrics.minExecutionTime,
            executionTime
        );

        const totalTime = this.performanceMetrics.averageExecutionTime *
                         (this.performanceMetrics.totalTests - 1);
        this.performanceMetrics.averageExecutionTime =
            (totalTime + executionTime) / this.performanceMetrics.totalTests;
    }

    // ユーティリティメソッド
    generateTestId() {
        return `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    findErrorPosition(errorMessage, pattern) {
        const positionMatch = errorMessage.match(/at position (\d+)/);
        if (positionMatch) {
            return parseInt(positionMatch[1]);
        }
        return -1;
    }

    // 正規表現説明生成
    explainPattern(pattern) {
        const explanation = {
            overall: '',
            parts: [],
            flags: {},
            examples: []
        };

        // パターンの各部分を解析
        explanation.parts = this.parsePatternParts(pattern);

        // 全体的な説明
        explanation.overall = this.generateOverallExplanation(pattern);

        // 使用例生成
        explanation.examples = this.generateExamples(pattern);

        return explanation;
    }

    parsePatternParts(pattern) {
        const parts = [];
        let i = 0;

        while (i < pattern.length) {
            const char = pattern[i];
            const part = { char, position: i, type: '', description: '' };

            if (char === '\\' && i + 1 < pattern.length) {
                const nextChar = pattern[i + 1];
                part.char = char + nextChar;
                part.type = 'escape';
                part.description = this.getEscapeDescription(nextChar);
                i += 2;
            } else if (char === '[') {
                const closeIndex = pattern.indexOf(']', i + 1);
                if (closeIndex !== -1) {
                    part.char = pattern.substring(i, closeIndex + 1);
                    part.type = 'character_class';
                    part.description = `文字クラス: ${part.char}`;
                    i = closeIndex + 1;
                } else {
                    part.type = 'literal';
                    part.description = `リテラル文字: ${char}`;
                    i++;
                }
            } else if (char === '(') {
                const closeIndex = this.findMatchingParen(pattern, i);
                if (closeIndex !== -1) {
                    part.char = pattern.substring(i, closeIndex + 1);
                    part.type = 'group';
                    part.description = `グループ: ${part.char}`;
                    i = closeIndex + 1;
                } else {
                    part.type = 'literal';
                    part.description = `リテラル文字: ${char}`;
                    i++;
                }
            } else if (this.metaCharacters[char]) {
                part.type = 'meta';
                part.description = this.metaCharacters[char];
                i++;
            } else {
                part.type = 'literal';
                part.description = `リテラル文字: ${char}`;
                i++;
            }

            parts.push(part);
        }

        return parts;
    }

    getEscapeDescription(char) {
        const escapes = {
            'd': '数字 [0-9]',
            'D': '数字以外 [^0-9]',
            'w': '単語文字 [a-zA-Z0-9_]',
            'W': '単語文字以外 [^a-zA-Z0-9_]',
            's': '空白文字',
            'S': '空白文字以外',
            't': 'タブ文字',
            'n': '改行文字',
            'r': '復帰文字',
            'b': '単語境界',
            'B': '単語境界以外'
        };

        return escapes[char] || `エスケープされた文字: ${char}`;
    }

    findMatchingParen(pattern, start) {
        let depth = 0;
        for (let i = start; i < pattern.length; i++) {
            if (pattern[i] === '(' && (i === 0 || pattern[i-1] !== '\\')) {
                depth++;
            } else if (pattern[i] === ')' && (i === 0 || pattern[i-1] !== '\\')) {
                depth--;
                if (depth === 0) {
                    return i;
                }
            }
        }
        return -1;
    }

    generateOverallExplanation(pattern) {
        let explanation = '此の正規表現は';

        if (pattern.startsWith('^')) {
            explanation += '行の開始から';
        }

        if (pattern.endsWith('$')) {
            explanation += '行の終了までを';
        }

        explanation += 'マッチします。';

        return explanation;
    }

    generateExamples(pattern) {
        // パターンから使用例を自動生成(簡易版)
        const examples = [];

        try {
            const regex = new RegExp(pattern);

            // 基本的なテストケース
            const testCases = [
                'hello world',
                'test123',
                'user@example.com',
                '2023-12-25',
                '(123) 456-7890',
                'https://example.com',
                'abc 123 xyz',
                'Hello, World!',
                '日本語テスト',
                ''
            ];

            testCases.forEach(testCase => {
                const match = regex.exec(testCase);
                if (match) {
                    examples.push({
                        input: testCase,
                        match: match[0],
                        isMatch: true
                    });
                }
            });

        } catch (error) {
            // パターンが無効な場合はスキップ
        }

        return examples.slice(0, 5); // 最大5個まで
    }
}

2. ビジュアル正規表現エディタ

インタラクティブな正規表現可視化システム

class VisualRegexEditor {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.tester = new RegexTester();
        this.currentPattern = '';
        this.currentFlags = '';
        this.testString = '';
        this.matchHighlights = [];
        this.syntaxTree = null;
        this.setupUI();
        this.setupEventListeners();
    }

    setupUI() {
        this.container.innerHTML = `
            <div class="regex-editor">
                <div class="input-section">
                    <div class="pattern-input">
                        <label>正規表現パターン:</label>
                        <div class="input-wrapper">
                            <span class="delimiter">/</span>
                            <input type="text" id="pattern-input" class="pattern-field" placeholder="正規表現を入力">
                            <span class="delimiter">/</span>
                            <input type="text" id="flags-input" class="flags-field" placeholder="フラグ" maxlength="6">
                        </div>
                    </div>
                    <div class="flags-helper">
                        <div class="flag-buttons">
                            <button type="button" data-flag="g" title="Global">g</button>
                            <button type="button" data-flag="i" title="Case-insensitive">i</button>
                            <button type="button" data-flag="m" title="Multiline">m</button>
                            <button type="button" data-flag="s" title="Dotall">s</button>
                            <button type="button" data-flag="u" title="Unicode">u</button>
                            <button type="button" data-flag="y" title="Sticky">y</button>
                        </div>
                    </div>
                    <div class="test-input">
                        <label>テスト文字列:</label>
                        <textarea id="test-string" class="test-field" rows="6" placeholder="テスト用の文字列を入力"></textarea>
                    </div>
                </div>

                <div class="visualization-section">
                    <div class="pattern-breakdown">
                        <h3>パターン解析</h3>
                        <div id="pattern-explanation" class="explanation"></div>
                        <div id="syntax-tree" class="syntax-tree"></div>
                    </div>

                    <div class="match-results">
                        <h3>マッチ結果</h3>
                        <div id="match-visualization" class="match-display"></div>
                        <div id="match-details" class="match-info"></div>
                    </div>
                </div>

                <div class="analysis-section">
                    <div class="performance-metrics">
                        <h3>パフォーマンス分析</h3>
                        <div id="performance-data" class="metrics"></div>
                    </div>

                    <div class="suggestions-panel">
                        <h3>改善提案</h3>
                        <div id="suggestions-list" class="suggestions"></div>
                    </div>
                </div>

                <div class="tools-section">
                    <button id="test-button" class="btn primary">テスト実行</button>
                    <button id="explain-button" class="btn secondary">パターン説明</button>
                    <button id="optimize-button" class="btn secondary">最適化提案</button>
                    <button id="save-button" class="btn secondary">保存</button>
                    <button id="load-button" class="btn secondary">読み込み</button>
                </div>
            </div>
        `;
    }

    setupEventListeners() {
        const patternInput = document.getElementById('pattern-input');
        const flagsInput = document.getElementById('flags-input');
        const testStringInput = document.getElementById('test-string');

        // リアルタイム更新
        patternInput.addEventListener('input', this.debounce(this.onPatternChange.bind(this), 300));
        flagsInput.addEventListener('input', this.debounce(this.onFlagsChange.bind(this), 300));
        testStringInput.addEventListener('input', this.debounce(this.onTestStringChange.bind(this), 300));

        // フラグボタン
        document.querySelectorAll('[data-flag]').forEach(button => {
            button.addEventListener('click', this.onFlagButtonClick.bind(this));
        });

        // ツールボタン
        document.getElementById('test-button').addEventListener('click', this.runTest.bind(this));
        document.getElementById('explain-button').addEventListener('click', this.explainPattern.bind(this));
        document.getElementById('optimize-button').addEventListener('click', this.showOptimizations.bind(this));
        document.getElementById('save-button').addEventListener('click', this.savePattern.bind(this));
        document.getElementById('load-button').addEventListener('click', this.loadPattern.bind(this));
    }

    debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    onPatternChange(event) {
        this.currentPattern = event.target.value;
        this.updateSyntaxHighlighting();
        this.runTest();
    }

    onFlagsChange(event) {
        this.currentFlags = event.target.value;
        this.updateFlagButtons();
        this.runTest();
    }

    onTestStringChange(event) {
        this.testString = event.target.value;
        this.runTest();
    }

    onFlagButtonClick(event) {
        const flag = event.target.dataset.flag;
        const flagsInput = document.getElementById('flags-input');
        let flags = flagsInput.value;

        if (flags.includes(flag)) {
            flags = flags.replace(flag, '');
        } else {
            flags += flag;
        }

        flagsInput.value = flags;
        this.currentFlags = flags;
        this.updateFlagButtons();
        this.runTest();
    }

    updateFlagButtons() {
        document.querySelectorAll('[data-flag]').forEach(button => {
            const flag = button.dataset.flag;
            button.classList.toggle('active', this.currentFlags.includes(flag));
        });
    }

    updateSyntaxHighlighting() {
        const patternInput = document.getElementById('pattern-input');

        // シンタックスハイライト(実際の実装では専用ライブラリを使用)
        patternInput.classList.remove('error', 'warning');

        try {
            new RegExp(this.currentPattern, this.currentFlags);
            patternInput.classList.add('valid');
        } catch (error) {
            patternInput.classList.add('error');
            this.displayError('パターンエラー: ' + error.message);
        }
    }

    runTest() {
        if (!this.currentPattern || !this.testString) {
            this.clearResults();
            return;
        }

        const result = this.tester.test(this.currentPattern, this.testString, this.currentFlags);
        this.displayResults(result);
    }

    displayResults(result) {
        this.displayMatchVisualization(result);
        this.displayMatchDetails(result);
        this.displayPerformanceMetrics(result);
        this.displaySuggestions(result);

        if (result.errors.length > 0) {
            this.displayErrors(result.errors);
        }
    }

    displayMatchVisualization(result) {
        const container = document.getElementById('match-visualization');
        let html = '';

        if (result.matches.length === 0) {
            html = '<div class="no-matches">マッチしませんでした</div>';
        } else {
            html = this.createHighlightedText(this.testString, result);
        }

        container.innerHTML = html;
    }

    createHighlightedText(text, result) {
        const parts = [];
        let lastIndex = 0;

        if (result.globalMatches && result.globalMatches.length > 0) {
            result.globalMatches.forEach((match, index) => {
                // マッチ前のテキスト
                if (match.index > lastIndex) {
                    parts.push({
                        text: text.substring(lastIndex, match.index),
                        type: 'text'
                    });
                }

                // マッチ部分
                parts.push({
                    text: match.match,
                    type: 'match',
                    index: index,
                    groups: match.groups
                });

                lastIndex = match.index + match.length;
            });

            // 残りのテキスト
            if (lastIndex < text.length) {
                parts.push({
                    text: text.substring(lastIndex),
                    type: 'text'
                });
            }
        }

        return parts.map(part => {
            switch (part.type) {
                case 'match':
                    return `<span class="match highlight-${part.index % 5}" title="Match ${part.index + 1}">${this.escapeHtml(part.text)}</span>`;
                case 'text':
                    return this.escapeHtml(part.text);
                default:
                    return '';
            }
        }).join('');
    }

    displayMatchDetails(result) {
        const container = document.getElementById('match-details');
        let html = '';

        if (result.matches.length === 0) {
            html = '<div class="no-details">マッチの詳細はありません</div>';
        } else {
            html = '<div class="match-summary">';
            html += `<div class="summary-item">マッチ数: <strong>${result.matches.length}</strong></div>`;
            html += `<div class="summary-item">実行時間: <strong>${result.executionTime.toFixed(3)}ms</strong></div>`;
            html += '</div>';

            if (result.globalMatches) {
                html += '<div class="matches-list">';
                result.globalMatches.forEach((match, index) => {
                    html += `<div class="match-item">
                        <div class="match-header">マッチ ${index + 1}</div>
                        <div class="match-content">
                            <div class="match-value">"${this.escapeHtml(match.match)}"</div>
                            <div class="match-position">位置: ${match.index} - ${match.index + match.length - 1}</div>
                            ${match.groups.length > 0 ? this.renderGroups(match.groups) : ''}
                        </div>
                    </div>`;
                });
                html += '</div>';
            }
        }

        container.innerHTML = html;
    }

    renderGroups(groups) {
        let html = '<div class="groups-section"><div class="groups-title">グループ:</div>';
        groups.forEach((group, index) => {
            if (group !== undefined) {
                html += `<div class="group-item">$${index + 1}: "${this.escapeHtml(group)}"</div>`;
            }
        });
        html += '</div>';
        return html;
    }

    displayPerformanceMetrics(result) {
        const container = document.getElementById('performance-data');
        const analysis = result.analysis;

        const html = `
            <div class="metrics-grid">
                <div class="metric">
                    <label>実行時間:</label>
                    <span class="${this.getPerformanceClass(result.executionTime)}">${result.executionTime.toFixed(3)}ms</span>
                </div>
                <div class="metric">
                    <label>複雑度:</label>
                    <span class="${this.getComplexityClass(analysis.complexity)}">${analysis.complexity}</span>
                </div>
                <div class="metric">
                    <label>バックトラッキングリスク:</label>
                    <span class="${analysis.backtrackingRisk.level}">${analysis.backtrackingRisk.level}</span>
                </div>
                <div class="metric">
                    <label>可読性スコア:</label>
                    <span class="${this.getReadabilityClass(analysis.readabilityScore)}">${analysis.readabilityScore}/100</span>
                </div>
                <div class="metric">
                    <label>推定時間複雑度:</label>
                    <span>${analysis.estimatedComplexity}</span>
                </div>
            </div>
        `;

        container.innerHTML = html;
    }

    displaySuggestions(result) {
        const container = document.getElementById('suggestions-list');

        if (result.suggestions.length === 0 && result.warnings.length === 0) {
            container.innerHTML = '<div class="no-suggestions">改善提案はありません</div>';
            return;
        }

        let html = '';

        // 警告
        if (result.warnings.length > 0) {
            html += '<div class="warnings-section"><h4>警告</h4>';
            result.warnings.forEach(warning => {
                html += `<div class="warning-item ${warning.severity}">
                    <div class="warning-message">${warning.message}</div>
                    ${warning.recommendation ? `<div class="warning-recommendation">${warning.recommendation}</div>` : ''}
                </div>`;
            });
            html += '</div>';
        }

        // 提案
        if (result.suggestions.length > 0) {
            html += '<div class="suggestions-section"><h4>改善提案</h4>';
            result.suggestions.forEach(suggestion => {
                html += `<div class="suggestion-item ${suggestion.priority}">
                    <div class="suggestion-message">${suggestion.message}</div>
                    ${suggestion.example ? `<div class="suggestion-example">例: ${suggestion.example}</div>` : ''}
                </div>`;
            });
            html += '</div>';
        }

        container.innerHTML = html;
    }

    explainPattern() {
        if (!this.currentPattern) return;

        const explanation = this.tester.explainPattern(this.currentPattern);
        this.displayPatternExplanation(explanation);
    }

    displayPatternExplanation(explanation) {
        const container = document.getElementById('pattern-explanation');

        let html = `<div class="explanation-overview">${explanation.overall}</div>`;

        if (explanation.parts.length > 0) {
            html += '<div class="pattern-parts">';
            explanation.parts.forEach(part => {
                html += `<div class="pattern-part ${part.type}">
                    <span class="part-char">${this.escapeHtml(part.char)}</span>
                    <span class="part-description">${part.description}</span>
                </div>`;
            });
            html += '</div>';
        }

        if (explanation.examples.length > 0) {
            html += '<div class="explanation-examples"><h4>使用例:</h4>';
            explanation.examples.forEach(example => {
                html += `<div class="example-item">
                    <span class="example-input">"${this.escapeHtml(example.input)}"</span>
                    ${example.isMatch ? `<span class="example-match">→ "${this.escapeHtml(example.match)}"</span>` : '<span class="example-no-match">マッチしません</span>'}
                </div>`;
            });
            html += '</div>';
        }

        container.innerHTML = html;
    }

    // ユーティリティメソッド
    escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    getPerformanceClass(time) {
        if (time > 100) return 'slow';
        if (time > 10) return 'moderate';
        return 'fast';
    }

    getComplexityClass(complexity) {
        if (complexity > 200) return 'high';
        if (complexity > 50) return 'medium';
        return 'low';
    }

    getReadabilityClass(score) {
        if (score > 80) return 'good';
        if (score > 50) return 'fair';
        return 'poor';
    }

    clearResults() {
        document.getElementById('match-visualization').innerHTML = '';
        document.getElementById('match-details').innerHTML = '';
        document.getElementById('performance-data').innerHTML = '';
        document.getElementById('suggestions-list').innerHTML = '';
    }

    displayError(message) {
        const container = document.getElementById('match-visualization');
        container.innerHTML = `<div class="error-message">${message}</div>`;
    }

    // 保存・読み込み機能
    savePattern() {
        const data = {
            pattern: this.currentPattern,
            flags: this.currentFlags,
            testString: this.testString,
            savedAt: new Date().toISOString()
        };

        localStorage.setItem('regex-tester-saved', JSON.stringify(data));
        alert('パターンが保存されました');
    }

    loadPattern() {
        const saved = localStorage.getItem('regex-tester-saved');
        if (saved) {
            const data = JSON.parse(saved);

            document.getElementById('pattern-input').value = data.pattern;
            document.getElementById('flags-input').value = data.flags;
            document.getElementById('test-string').value = data.testString;

            this.currentPattern = data.pattern;
            this.currentFlags = data.flags;
            this.testString = data.testString;

            this.updateFlagButtons();
            this.runTest();

            alert('パターンが読み込まれました');
        } else {
            alert('保存されたパターンがありません');
        }
    }
}

3. 正規表現ライブラリとパターン集

よく使用される正規表現パターンライブラリ

class RegexPatternLibrary {
    constructor() {
        this.patterns = new Map();
        this.categories = new Map();
        this.loadBuiltinPatterns();
    }

    loadBuiltinPatterns() {
        // メールアドレス
        this.addPattern('email', {
            pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
            description: '基本的なメールアドレス形式',
            category: 'validation',
            examples: {
                valid: ['user@example.com', 'test.email+tag@domain.co.jp'],
                invalid: ['invalid.email', 'user@', '@domain.com']
            },
            tags: ['email', 'validation', 'contact']
        });

        // 強力なメールアドレス(RFC 5322準拠)
        this.addPattern('email_rfc5322', {
            pattern: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
            description: 'RFC 5322準拠の厳密なメールアドレス',
            category: 'validation',
            complexity: 'high',
            tags: ['email', 'rfc5322', 'strict']
        });

        // 電話番号(日本)
        this.addPattern('phone_jp', {
            pattern: /^(\+81|0)[0-9]{1,4}-?[0-9]{1,4}-?[0-9]{3,4}$/,
            description: '日本の電話番号形式',
            category: 'validation',
            examples: {
                valid: ['090-1234-5678', '03-1234-5678', '+81-90-1234-5678'],
                invalid: ['123-456', '090-12-345']
            },
            tags: ['phone', 'japan', 'mobile']
        });

        // URL
        this.addPattern('url', {
            pattern: /^https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$/,
            description: 'HTTP/HTTPS URL',
            category: 'validation',
            examples: {
                valid: ['https://example.com', 'http://test.co.jp/path?param=value'],
                invalid: ['ftp://example.com', 'not-a-url']
            },
            tags: ['url', 'http', 'web']
        });

        // IPv4アドレス
        this.addPattern('ipv4', {
            pattern: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
            description: 'IPv4アドレス',
            category: 'network',
            examples: {
                valid: ['192.168.1.1', '10.0.0.1', '255.255.255.255'],
                invalid: ['256.1.1.1', '192.168.1', '192.168.1.1.1']
            },
            tags: ['ip', 'network', 'ipv4']
        });

        // 日付(YYYY-MM-DD)
        this.addPattern('date_iso', {
            pattern: /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/,
            description: 'ISO 8601日付形式 (YYYY-MM-DD)',
            category: 'date',
            examples: {
                valid: ['2023-12-25', '2023-01-01', '2023-12-31'],
                invalid: ['2023-13-01', '2023-12-32', '23-12-25']
            },
            tags: ['date', 'iso8601', 'time']
        });

        // 日本の郵便番号
        this.addPattern('zipcode_jp', {
            pattern: /^\d{3}-?\d{4}$/,
            description: '日本の郵便番号',
            category: 'location',
            examples: {
                valid: ['123-4567', '1234567'],
                invalid: ['12-345', '123-45678']
            },
            tags: ['zipcode', 'japan', 'postal']
        });

        // クレジットカード番号
        this.addPattern('credit_card', {
            pattern: /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$/,
            description: '主要なクレジットカード番号',
            category: 'payment',
            examples: {
                valid: ['4111111111111111', '5555555555554444'],
                invalid: ['1234567890123456', '411111111111111']
            },
            tags: ['credit', 'payment', 'finance'],
            sensitive: true
        });

        // パスワード強度(強)
        this.addPattern('password_strong', {
            pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
            description: '強いパスワード(小文字、大文字、数字、特殊文字を含む8文字以上)',
            category: 'security',
            examples: {
                valid: ['MyPass123!', 'SecureP@ssw0rd'],
                invalid: ['password', 'PASSWORD', '12345678']
            },
            tags: ['password', 'security', 'validation']
        });

        // HTMLタグ
        this.addPattern('html_tag', {
            pattern: /<\/?[\w\s="/.':;#-\/\?]+>/gi,
            description: 'HTMLタグ',
            category: 'parsing',
            examples: {
                valid: ['<div>', '<p class="text">', '</body>'],
                invalid: ['<div', 'div>', 'not a tag']
            },
            tags: ['html', 'parsing', 'web']
        });

        // 16進数カラーコード
        this.addPattern('hex_color', {
            pattern: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
            description: '16進数カラーコード',
            category: 'color',
            examples: {
                valid: ['#FF0000', '#f00', '#123abc'],
                invalid: ['FF0000', '#GG0000', '#12']
            },
            tags: ['color', 'hex', 'css']
        });

        // ユーザー名(英数字、アンダースコア、ハイフン)
        this.addPattern('username', {
            pattern: /^[a-zA-Z0-9_-]{3,20}$/,
            description: 'ユーザー名(英数字、_、-を使用、3-20文字)',
            category: 'validation',
            examples: {
                valid: ['user123', 'my_username', 'test-user'],
                invalid: ['us', 'verylongusernamethatistoolong', 'user@name']
            },
            tags: ['username', 'validation', 'user']
        });

        // 日本語(ひらがな、カタカナ、漢字)
        this.addPattern('japanese', {
            pattern: /^[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]+$/,
            description: '日本語文字(ひらがな、カタカナ、漢字)',
            category: 'language',
            examples: {
                valid: ['こんにちは', 'カタカナ', '日本語', 'ひらがなカタカナ漢字'],
                invalid: ['Hello', 'こんにちはWorld', '123']
            },
            tags: ['japanese', 'hiragana', 'katakana', 'kanji']
        });

        // JSON文字列
        this.addPattern('json', {
            pattern: /^[\],:{}\s]*$|^true$|^false$|^null$|^-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?$|^"(?:[^"\\]|\\.)*"$/,
            description: 'JSON形式の値',
            category: 'data',
            complexity: 'high',
            tags: ['json', 'data', 'parsing']
        });

        // Base64
        this.addPattern('base64', {
            pattern: /^[A-Za-z0-9+\/]*={0,2}$/,
            description: 'Base64エンコードされた文字列',
            category: 'encoding',
            examples: {
                valid: ['SGVsbG8=', 'dGVzdA==', 'YWJjZGVmZw'],
                invalid: ['Hello!', 'invalid@base64']
            },
            tags: ['base64', 'encoding', 'data']
        });
    }

    addPattern(name, config) {
        this.patterns.set(name, {
            ...config,
            name,
            createdAt: new Date(),
            usage: 0
        });

        // カテゴリ別インデックス
        if (config.category) {
            if (!this.categories.has(config.category)) {
                this.categories.set(config.category, new Set());
            }
            this.categories.get(config.category).add(name);
        }
    }

    getPattern(name) {
        const pattern = this.patterns.get(name);
        if (pattern) {
            pattern.usage++;
            return pattern;
        }
        return null;
    }

    searchPatterns(query, options = {}) {
        const results = [];
        const searchTerms = query.toLowerCase().split(' ');

        for (const [name, pattern] of this.patterns) {
            let score = 0;

            // 名前での一致
            if (name.toLowerCase().includes(query.toLowerCase())) {
                score += 10;
            }

            // 説明での一致
            if (pattern.description.toLowerCase().includes(query.toLowerCase())) {
                score += 5;
            }

            // タグでの一致
            if (pattern.tags) {
                pattern.tags.forEach(tag => {
                    if (searchTerms.some(term => tag.toLowerCase().includes(term))) {
                        score += 3;
                    }
                });
            }

            // カテゴリでの一致
            if (pattern.category && pattern.category.toLowerCase().includes(query.toLowerCase())) {
                score += 7;
            }

            // フィルタ適用
            if (options.category && pattern.category !== options.category) {
                continue;
            }

            if (options.complexity && pattern.complexity !== options.complexity) {
                continue;
            }

            if (score > 0) {
                results.push({ ...pattern, score });
            }
        }

        return results.sort((a, b) => b.score - a.score);
    }

    getPatternsByCategory(category) {
        const categoryPatterns = this.categories.get(category);
        if (!categoryPatterns) return [];

        return Array.from(categoryPatterns).map(name => this.patterns.get(name));
    }

    getPopularPatterns(limit = 10) {
        return Array.from(this.patterns.values())
            .sort((a, b) => b.usage - a.usage)
            .slice(0, limit);
    }

    getRecommendations(currentPattern) {
        const recommendations = [];

        // パターンの特徴を分析
        const features = this.analyzePatternFeatures(currentPattern);

        // 類似パターンを検索
        for (const [name, pattern] of this.patterns) {
            const similarity = this.calculateSimilarity(features, pattern);
            if (similarity > 0.3) {
                recommendations.push({
                    ...pattern,
                    similarity,
                    reason: this.generateRecommendationReason(features, pattern)
                });
            }
        }

        return recommendations
            .sort((a, b) => b.similarity - a.similarity)
            .slice(0, 5);
    }

    analyzePatternFeatures(pattern) {
        return {
            hasEmail: /@/.test(pattern),
            hasUrl: /https?/.test(pattern),
            hasNumbers: /\d/.test(pattern),
            hasSpecialChars: /[!@#$%^&*(),.?":{}|<>]/.test(pattern),
            hasWordBoundary: /\\b/.test(pattern),
            hasAnchors: /[\^$]/.test(pattern),
            hasQuantifiers: /[*+?{}]/.test(pattern),
            hasCharacterClass: /\[.*\]/.test(pattern),
            hasGroups: /\(.*\)/.test(pattern),
            length: pattern.length
        };
    }

    calculateSimilarity(features1, pattern2) {
        const features2 = this.analyzePatternFeatures(pattern2.pattern.toString());
        let similarity = 0;
        let totalFeatures = 0;

        for (const [key, value] of Object.entries(features1)) {
            if (key === 'length') continue;
            totalFeatures++;
            if (value === features2[key]) {
                similarity++;
            }
        }

        return totalFeatures > 0 ? similarity / totalFeatures : 0;
    }

    generateRecommendationReason(features, pattern) {
        const reasons = [];

        if (features.hasEmail && pattern.tags.includes('email')) {
            reasons.push('メールアドレス処理に関連');
        }
        if (features.hasUrl && pattern.tags.includes('url')) {
            reasons.push('URL処理に関連');
        }
        if (features.hasNumbers && pattern.tags.includes('number')) {
            reasons.push('数値処理に関連');
        }

        return reasons.length > 0 ? reasons.join(', ') : '類似パターン';
    }

    exportPatterns(format = 'json') {
        const data = {
            patterns: Array.from(this.patterns.entries()),
            categories: Array.from(this.categories.entries()),
            exportedAt: new Date().toISOString(),
            version: '1.0.0'
        };

        switch (format) {
            case 'json':
                return JSON.stringify(data, null, 2);
            case 'csv':
                return this.convertToCSV(data);
            default:
                return data;
        }
    }

    importPatterns(data) {
        if (typeof data === 'string') {
            data = JSON.parse(data);
        }

        if (data.patterns) {
            data.patterns.forEach(([name, pattern]) => {
                this.addPattern(name, pattern);
            });
        }

        return {
            imported: data.patterns ? data.patterns.length : 0,
            timestamp: new Date()
        };
    }

    generateCheatSheet() {
        const cheatSheet = {
            metacharacters: this.getMetaCharacterList(),
            quantifiers: this.getQuantifierList(),
            characterClasses: this.getCharacterClassList(),
            anchors: this.getAnchorList(),
            groups: this.getGroupList(),
            flags: this.getFlagList(),
            commonPatterns: this.getPopularPatterns(20)
        };

        return cheatSheet;
    }

    getMetaCharacterList() {
        return [
            { char: '.', description: '任意の一文字(改行以外)' },
            { char: '*', description: '直前の文字が0回以上' },
            { char: '+', description: '直前の文字が1回以上' },
            { char: '?', description: '直前の文字が0回または1回' },
            { char: '^', description: '行の開始' },
            { char: '$', description: '行の終了' },
            { char: '|', description: 'OR演算子' },
            { char: '\\', description: 'エスケープ文字' }
        ];
    }

    getQuantifierList() {
        return [
            { syntax: '{n}', description: 'ちょうどn回' },
            { syntax: '{n,}', description: 'n回以上' },
            { syntax: '{n,m}', description: 'n回からm回' },
            { syntax: '*?', description: '非貪欲な0回以上' },
            { syntax: '+?', description: '非貪欲な1回以上' },
            { syntax: '??', description: '非貪欲な0回または1回' }
        ];
    }

    getCharacterClassList() {
        return [
            { syntax: '[abc]', description: 'a、b、cのいずれか' },
            { syntax: '[^abc]', description: 'a、b、c以外' },
            { syntax: '[a-z]', description: 'a から z まで' },
            { syntax: '\\d', description: '数字 [0-9]' },
            { syntax: '\\D', description: '数字以外' },
            { syntax: '\\w', description: '単語文字 [a-zA-Z0-9_]' },
            { syntax: '\\W', description: '単語文字以外' },
            { syntax: '\\s', description: '空白文字' },
            { syntax: '\\S', description: '空白文字以外' }
        ];
    }

    getAnchorList() {
        return [
            { syntax: '^', description: '行の開始' },
            { syntax: '$', description: '行の終了' },
            { syntax: '\\b', description: '単語境界' },
            { syntax: '\\B', description: '単語境界以外' },
            { syntax: '\\A', description: '文字列の開始' },
            { syntax: '\\Z', description: '文字列の終了' }
        ];
    }

    getGroupList() {
        return [
            { syntax: '(pattern)', description: 'キャプチャグループ' },
            { syntax: '(?:pattern)', description: '非キャプチャグループ' },
            { syntax: '(?<name>pattern)', description: '名前付きグループ' },
            { syntax: '(?=pattern)', description: '先読みアサーション' },
            { syntax: '(?!pattern)', description: '否定先読み' },
            { syntax: '(?<=pattern)', description: '後読みアサーション' },
            { syntax: '(?<!pattern)', description: '否定後読み' }
        ];
    }

    getFlagList() {
        return [
            { flag: 'g', description: 'グローバル検索' },
            { flag: 'i', description: '大文字小文字を無視' },
            { flag: 'm', description: '複数行モード' },
            { flag: 's', description: 'dotallモード(.が改行にマッチ)' },
            { flag: 'u', description: 'Unicode対応' },
            { flag: 'y', description: 'stickyモード' }
        ];
    }
}

実用的な活用例

1. ログ解析システム

// ログファイル解析用の正規表現システム
class LogAnalyzer {
    constructor() {
        this.tester = new RegexTester();
        this.patterns = new Map();
        this.loadLogPatterns();
        this.results = [];
    }

    loadLogPatterns() {
        // Apache ログ
        this.patterns.set('apache_access', {
            pattern: /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\d+|-) "([^"]*)" "([^"]*)"$/,
            fields: ['ip', 'timestamp', 'method', 'url', 'protocol', 'status', 'size', 'referer', 'user_agent'],
            description: 'Apache アクセスログ'
        });

        // Nginx ログ
        this.patterns.set('nginx_access', {
            pattern: /^(\S+) - \S+ \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\d+) "([^"]*)" "([^"]*)" (\S+)$/,
            fields: ['ip', 'timestamp', 'method', 'url', 'protocol', 'status', 'size', 'referer', 'user_agent', 'response_time'],
            description: 'Nginx アクセスログ'
        });

        // エラーログ
        this.patterns.set('error_log', {
            pattern: /^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/,
            fields: ['timestamp', 'level', 'source', 'message'],
            description: '汎用エラーログ'
        });

        // JSON ログ
        this.patterns.set('json_log', {
            pattern: /^\{.*\}$/,
            fields: ['json_data'],
            description: 'JSON形式のログ',
            parser: 'json'
        });
    }

    analyzeLogFile(logContent, patternName = 'auto') {
        const lines = logContent.split('\n').filter(line => line.trim());
        const results = {
            totalLines: lines.length,
            parsedLines: 0,
            errors: [],
            statistics: new Map(),
            timeline: [],
            patterns: new Map()
        };

        let selectedPattern = null;

        if (patternName === 'auto') {
            selectedPattern = this.detectLogFormat(lines.slice(0, 10));
        } else {
            selectedPattern = this.patterns.get(patternName);
        }

        if (!selectedPattern) {
            results.errors.push('ログ形式を特定できませんでした');
            return results;
        }

        lines.forEach((line, index) => {
            try {
                const match = this.parseLogLine(line, selectedPattern);
                if (match) {
                    results.parsedLines++;
                    this.updateStatistics(match, results.statistics);
                    this.updateTimeline(match, results.timeline);
                } else {
                    results.errors.push({
                        line: index + 1,
                        content: line,
                        error: 'パターンにマッチしません'
                    });
                }
            } catch (error) {
                results.errors.push({
                    line: index + 1,
                    content: line,
                    error: error.message
                });
            }
        });

        results.successRate = (results.parsedLines / results.totalLines) * 100;
        return results;
    }

    detectLogFormat(sampleLines) {
        for (const [name, pattern] of this.patterns) {
            let matchCount = 0;

            sampleLines.forEach(line => {
                if (new RegExp(pattern.pattern).test(line)) {
                    matchCount++;
                }
            });

            if (matchCount / sampleLines.length > 0.8) {
                return pattern;
            }
        }

        return null;
    }

    parseLogLine(line, pattern) {
        if (pattern.parser === 'json') {
            try {
                return JSON.parse(line);
            } catch (error) {
                return null;
            }
        }

        const match = line.match(pattern.pattern);
        if (!match) return null;

        const result = { raw: line };
        pattern.fields.forEach((field, index) => {
            result[field] = match[index + 1] || '';
        });

        return result;
    }

    updateStatistics(logEntry, statistics) {
        // ステータスコード統計
        if (logEntry.status) {
            const statusKey = `status_${logEntry.status}`;
            statistics.set(statusKey, (statistics.get(statusKey) || 0) + 1);
        }

        // IPアドレス統計
        if (logEntry.ip) {
            const ipKey = `ip_${logEntry.ip}`;
            statistics.set(ipKey, (statistics.get(ipKey) || 0) + 1);
        }

        // User Agent統計
        if (logEntry.user_agent) {
            const uaKey = `ua_${this.categorizeUserAgent(logEntry.user_agent)}`;
            statistics.set(uaKey, (statistics.get(uaKey) || 0) + 1);
        }
    }

    categorizeUserAgent(userAgent) {
        if (/bot|crawler|spider/i.test(userAgent)) return 'bot';
        if (/mobile|android|iphone/i.test(userAgent)) return 'mobile';
        if (/chrome/i.test(userAgent)) return 'chrome';
        if (/firefox/i.test(userAgent)) return 'firefox';
        if (/safari/i.test(userAgent)) return 'safari';
        return 'other';
    }

    updateTimeline(logEntry, timeline) {
        if (logEntry.timestamp) {
            const time = this.parseTimestamp(logEntry.timestamp);
            if (time) {
                timeline.push({
                    timestamp: time,
                    ip: logEntry.ip,
                    status: logEntry.status,
                    url: logEntry.url,
                    size: parseInt(logEntry.size) || 0
                });
            }
        }
    }

    parseTimestamp(timestampStr) {
        // 一般的なログ形式のタイムスタンプをパース
        const formats = [
            /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, // 2023-12-25 10:30:45
            /^\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2}/, // 25/Dec/2023:10:30:45
            /^\w{3} \d{2} \d{2}:\d{2}:\d{2}/ // Dec 25 10:30:45
        ];

        for (const format of formats) {
            if (format.test(timestampStr)) {
                return new Date(timestampStr);
            }
        }

        return null;
    }

    generateReport(analysisResult) {
        return {
            summary: {
                totalLines: analysisResult.totalLines,
                parsedLines: analysisResult.parsedLines,
                errorLines: analysisResult.errors.length,
                successRate: `${analysisResult.successRate.toFixed(2)}%`
            },
            topIPs: this.getTopEntries(analysisResult.statistics, 'ip_', 10),
            statusCodes: this.getTopEntries(analysisResult.statistics, 'status_', 20),
            userAgents: this.getTopEntries(analysisResult.statistics, 'ua_', 10),
            timelineAnalysis: this.analyzeTimeline(analysisResult.timeline),
            errors: analysisResult.errors.slice(0, 100), // 最初の100エラー
            recommendations: this.generateLogRecommendations(analysisResult)
        };
    }

    getTopEntries(statistics, prefix, limit) {
        return Array.from(statistics.entries())
            .filter(([key]) => key.startsWith(prefix))
            .map(([key, count]) => ({
                item: key.replace(prefix, ''),
                count
            }))
            .sort((a, b) => b.count - a.count)
            .slice(0, limit);
    }

    analyzeTimeline(timeline) {
        if (timeline.length === 0) return {};

        const sortedTimeline = timeline.sort((a, b) => a.timestamp - b.timestamp);
        const startTime = sortedTimeline[0].timestamp;
        const endTime = sortedTimeline[sortedTimeline.length - 1].timestamp;
        const duration = endTime - startTime;

        // 時間別リクエスト数
        const hourlyStats = new Map();
        timeline.forEach(entry => {
            const hour = entry.timestamp.getHours();
            hourlyStats.set(hour, (hourlyStats.get(hour) || 0) + 1);
        });

        return {
            startTime,
            endTime,
            duration: `${Math.round(duration / (1000 * 60))} minutes`,
            requestsPerMinute: timeline.length / (duration / (1000 * 60)),
            peakHour: this.findPeakHour(hourlyStats),
            hourlyDistribution: Object.fromEntries(hourlyStats)
        };
    }

    findPeakHour(hourlyStats) {
        let maxHour = 0;
        let maxCount = 0;

        for (const [hour, count] of hourlyStats) {
            if (count > maxCount) {
                maxCount = count;
                maxHour = hour;
            }
        }

        return { hour: maxHour, requests: maxCount };
    }

    generateLogRecommendations(analysisResult) {
        const recommendations = [];

        if (analysisResult.successRate < 90) {
            recommendations.push({
                type: 'parsing',
                message: 'ログの解析成功率が低いです。ログ形式の見直しを検討してください',
                priority: 'high'
            });
        }

        const errorRate = (analysisResult.errors.length / analysisResult.totalLines) * 100;
        if (errorRate > 10) {
            recommendations.push({
                type: 'quality',
                message: 'エラー率が高いです。ログ出力の品質向上が必要です',
                priority: 'medium'
            });
        }

        return recommendations;
    }
}

安全性和隐私保护

所有处理都在浏览器内完成,数据不会发送到外部。您可以安全地使用个人信息或机密数据。

故障排除

常见问题

  • 无法运行: 清除浏览器缓存并重新加载
  • 处理速度慢: 检查文件大小(建议20MB以下)
  • 结果与预期不符: 确认输入格式和设置

如果问题仍未解决,请将浏览器更新到最新版本或尝试其他浏览器。

まとめ

正規表現テスターの実装により、以下の重要な利点が得られます:

  1. 開発効率の向上: 複雑な文字列処理パターンの迅速な検証とデバッグ
  2. 品質保証: リアルタイムバリデーションによる正確なパターンマッチング
  3. 学習支援: 可視化機能による正規表現の理解促進
  4. 性能最適化: バックトラッキング分析による効率的なパターン設計

適切な正規表現テスターの活用は、テキスト処理を含むあらゆる開発プロジェクトにおいて必須のスキルです。本ガイドの実装パターンを参考に、プロジェクトに最適な正規表現ソリューションを構築してください。

カテゴリ別ツール

他のツールもご覧ください:

関連ツール

最后更新: