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¶m2=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 . '/'; // 正确的模式
?>
本节练习
基础练习
- 编写正则表达式验证手机号格式
- 创建验证邮箱格式的正则表达式
- 编写提取URL中域名和路径的正则表达式
- 创建验证密码复杂度的正则表达式
进阶练习
- 实现一个简单的模板引擎,支持变量替换
- 编写HTML标签清理函数,保留指定标签
- 创建日志文件分析器,提取特定信息
- 实现关键词搜索和结果高亮功能
实战练习
- 完善KeywordHighlighter类,添加更多功能
- 扩展LogAnalyzer类,支持更多日志格式
- 创建一个完整的表单验证系统
- 实现一个简单的爬虫,提取网页中的特定信息
总结
本节我们学习了PHP正则表达式的基础知识和应用:
- 基础语法:字符类、量词、锚点、分组等
- PHP函数:
preg_match(),preg_replace(),preg_split()等 - 常用模式:邮箱、手机号、URL、密码强度验证
- 实际应用:文本处理、日志分析、搜索高亮等
- 性能优化:避免回溯灾难、简化模式、正确使用修饰符
正则表达式是一个强大但复杂的工具,需要大量练习才能熟练掌握。在实际应用中要注意性能和安全问题,避免过度复杂的正则表达式。
💡 学习建议:
- 从简单的模式开始,逐步增加复杂度
- 多练习,多查看他人的正则表达式写法
- 使用正则表达式测试工具验证模式
- 注意性能,避免过度复杂的正则表达式
- 始终考虑安全性,正确转义用户输入
下一节预告:我们将学习字符串格式化技术,包括数字格式化、日期时间处理和模板系统。