[PHP]preg_match関数の使い方|正規表現マッチングの基本から応用まで完全マスター

PHP

PHPで文字列のパターンマッチングを行う際、最も頻繁に使用される関数がpreg_matchです。メールアドレスの検証、URLの抽出、データのバリデーションなど、Webアプリケーション開発において必須のスキルです。

この記事では、preg_match関数の基本的な使い方から、実践的なテクニック、パフォーマンス最適化まで詳しく解説します。

preg_match関数とは?

preg_match()は、正規表現パターンを使って文字列をマッチングし、最初にマッチした部分を返すPHP関数です。

基本構文

int|false preg_match(
    string $pattern,
    string $subject,
    array &$matches = null,
    int $flags = 0,
    int $offset = 0
)
  • $pattern: 正規表現パターン(区切り文字で囲む)
  • $subject: 検索対象の文字列
  • $matches: マッチした結果を格納する配列(参照渡し)
  • $flags: オプションフラグ(PREG_OFFSET_CAPTURE等)
  • $offset: 検索開始位置(バイト単位)
  • 戻り値: マッチ回数(0または1)、エラー時はfalse

基本的な使い方

シンプルなマッチング

<?php
// 基本的なパターンマッチ
$text = "Hello, World!";
$pattern = "/World/";

if (preg_match($pattern, $text)) {
    echo "マッチしました!\n";
} else {
    echo "マッチしませんでした。\n";
}

// 結果: マッチしました!
?>

マッチした部分の取得

<?php
$text = "私のメールは test@example.com です";
$pattern = "/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/";

if (preg_match($pattern, $text, $matches)) {
    echo "見つかったメールアドレス: " . $matches[0] . "\n";
    // 結果: test@example.com
}
?>

グループキャプチャ

<?php
$text = "生年月日: 1990-05-15";
$pattern = "/(\d{4})-(\d{2})-(\d{2})/";

if (preg_match($pattern, $text, $matches)) {
    echo "完全マッチ: " . $matches[0] . "\n";  // 1990-05-15
    echo "年: " . $matches[1] . "\n";           // 1990
    echo "月: " . $matches[2] . "\n";           // 05
    echo "日: " . $matches[3] . "\n";           // 15
}
?>

名前付きキャプチャグループ

<?php
$text = "価格: ¥1,500";
$pattern = "/¥(?<price>[\d,]+)/";

if (preg_match($pattern, $text, $matches)) {
    echo "価格: " . $matches['price'] . "\n";  // 1,500
    // または $matches[1] でもアクセス可能
}
?>

実践的な活用例

1. 包括的なバリデーションクラス

<?php
class Validator {

    /**
     * メールアドレスの検証
     */
    public static function isValidEmail($email) {
        $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
        return preg_match($pattern, $email) === 1;
    }

    /**
     * URLの検証
     */
    public static function isValidUrl($url) {
        $pattern = '/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/';
        return preg_match($pattern, $url) === 1;
    }

    /**
     * 電話番号の検証(日本)
     */
    public static function isValidPhoneJP($phone) {
        // 固定電話または携帯電話
        $patterns = [
            '/^0\d{1,4}-\d{1,4}-\d{4}$/',     // ハイフン付き
            '/^0\d{9,10}$/'                    // ハイフンなし
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $phone)) {
                return true;
            }
        }

        return false;
    }

    /**
     * パスワード強度の検証
     */
    public static function isStrongPassword($password, $minLength = 8) {
        // 最低8文字、大文字、小文字、数字、特殊文字を含む
        $checks = [
            'length' => strlen($password) >= $minLength,
            'lowercase' => preg_match('/[a-z]/', $password) === 1,
            'uppercase' => preg_match('/[A-Z]/', $password) === 1,
            'number' => preg_match('/[0-9]/', $password) === 1,
            'special' => preg_match('/[^a-zA-Z0-9]/', $password) === 1
        ];

        return array_filter($checks) === $checks;
    }

    /**
     * 郵便番号の検証(日本)
     */
    public static function isValidPostalCodeJP($code) {
        $pattern = '/^\d{3}-?\d{4}$/';
        return preg_match($pattern, $code) === 1;
    }

    /**
     * クレジットカード番号の検証(基本形式)
     */
    public static function isValidCreditCard($number) {
        // スペースやハイフンを削除
        $cleaned = preg_replace('/[\s-]/', '', $number);

        // 13-19桁の数字
        if (preg_match('/^\d{13,19}$/', $cleaned) !== 1) {
            return false;
        }

        // Luhnアルゴリズムで検証
        return self::luhnCheck($cleaned);
    }

    private static function luhnCheck($number) {
        $sum = 0;
        $length = strlen($number);

        for ($i = 0; $i < $length; $i++) {
            $digit = (int)$number[$length - $i - 1];

            if ($i % 2 === 1) {
                $digit *= 2;
                if ($digit > 9) {
                    $digit -= 9;
                }
            }

            $sum += $digit;
        }

        return $sum % 10 === 0;
    }

    /**
     * IPアドレスの検証(IPv4)
     */
    public static function isValidIPv4($ip) {
        $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]?)$/';
        return preg_match($pattern, $ip) === 1;
    }

    /**
     * 日付形式の検証
     */
    public static function isValidDate($date, $format = 'Y-m-d') {
        $patterns = [
            'Y-m-d' => '/^\d{4}-\d{2}-\d{2}$/',
            'Y/m/d' => '/^\d{4}\/\d{2}\/\d{2}$/',
            'd-m-Y' => '/^\d{2}-\d{2}-\d{4}$/',
            'd/m/Y' => '/^\d{2}\/\d{2}\/\d{4}$/'
        ];

        if (!isset($patterns[$format])) {
            return false;
        }

        if (preg_match($patterns[$format], $date) !== 1) {
            return false;
        }

        // 実際に有効な日付かチェック
        $dateObj = \DateTime::createFromFormat($format, $date);
        return $dateObj && $dateObj->format($format) === $date;
    }
}

// 使用例
echo "=== バリデーションデモ ===\n\n";

// メールアドレス
$emails = ['test@example.com', 'invalid-email', 'user+tag@domain.co.jp'];
foreach ($emails as $email) {
    $valid = Validator::isValidEmail($email) ? '✓' : '✗';
    echo "{$valid} {$email}\n";
}

echo "\n";

// パスワード強度
$passwords = ['weak', 'Strong123!', 'NoSpecial123', 'short!A1'];
foreach ($passwords as $password) {
    $valid = Validator::isStrongPassword($password) ? '✓' : '✗';
    echo "{$valid} {$password}\n";
}

echo "\n";

// 電話番号
$phones = ['090-1234-5678', '0312345678', '03-1234-5678', '123-4567'];
foreach ($phones as $phone) {
    $valid = Validator::isValidPhoneJP($phone) ? '✓' : '✗';
    echo "{$valid} {$phone}\n";
}
?>

2. データ抽出システム

<?php
class DataExtractor {

    /**
     * テキストからすべてのメールアドレスを抽出
     */
    public static function extractEmails($text) {
        $pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';
        preg_match_all($pattern, $text, $matches);
        return array_unique($matches[0]);
    }

    /**
     * テキストからすべてのURLを抽出
     */
    public static function extractUrls($text) {
        $pattern = '/https?:\/\/[^\s]+/';
        preg_match_all($pattern, $text, $matches);
        return array_unique($matches[0]);
    }

    /**
     * HTMLタグから属性値を抽出
     */
    public static function extractAttribute($html, $tag, $attribute) {
        $pattern = "/<{$tag}[^>]*{$attribute}=[\"']([^\"']*)[\"'][^>]*>/i";

        if (preg_match($pattern, $html, $matches)) {
            return $matches[1];
        }

        return null;
    }

    /**
     * テキストから日付を抽出
     */
    public static function extractDates($text) {
        $patterns = [
            'YYYY-MM-DD' => '/\d{4}-\d{2}-\d{2}/',
            'DD/MM/YYYY' => '/\d{2}\/\d{2}\/\d{4}/',
            'Month DD, YYYY' => '/[A-Za-z]+\s+\d{1,2},\s+\d{4}/'
        ];

        $dates = [];
        foreach ($patterns as $format => $pattern) {
            preg_match_all($pattern, $text, $matches);
            foreach ($matches[0] as $date) {
                $dates[] = ['format' => $format, 'date' => $date];
            }
        }

        return $dates;
    }

    /**
     * テキストから価格情報を抽出
     */
    public static function extractPrices($text) {
        $patterns = [
            'yen' => '/¥([\d,]+)/',
            'dollar' => '/\$([\d,]+(?:\.\d{2})?)/',
            'euro' => '/€([\d,]+(?:\.\d{2})?)/'
        ];

        $prices = [];
        foreach ($patterns as $currency => $pattern) {
            if (preg_match_all($pattern, $text, $matches)) {
                foreach ($matches[1] as $price) {
                    $prices[] = [
                        'currency' => $currency,
                        'amount' => str_replace(',', '', $price)
                    ];
                }
            }
        }

        return $prices;
    }

    /**
     * HTMLからメタタグ情報を抽出
     */
    public static function extractMetaTags($html) {
        $meta = [];

        // title
        if (preg_match('/<title>([^<]+)<\/title>/i', $html, $matches)) {
            $meta['title'] = $matches[1];
        }

        // description
        if (preg_match('/<meta\s+name=["\']description["\']\s+content=["\']([^"\']+)["\']/i', $html, $matches)) {
            $meta['description'] = $matches[1];
        }

        // keywords
        if (preg_match('/<meta\s+name=["\']keywords["\']\s+content=["\']([^"\']+)["\']/i', $html, $matches)) {
            $meta['keywords'] = explode(',', $matches[1]);
        }

        return $meta;
    }
}

// デモ実行
echo "=== データ抽出デモ ===\n\n";

$sampleText = "連絡先: test@example.com, support@company.co.jp
ウェブサイト: https://example.com, http://test.org
価格: ¥1,500、$29.99、€15.50
日付: 2024-01-15, 2023/12/31";

echo "【メールアドレス】\n";
$emails = DataExtractor::extractEmails($sampleText);
foreach ($emails as $email) {
    echo "  - {$email}\n";
}

echo "\n【URL】\n";
$urls = DataExtractor::extractUrls($sampleText);
foreach ($urls as $url) {
    echo "  - {$url}\n";
}

echo "\n【価格】\n";
$prices = DataExtractor::extractPrices($sampleText);
foreach ($prices as $price) {
    echo "  - {$price['currency']}: {$price['amount']}\n";
}

echo "\n【日付】\n";
$dates = DataExtractor::extractDates($sampleText);
foreach ($dates as $date) {
    echo "  - {$date['date']} ({$date['format']})\n";
}
?>

3. セキュアな入力サニタイゼーション

<?php
class SecureInput {

    /**
     * SQLインジェクション検出
     */
    public static function detectSQLInjection($input) {
        $dangerous_patterns = [
            '/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)\b)/i',
            '/(UNION\s+SELECT)/i',
            '/(-{2}|\/\*|\*\/|;)/',  // コメントやセミコロン
            '/(\bOR\b|\bAND\b)\s+[\'"]\d+[\'"]?\s*=\s*[\'"]\d+[\'"]?/i'  // OR 1=1 等
        ];

        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }

        return false;
    }

    /**
     * XSS攻撃パターン検出
     */
    public static function detectXSS($input) {
        $xss_patterns = [
            '/<script[^>]*>.*?<\/script>/is',
            '/javascript:/i',
            '/on\w+\s*=/i',  // onclick, onload等
            '/<iframe/i',
            '/<object/i',
            '/<embed/i'
        ];

        foreach ($xss_patterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }

        return false;
    }

    /**
     * パストラバーサル検出
     */
    public static function detectPathTraversal($input) {
        $patterns = [
            '/\.\.\//',      // ../
            '/\.\\.\\\\/',   // ..\
            '/%2e%2e/',      // URLエンコード
            '/\.\.//'        // ..
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 安全な文字列かチェック(アルファベット・数字・基本記号のみ)
     */
    public static function isSafeString($input) {
        return preg_match('/^[a-zA-Z0-9\s\-_.@]+$/', $input) === 1;
    }

    /**
     * ファイル名の検証
     */
    public static function isValidFilename($filename) {
        // 許可する文字のみ
        if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename) !== 1) {
            return false;
        }

        // 危険な拡張子をチェック
        $dangerous_extensions = ['php', 'exe', 'bat', 'sh', 'cmd'];
        $pattern = '/\.(' . implode('|', $dangerous_extensions) . ')$/i';

        if (preg_match($pattern, $filename)) {
            return false;
        }

        return true;
    }

    /**
     * 包括的なセキュリティチェック
     */
    public static function securityCheck($input, $context = 'general') {
        $results = [
            'safe' => true,
            'threats' => []
        ];

        if (self::detectSQLInjection($input)) {
            $results['safe'] = false;
            $results['threats'][] = 'SQL Injection';
        }

        if (self::detectXSS($input)) {
            $results['safe'] = false;
            $results['threats'][] = 'XSS';
        }

        if ($context === 'filepath' && self::detectPathTraversal($input)) {
            $results['safe'] = false;
            $results['threats'][] = 'Path Traversal';
        }

        return $results;
    }
}

// デモ実行
echo "=== セキュリティチェックデモ ===\n\n";

$testInputs = [
    'normal_text' => 'Hello World 123',
    'sql_injection' => "admin' OR '1'='1",
    'xss_attack' => '<script>alert("XSS")</script>',
    'path_traversal' => '../../../etc/passwd',
    'safe_filename' => 'document_2024.pdf',
    'unsafe_filename' => 'malware.php.jpg'
];

foreach ($testInputs as $label => $input) {
    echo "【{$label}】\n";
    echo "入力: {$input}\n";

    $check = SecureInput::securityCheck($input);

    if ($check['safe']) {
        echo "結果: ✓ 安全\n";
    } else {
        echo "結果: ✗ 危険\n";
        echo "検出された脅威: " . implode(', ', $check['threats']) . "\n";
    }

    echo "\n";
}
?>

パフォーマンス最適化

1. パターンのコンパイルとキャッシュ

<?php
class OptimizedRegex {
    private static $compiledPatterns = [];

    public static function match($pattern, $subject, &$matches = null) {
        // パターンの事前コンパイル(PCREキャッシュを活用)
        if (!isset(self::$compiledPatterns[$pattern])) {
            // パターンが有効かチェック
            if (@preg_match($pattern, '') === false) {
                throw new Exception("無効な正規表現パターン: {$pattern}");
            }
            self::$compiledPatterns[$pattern] = true;
        }

        return preg_match($pattern, $subject, $matches);
    }

    public static function benchmark($pattern, $subject, $iterations = 10000) {
        $start = microtime(true);

        for ($i = 0; $i < $iterations; $i++) {
            preg_match($pattern, $subject);
        }

        $time = microtime(true) - $start;

        return [
            'iterations' => $iterations,
            'total_time' => $time,
            'average_time' => $time / $iterations,
            'operations_per_second' => $iterations / $time
        ];
    }
}

// パフォーマンステスト
echo "=== パフォーマンスベンチマーク ===\n\n";

$patterns = [
    'simple' => '/test/',
    'complex' => '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
    'very_complex' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/'
];

$subject = 'Test@Example123!';

foreach ($patterns as $name => $pattern) {
    $result = OptimizedRegex::benchmark($pattern, $subject);

    echo "{$name}パターン:\n";
    echo "  総時間: " . round($result['total_time'] * 1000, 2) . "ms\n";
    echo "  平均: " . round($result['average_time'] * 1000000, 2) . "μs\n";
    echo "  速度: " . number_format($result['operations_per_second'], 0) . " ops/sec\n\n";
}
?>

まとめ

preg_match関数は、PHPの正規表現処理における最も基本的で重要な関数です。

✨ 主要な使い方

  • バリデーション: 入力データの形式チェック
  • データ抽出: テキストからの情報取得
  • セキュリティ: 危険なパターンの検出
  • パース処理: 構造化データの解析

🚀 ベストプラクティス

  • エラーハンドリングを必ず実装
  • パフォーマンスを考慮したパターン設計
  • セキュリティを意識した検証
  • 適切なフラグとオプションの使用

この記事の実装例を参考に、安全で効率的な正規表現処理を実装してください!

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