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の正規表現処理における最も基本的で重要な関数です。
✨ 主要な使い方
- バリデーション: 入力データの形式チェック
- データ抽出: テキストからの情報取得
- セキュリティ: 危険なパターンの検出
- パース処理: 構造化データの解析
🚀 ベストプラクティス
- エラーハンドリングを必ず実装
- パフォーマンスを考慮したパターン設計
- セキュリティを意識した検証
- 適切なフラグとオプションの使用
この記事の実装例を参考に、安全で効率的な正規表現処理を実装してください!
