6.3 正则表达式入门

正则表达式(Regular Expression,简称Regex)是一种强大的文本模式匹配工具,它使用特定的模式来描述、匹配、查找和替换字符串。在PHP开发中,正则表达式经常用于表单验证、数据提取、文本处理等场景。

什么是正则表达式?

正则表达式是由普通字符和特殊字符组成的模式,用于描述字符串的匹配规则。它可以帮我们:

  • 验证格式:检查输入是否符合特定格式(邮箱、手机号、身份证等)
  • 提取数据:从复杂文本中提取有用信息
  • 替换文本:批量替换符合特定模式的文本
  • 分割字符串:按照复杂规则分割文本

一个简单的例子

<?php
// 检查字符串是否包含手机号
$text = "联系电话:13812345678";
$pattern = '/1[3-9]\d{9}/';  // 手机号正则表达式

if (preg_match($pattern, $text)) {
    echo "文本包含手机号\n";
} else {
    echo "文本不包含手机号\n";
}
?>

正则表达式基础语法

定界符

正则表达式通常用斜杠(/)或其他字符作为定界符:

<?php
// 使用斜杠作为定界符
$pattern1 = '/hello/';

// 使用其他字符作为定界符(当模式中包含斜杠时很有用)
$pattern2 = '#http://example\.com#';
$pattern3 = '~^https?://~';
?>

字符类和元字符

1. 基本字符类

<?php
// [abc] - 匹配 a、b 或 c 中的任意一个字符
$pattern = '/[abc]/';

// [^abc] - 匹配除 a、b、c 之外的任意字符
$pattern = '/[^abc]/';

// [a-z] - 匹配任意小写字母
$pattern = '/[a-z]/';

// [A-Z] - 匹配任意大写字母
$pattern = '/[A-Z]/';

// [0-9] - 匹配任意数字
$pattern = '/[0-9]/';

// [a-zA-Z0-9] - 匹配任意字母或数字
$pattern = '/[a-zA-Z0-9]/';

// 示例:检查是否包含字母
$text = "Hello123";
if (preg_match('/[a-zA-Z]/', $text)) {
    echo "包含字母\n";
}
?>

2. 预定义字符类

<?php
// \d - 匹配任意数字,等同于 [0-9]
$pattern = '/\d+/';

// \D - 匹配任意非数字字符,等同于 [^0-9]
$pattern = '/\D+/';

// \w - 匹配任意单词字符(字母、数字、下划线),等同于 [a-zA-Z0-9_]
$pattern = '/\w+/';

// \W - 匹配任意非单词字符,等同于 [^a-zA-Z0-9_]
$pattern = '/\W+/';

// \s - 匹配任意空白字符(空格、制表符、换行符等)
$pattern = '/\s+/';

// \S - 匹配任意非空白字符
$pattern = '/\S+/';

// 示例:验证密码格式(至少8位,包含字母和数字)
function validatePassword($password) {
    if (strlen($password) < 8) {
        return "密码长度至少8位";
    }

    if (!preg_match('/\d/', $password)) {
        return "密码必须包含数字";
    }

    if (!preg_match('/[a-zA-Z]/', $password)) {
        return "密码必须包含字母";
    }

    return "密码格式正确";
}

echo validatePassword("password123") . "\n";  // 密码格式正确
echo validatePassword("12345678") . "\n";      // 密码必须包含字母
echo validatePassword("password") . "\n";      // 密码必须包含数字
?>

3. 量词

<?php
// * - 匹配0次或多次
$pattern = '/a*/';  // 匹配0个或多个a

// + - 匹配1次或多次
$pattern = '/a+/';  // 匹配1个或多个a

// ? - 匹配0次或1次
$pattern = '/colou?r/';  // 匹配 color 或 colour

// {n} - 匹配恰好n次
$pattern = '/\d{3}/';  // 匹配恰好3个数字

// {n,} - 匹配至少n次
$pattern = '/\d{3,}/';  // 匹配至少3个数字

// {n,m} - 匹配n到m次
$pattern = '/\d{3,5}/';  // 匹配3到5个数字

// 示例:验证手机号格式
function validatePhone($phone) {
    // 移除所有非数字字符
    $phone = preg_replace('/\D/', '', $phone);

    // 验证11位数字,以1开头
    $pattern = '/^1[3-9]\d{9}$/';

    return preg_match($pattern, $phone) ? "手机号格式正确" : "手机号格式错误";
}

echo validatePhone("13812345678") . "\n";  // 手机号格式正确
echo validatePhone("188-1234-5678") . "\n";  // 手机号格式正确
echo validatePhone("12345678901") . "\n";   // 手机号格式错误
?>

4. 位置锚点

<?php
// ^ - 匹配字符串开头
$pattern = '/^hello/';  // 匹配以hello开头的字符串

// $ - 匹配字符串结尾
$pattern = '/world$/';  // 匹配以world结尾的字符串

// \b - 匹配单词边界
$pattern = '/\bword\b/';  // 匹配独立的单词word

// \B - 匹配非单词边界
$pattern = '/\Bword\B/';  // 匹配在单词内部的word

// 示例:验证用户名格式(3-20个字符,字母数字下划线,不以数字开头)
function validateUsername($username) {
    $pattern = '/^[a-zA-Z_]\w{2,19}$/';
    return preg_match($pattern, $username) ? "用户名格式正确" : "用户名格式错误";
}

echo validateUsername("user123") . "\n";      // 用户名格式正确
echo validateUsername("_username") . "\n";    // 用户名格式正确
echo validateUsername("123user") . "\n";      // 用户名格式错误
echo validateUsername("ab") . "\n";          // 用户名格式错误
?>

5. 分组和选择

<?php
// () - 分组
$pattern = '/(ab)+/';  // 匹配1个或多个ab

// | - 或条件
$pattern = '/cat|dog/';  // 匹配cat或dog

// 示例:提取URL中的域名
function extractDomain($url) {
    $pattern = '/^(https?:\/\/)?([^\/]+)/i';
    if (preg_match($pattern, $url, $matches)) {
        return $matches[2];
    }
    return null;
}

echo extractDomain("https://www.example.com/path") . "\n";  // www.example.com
echo extractDomain("http://example.org") . "\n";           // example.org
echo extractDomain("example.net") . "\n";                  // example.net

// 示例:验证邮箱格式
function validateEmail($email) {
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    return preg_match($pattern, $email) ? "邮箱格式正确" : "邮箱格式错误";
}

echo validateEmail("user@example.com") . "\n";     // 邮箱格式正确
echo validateEmail("user.name+tag@domain.co.uk") . "\n";  // 邮箱格式正确
echo validateEmail("invalid.email") . "\n";        // 邮箱格式错误
?>

PHP正则表达式函数

preg_match() - 执行正则表达式匹配

<?php
// 基本匹配
$text = "The quick brown fox jumps over the lazy dog";
$pattern = '/fox/';

if (preg_match($pattern, $text)) {
    echo "找到匹配\n";
}

// 捕获分组
$text = "联系电话:138-1234-5678";
$pattern = '/(\d{3})-(\d{4})-(\d{4})/';

if (preg_match($pattern, $text, $matches)) {
    echo "完整匹配: " . $matches[0] . "\n";
    echo "区号: " . $matches[1] . "\n";
    echo "前四位: " . $matches[2] . "\n";
    echo "后四位: " . $matches[3] . "\n";
}

// 带偏移量的匹配
$text = "123abc456def789";
$pattern = '/\d+/';
$count = 0;

// 从第5个字符开始匹配
preg_match($pattern, $text, $matches, 0, 5);
echo "从第5个字符开始的匹配: " . $matches[0] . "\n";  // 456

// 统计匹配次数
$text = "apple banana apple orange apple";
preg_match_all('/apple/', $text, $matches);
echo "'apple'出现次数: " . count($matches[0]) . "\n";  // 3
?>

preg_match_all() - 执行全局正则表达式匹配

<?php
// 提取所有邮箱地址
$text = "联系方式:admin@example.com 或 support@company.org,sales@business.net";
$pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';

preg_match_all($pattern, $text, $matches);
echo "找到的邮箱地址:\n";
foreach ($matches[0] as $email) {
    echo "- {$email}\n";
}

// 提取所有HTML标签
$html = "<div class='container'><p>段落内容</p><span>文本</span></div>";
$pattern = '/<[^>]+>/';

preg_match_all($pattern, $html, $matches);
echo "HTML标签:\n";
foreach ($matches[0] as $tag) {
    echo "- {$tag}\n";
}

// 提取价格信息
$text = "价格:¥299.99,折扣价:¥199.00,VIP价:¥99.50";
$pattern = '/¥(\d+\.\d{2})/';

preg_match_all($pattern, $text, $matches);
echo "价格信息:\n";
foreach ($matches[1] as $price) {
    echo "- ¥{$price}\n";
}
?>

preg_replace() - 执行正则表达式替换

<?php
// 基本替换
$text = "我的手机号是13812345678,请致电联系";
$pattern = '/1[3-9]\d{9}/';
$replacement = '[手机号已隐藏]';

$protected = preg_replace($pattern, $replacement, $text);
echo "保护后: {$protected}\n";

// 使用回调函数进行替换
$text = "苹果的价格是5元,香蕉的价格是3元,橙子的价格是8元";
$pattern = '/(\d+)元/';

$converted = preg_replace_callback($pattern, function($matches) {
    $price = $matches[1];
    return '¥' . $price . '元';
}, $text);

echo "价格转换后: {$converted}\n";

// 批量替换
$text = "Hello <world>! This is a <test>.";
$search = ['/<[^>]*>/', '/\s+/'];
$replace = ['', ' '];

$cleaned = preg_replace($search, $replace, $text);
echo "清理后: '" . trim($cleaned) . "'\n";

// 限制替换次数
$text = "apple banana apple orange apple";
$pattern = '/apple/';
$replacement = 'fruit';

$limited = preg_replace($pattern, $replacement, $text, 2);
echo "限制替换2次: {$limited}\n";  // fruit banana fruit orange apple
?>

preg_split() - 用正则表达式分割字符串

<?php
// 按多种空白字符分割
$text = "Hello   World\tThis\nis a  test";
$words = preg_split('/\s+/', $text);

print_r($words);

// 按标点符号分割句子
$text = "Hello, world! How are you? I'm fine. Thanks!";
$sentences = preg_split('/[.!?]+/', $text, -1, PREG_SPLIT_NO_EMPTY);

echo "句子:\n";
foreach ($sentences as $sentence) {
    echo "- '" . trim($sentence) . "'\n";
}

// 提取单词(过滤标点符号)
$text = "Hello, world! How are you today?";
$words = preg_split('/[\W\s]+/', $text, -1, PREG_SPLIT_NO_EMPTY);

echo "单词:\n";
foreach ($words as $word) {
    echo "- {$word}\n";
}

// 限制分割数量
$text = "one,two,three,four,five";
$parts = preg_split('/,/', $text, 3);

echo "分割前3部分:\n";
print_r($parts);
?>

preg_grep() - 返回匹配模式的数组条目

<?php
// 筛选包含数字的字符串
$strings = ["hello", "world123", "test", "456", "php2024", "abc"];
$pattern = '/\d+/';

$with_numbers = preg_grep($pattern, $strings);
print_r($with_numbers);

// 筛选有效的邮箱
$emails = [
    "user@example.com",
    "invalid.email",
    "test@domain.org",
    "not-an-email",
    "admin@company.net"
];
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';

$valid_emails = preg_grep($pattern, $emails);
echo "有效邮箱:\n";
foreach ($valid_emails as $email) {
    echo "- {$email}\n";
}

// 反向匹配(PREG_GREP_INVERT)
$invalid_emails = preg_grep($pattern, $emails, PREG_GREP_INVERT);
echo "无效邮箱:\n";
foreach ($invalid_emails as $email) {
    echo "- {$email}\n";
}
?>

常用正则表达式模式

邮箱验证

<?php
function validateEmailDetailed($email) {
    // 基本邮箱格式
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';

    if (!preg_match($pattern, $email)) {
        return [
            'valid' => false,
            'error' => '邮箱格式不正确'
        ];
    }

    // 检查域名长度
    if (strlen($email) > 254) {
        return [
            'valid' => false,
            'error' => '邮箱地址过长'
        ];
    }

    // 检查用户名长度
    $local = explode('@', $email)[0];
    if (strlen($local) > 64) {
        return [
            'valid' => false,
            'error' => '用户名部分过长'
        ];
    }

    return [
        'valid' => true,
        'message' => '邮箱格式正确'
    ];
}

$emails = [
    "user@example.com",
    "very.long.username.123@example-domain.com",
    "invalid.email",
    "user@localhost",
    "a" . str_repeat('a', 60) . "@example.com"
];

foreach ($emails as $email) {
    $result = validateEmailDetailed($email);
    echo "{$email}: " . ($result['valid'] ? $result['message'] : $result['error']) . "\n";
}
?>

手机号验证(中国大陆)

<?php
function validateChinesePhone($phone) {
    // 清理输入,只保留数字
    $phone = preg_replace('/\D/', '', $phone);

    // 中国手机号规则:1开头,第二位3-9,总共11位数字
    $pattern = '/^1[3-9]\d{9}$/';

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

    // 检查特殊号段(可选)
    $special_prefixes = ['170', '171', '172', '174', '175', '176', '178'];
    $prefix = substr($phone, 0, 3);

    if (in_array($prefix, $special_prefixes)) {
        // 虚拟运营商号段可能需要特殊处理
        return 'virtual';
    }

    return 'regular';
}

$phones = [
    "13812345678",
    "188-1234-5678",
    "+86 139 1234 5678",
    "17012345678",  // 虚拟运营商
    "12345678901",  // 无效
    "1381234567"    // 位数不对
];

foreach ($phones as $phone) {
    $result = validateChinesePhone($phone);
    switch ($result) {
        case 'regular':
            echo "{$phone}: 普通运营商号段\n";
            break;
        case 'virtual':
            echo "{$phone}: 虚拟运营商号段\n";
            break;
        case false:
            echo "{$phone}: 无效的手机号\n";
            break;
    }
}
?>

身份证号码验证

<?php
function validateChineseID($id) {
    // 18位身份证正则
    $pattern18 = '/^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/';

    // 15位身份证正则
    $pattern15 = '/^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/';

    if (preg_match($pattern18, $id)) {
        return validateIDChecksum($id) ? '18位有效' : '18位校验码错误';
    } elseif (preg_match($pattern15, $id)) {
        return '15位有效';
    }

    return '无效的身份证号';
}

function validateIDChecksum($id) {
    if (strlen($id) !== 18) {
        return false;
    }

    // 权重系数
    $weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
    // 校验码对应值
    $checksums = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];

    $sum = 0;
    for ($i = 0; $i < 17; $i++) {
        $sum += intval($id[$i]) * $weights[$i];
    }

    $index = $sum % 11;
    return strtoupper($id[17]) === $checksums[$index];
}

$ids = [
    "110101199001011234",  // 18位示例
    "110101900101123",      // 15位示例
    "123456789012345678",  // 无效
    "11010119900101123X"   // 18位,校验码X
];

foreach ($ids as $id) {
    echo "{$id}: " . validateChineseID($id) . "\n";
}
?>

URL验证和提取

<?php
function parseURL($url) {
    $pattern = '/^(https?):\/\/([^:\/\s]+)(?::(\d+))?(\/[^\s?#]*)?(?:\?([^#\s]*))?(#.*)?$/i';

    if (preg_match($pattern, $url, $matches)) {
        return [
            'scheme' => $matches[1] ?: '',
            'host' => $matches[2] ?: '',
            'port' => $matches[3] ?: '',
            'path' => $matches[4] ?: '',
            'query' => $matches[5] ?: '',
            'fragment' => $matches[6] ?: ''
        ];
    }

    return null;
}

// 解析查询参数
function parseQuery($query) {
    if (empty($query)) {
        return [];
    }

    $params = [];
    $pattern = '/([^&=]+)=([^&]*)/';

    preg_match_all($pattern, $query, $matches);

    for ($i = 0; $i < count($matches[1]); $i++) {
        $params[urldecode($matches[1][$i])] = urldecode($matches[2][$i]);
    }

    return $params;
}

$urls = [
    "https://www.example.com/path/to/page?param1=value1&param2=value2#section1",
    "http://localhost:8080/api/users?id=123&format=json",
    "ftp://ftp.example.org/files/download.zip"
];

foreach ($urls as $url) {
    echo "解析URL: {$url}\n";
    $parsed = parseURL($url);
    if ($parsed) {
        foreach ($parsed as $key => $value) {
            echo "  {$key}: {$value}\n";
        }

        if (!empty($parsed['query'])) {
            echo "  查询参数:\n";
            $params = parseQuery($parsed['query']);
            foreach ($params as $key => $value) {
                echo "    {$key}: {$value}\n";
            }
        }
    }
    echo "\n";
}
?>

密码强度验证

<?php
function checkPasswordStrength($password) {
    $strength = [
        'score' => 0,
        'feedback' => [],
        'level' => 'weak'
    ];

    // 长度检查
    if (strlen($password) >= 8) {
        $strength['score'] += 1;
    } else {
        $strength['feedback'][] = "密码长度至少需要8位";
    }

    if (strlen($password) >= 12) {
        $strength['score'] += 1;
    }

    // 包含小写字母
    if (preg_match('/[a-z]/', $password)) {
        $strength['score'] += 1;
    } else {
        $strength['feedback'][] = "密码应包含小写字母";
    }

    // 包含大写字母
    if (preg_match('/[A-Z]/', $password)) {
        $strength['score'] += 1;
    } else {
        $strength['feedback'][] = "密码应包含大写字母";
    }

    // 包含数字
    if (preg_match('/\d/', $password)) {
        $strength['score'] += 1;
    } else {
        $strength['feedback'][] = "密码应包含数字";
    }

    // 包含特殊字符
    if (preg_match('/[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]/', $password)) {
        $strength['score'] += 1;
    } else {
        $strength['feedback'][] = "密码应包含特殊字符";
    }

    // 检查常见弱密码模式
    if (preg_match('/^[a-zA-Z]+$/', $password) || preg_match('/^\d+$/', $password)) {
        $strength['score'] -= 1;
        $strength['feedback'][] = "密码不应是纯字母或纯数字";
    }

    // 检查重复字符
    if (preg_match('/(.)\1{2,}/', $password)) {
        $strength['score'] -= 1;
        $strength['feedback'][] = "密码不应包含连续重复字符";
    }

    // 检查常见密码
    $common_passwords = ['password', '123456', 'admin', 'qwerty', 'abc123'];
    foreach ($common_passwords as $common) {
        if (stripos($password, $common) !== false) {
            $strength['score'] -= 2;
            $strength['feedback'][] = "密码包含常见的弱密码组合";
            break;
        }
    }

    // 确定强度等级
    if ($strength['score'] >= 5) {
        $strength['level'] = 'very_strong';
    } elseif ($strength['score'] >= 4) {
        $strength['level'] = 'strong';
    } elseif ($strength['score'] >= 2) {
        $strength['level'] = 'medium';
    } else {
        $strength['level'] = 'weak';
    }

    return $strength;
}

$passwords = [
    "password",
    "12345678",
    "Password123",
    "P@ssw0rd!2024",
    "MyStr0ng#P@ss"
];

foreach ($passwords as $password) {
    echo "密码: {$password}\n";
    $result = checkPasswordStrength($password);
    echo "  强度: {$result['level']} (得分: {$result['score']})\n";
    if (!empty($result['feedback'])) {
        echo "  建议:\n";
        foreach ($result['feedback'] as $feedback) {
            echo "    - {$feedback}\n";
        }
    }
    echo "\n";
}
?>

实际应用示例

搜索引擎关键词高亮

<?php
class KeywordHighlighter {
    private $keywords;
    private $highlightClass;

    public function __construct($keywords = [], $highlightClass = 'highlight') {
        $this->keywords = $keywords;
        $this->highlightClass = $highlightClass;
    }

    /**
     * 高亮文本中的关键词
     */
    public function highlight($text) {
        if (empty($this->keywords)) {
            return $text;
        }

        // 转义HTML特殊字符
        $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');

        foreach ($this->keywords as $keyword) {
            if (!empty(trim($keyword))) {
                $pattern = '/(' . preg_quote($keyword, '/') . ')/i';
                $replacement = '<span class="' . $this->highlightClass . '">$1</span>';
                $text = preg_replace($pattern, $replacement, $text);
            }
        }

        return $text;
    }

    /**
     * 生成包含上下文的关键词摘要
     */
    public function generateExcerpt($text, $maxLength = 200, $contextLength = 50) {
        $excerpt = '';

        foreach ($this->keywords as $keyword) {
            if (empty(trim($keyword))) {
                continue;
            }

            // 查找关键词位置
            $pattern = '/(' . preg_quote($keyword, '/') . ')/i';
            if (preg_match($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) {
                $position = $matches[0][1];
                $start = max(0, $position - $contextLength);
                $end = min(strlen($text), $position + strlen($keyword) + $contextLength);

                $context = substr($text, $start, $end - $start);

                // 高亮关键词
                $highlighted = preg_replace(
                    $pattern,
                    '<span class="' . $this->highlightClass . '">$1</span>',
                    $context
                );

                if (!empty($excerpt)) {
                    $excerpt .= ' ... ';
                }

                $excerpt .= ($start > 0 ? '...' : '') . $highlighted . ($end < strlen($text) ? '...' : '');

                if (strlen($excerpt) >= $maxLength) {
                    break;
                }
            }
        }

        return $excerpt ?: substr($text, 0, $maxLength) . '...';
    }
}

// 使用示例
$search_keywords = ['PHP', '正则表达式', '文本'];
$highlighter = new KeywordHighlighter($search_keywords);

$text = "PHP是一种广泛使用的编程语言。正则表达式是PHP中处理文本的强大工具,可以用于复杂的模式匹配和文本替换。学习PHP的正则表达式功能对于Web开发非常重要。";

echo "高亮后的文本:\n";
echo $highlighter->highlight($text) . "\n\n";

echo "包含关键词的摘要:\n";
echo $highlighter->generateExcerpt($text, 150) . "\n";

// 输出对应的CSS
echo "\n推荐CSS:\n";
echo ".highlight { background-color: yellow; font-weight: bold; }\n";
?>

日志文件分析器

<?php
class LogAnalyzer {
    /**
     * 解析Apache访问日志
     */
    public static function parseApacheLog($logLine) {
        // Apache Common Log Format
        $pattern = '/^(\S+) \S+ \S+ \[([\w:/]+\s[+\-]\d{4})\] "(\S+) (\S+) (\S+)" (\d{3}) (\d+|-) "([^"]*)" "([^"]*)"$/';

        if (preg_match($pattern, $logLine, $matches)) {
            return [
                'ip' => $matches[1],
                'timestamp' => $matches[2],
                'method' => $matches[3],
                'url' => $matches[4],
                'protocol' => $matches[5],
                'status' => $matches[6],
                'size' => $matches[7],
                'referrer' => $matches[8],
                'user_agent' => $matches[9]
            ];
        }

        return null;
    }

    /**
     * 分析日志文件
     */
    public static function analyzeLogFile($filename, $maxLines = 1000) {
        $stats = [
            'total_requests' => 0,
            'status_codes' => [],
            'top_ips' => [],
            'top_pages' => [],
            'error_requests' => []
        ];

        $lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $lineCount = 0;

        foreach ($lines as $line) {
            if ($lineCount >= $maxLines) {
                break;
            }

            $parsed = self::parseApacheLog($line);
            if ($parsed) {
                $stats['total_requests']++;

                // 统计状态码
                $status = $parsed['status'];
                if (!isset($stats['status_codes'][$status])) {
                    $stats['status_codes'][$status] = 0;
                }
                $stats['status_codes'][$status]++;

                // 统计IP访问次数
                $ip = $parsed['ip'];
                if (!isset($stats['top_ips'][$ip])) {
                    $stats['top_ips'][$ip] = 0;
                }
                $stats['top_ips'][$ip]++;

                // 统计页面访问次数
                $url = $parsed['url'];
                if (!isset($stats['top_pages'][$url])) {
                    $stats['top_pages'][$url] = 0;
                }
                $stats['top_pages'][$url]++;

                // 记录错误请求
                if (substr($status, 0, 1) === '4' || substr($status, 0, 1) === '5') {
                    $stats['error_requests'][] = $parsed;
                }
            }

            $lineCount++;
        }

        // 排序统计结果
        arsort($stats['top_ips']);
        arsort($stats['top_pages']);
        $stats['top_ips'] = array_slice($stats['top_ips'], 0, 10, true);
        $stats['top_pages'] = array_slice($stats['top_pages'], 0, 10, true);

        return $stats;
    }

    /**
     * 提取特定时间段的日志
     */
    public static function filterByTime($logs, $startTime, $endTime) {
        $filtered = [];

        foreach ($logs as $log) {
            $timestamp = strtotime($log['timestamp']);
            if ($timestamp >= $startTime && $timestamp <= $endTime) {
                $filtered[] = $log;
            }
        }

        return $filtered;
    }
}

// 创建示例日志文件
$sampleLog = "192.168.1.1 - - [25/Dec/2024:10:00:00 +0800] \"GET /index.html HTTP/1.1\" 200 1234 \"-\" \"Mozilla/5.0\"\n" .
            "192.168.1.2 - - [25/Dec/2024:10:00:05 +0800] \"GET /about.html HTTP/1.1\" 200 5678 \"http://example.com/\" \"Mozilla/5.0\"\n" .
            "192.168.1.1 - - [25/Dec/2024:10:00:10 +0800] \"POST /login.php HTTP/1.1\" 200 345 \"http://example.com/login.html\" \"Mozilla/5.0\"\n" .
            "192.168.1.3 - - [25/Dec/2024:10:00:15 +0800] \"GET /nonexistent.html HTTP/1.1\" 404 789 \"http://example.com/\" \"Mozilla/5.0\"\n" .
            "192.168.1.2 - - [25/Dec/2024:10:00:20 +0800] \"GET /admin.html HTTP/1.1\" 403 456 \"-\" \"Mozilla/5.0\"\n";

file_put_contents('sample_access.log', $sampleLog);

// 分析日志
$stats = LogAnalyzer::analyzeLogFile('sample_access.log');

echo "日志分析结果:\n";
echo "总请求数: {$stats['total_requests']}\n\n";

echo "状态码统计:\n";
foreach ($stats['status_codes'] as $code => $count) {
    echo "  {$code}: {$count} 次\n";
}

echo "\n访问最多的IP:\n";
foreach ($stats['top_ips'] as $ip => $count) {
    echo "  {$ip}: {$count} 次\n";
}

echo "\n访问最多的页面:\n";
foreach ($stats['top_pages'] as $page => $count) {
    echo "  {$page}: {$count} 次\n";
}

echo "\n错误请求:\n";
foreach ($stats['error_requests'] as $error) {
    echo "  {$error['ip']} {$error['timestamp']} {$error['status']} {$error['url']}\n";
}
?>

正则表达式优化技巧

1. 提高匹配效率

<?php
// 使用具体字符而不是通配符
$bad = '/.*hello.*/';      // 效率低
$good = '/\bhello\b/';     // 效率高

// 使用锚点减少回溯
$bad = '/hello.*world/';
$good = '/^hello.*world$/';

// 避免贪婪匹配,使用非贪婪
$text = "<div>内容1</div><div>内容2</div>";

// 贪婪匹配(可能匹配过多)
$greedy = preg_match('/<div>.*<\/div>/', $text, $matches);
echo "贪婪匹配: " . $matches[0] . "\n";

// 非贪婪匹配(更精确)
$non_greedy = preg_match('/<div>.*?<\/div>/', $text, $matches);
echo "非贪婪匹配: " . $matches[0] . "\n";

// 使用原子组防止回溯
$pattern = '/(?>hello|world)+/';
?>

2. 减少复杂度

<?php
// 简化字符类
$bad = '/[a-zA-Z0-9_]/';
$good = '/\w/';

// 使用预定义字符类
$bad = '/[0-9]/';
$good = '/\d/';

$bad = '/[^0-9]/';
$good = '/\D/';

// 合并相似的分组
$bad = '/(cat|dog|bird|fish)/';
$good = '/(cat|dog|bird|fish)/';  // 这个示例中无法简化,但原理是减少回溯

// 使用更具体的模式
$bad = '/.*@.*\..*/';  // 太宽泛
$good = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';  // 更具体
?>

3. 使用修饰符

<?php
// i - 大小写不敏感
$pattern = '/hello/i';

// m - 多行模式(^和$匹配每行的开始和结束)
$text = "hello\nworld\nhello";
preg_match_all('/^hello$/m', $text, $matches);

// s - 点号匹配换行符
$html = "<div>内容\n包含换行</div>";
preg_match('/<div>.*<\/div>/s', $html, $matches);

// U - 非贪婪模式(全局设置)
preg_match('/<div>.*<\/div>/U', $text, $matches);

// x - 忽略空白字符(便于阅读)
$pattern = '/
    ^                 # 开始
    1[3-9]           # 手机号前缀
    \d{9}            # 后9位数字
    $                 # 结束
/x';
?>

常见错误和解决方案

1. 回溯灾难

<?php
// 错误示例:可能导致回溯灾难
$text = str_repeat('a', 10000) . 'b';
$bad_pattern = '/a+ab/';

// 可能导致性能问题
$result = preg_match($bad_pattern, $text);

// 正确的做法:使用原子组
$good_pattern = '/(?>a+)b/';
$result = preg_match($good_pattern, $text);

// 或者使用占有量词
$better_pattern = '/a++b/';
$result = preg_match($better_pattern, $text);
?>

2. 忽略边界情况

<?php
// 错误:没有处理空字符串
function validateEmail($email) {
    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    return preg_match($pattern, $email);  // 空字符串也会返回false
}

// 正确:先检查空值
function validateEmail($email) {
    if (empty($email)) {
        return false;
    }

    $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
    return preg_match($pattern, $email);
}

// 错误:没有限制长度
function validateUsername($username) {
    return preg_match('/^[a-zA-Z0-9_]+$/', $username);
}

// 正确:检查长度
function validateUsername($username) {
    $length = strlen($username);
    if ($length < 3 || $length > 20) {
        return false;
    }

    return preg_match('/^[a-zA-Z0-9_]+$/', $username);
}
?>

3. 转义问题

<?php
// 错误:没有转义用户输入
function search($text, $keyword) {
    $pattern = '/' . $keyword . '/';  // 危险!
    return preg_match($pattern, $text);
}

// 正确:转义特殊字符
function search($text, $keyword) {
    $escaped_keyword = preg_quote($keyword, '/');
    $pattern = '/' . $escaped_keyword . '/i';
    return preg_match($pattern, $text);
}

// 示例:用户输入包含正则特殊字符
$text = "The price is $5.99";
$keyword = "$5";  // 用户输入

// 不转义的后果
$pattern = '/' . $keyword . '/';  // 会变成 /\$5/,可能不是预期的行为

// 正确的做法
$escaped = preg_quote($keyword, '/');
$pattern = '/' . $escaped . '/';  // 正确的模式
?>

本节练习

基础练习

  1. 编写正则表达式验证手机号格式
  2. 创建验证邮箱格式的正则表达式
  3. 编写提取URL中域名和路径的正则表达式
  4. 创建验证密码复杂度的正则表达式

进阶练习

  1. 实现一个简单的模板引擎,支持变量替换
  2. 编写HTML标签清理函数,保留指定标签
  3. 创建日志文件分析器,提取特定信息
  4. 实现关键词搜索和结果高亮功能

实战练习

  1. 完善KeywordHighlighter类,添加更多功能
  2. 扩展LogAnalyzer类,支持更多日志格式
  3. 创建一个完整的表单验证系统
  4. 实现一个简单的爬虫,提取网页中的特定信息

总结

本节我们学习了PHP正则表达式的基础知识和应用:

  • 基础语法:字符类、量词、锚点、分组等
  • PHP函数preg_match(), preg_replace(), preg_split()
  • 常用模式:邮箱、手机号、URL、密码强度验证
  • 实际应用:文本处理、日志分析、搜索高亮等
  • 性能优化:避免回溯灾难、简化模式、正确使用修饰符

正则表达式是一个强大但复杂的工具,需要大量练习才能熟练掌握。在实际应用中要注意性能和安全问题,避免过度复杂的正则表达式。

💡 学习建议

  1. 从简单的模式开始,逐步增加复杂度
  2. 多练习,多查看他人的正则表达式写法
  3. 使用正则表达式测试工具验证模式
  4. 注意性能,避免过度复杂的正则表达式
  5. 始终考虑安全性,正确转义用户输入

下一节预告:我们将学习字符串格式化技术,包括数字格式化、日期时间处理和模板系统。