許多開發者在久違地重拾正則表達式時,都會有種「似曾相識」的熟悉感:明明是個小案例,卻總覺得得徹底搞懂不可,於是翻閱手冊、寫測試、終於豁然開朗,但下次遇到又得從頭來過。今天就把實務上常用的「兩階段匹配法」記錄下來,供日後快速參考。
直接對整張 HTML 寫一個超級複雜的正則既難維護又易出錯,不如拆成兩步走:
<div> 區塊完整擷取出來。<a href="...">文字</a>,捕獲組 1 取 href,捕獲組 2 取顯示文字。$str = '<div><a href="shoes/running.html">跑步鞋</a><a href="shoes/basketball.html">籃球鞋</a></div>tryagin';
// 階段 1:取出第一個 div 區塊
preg_match_all('/<div>(.*?)<\/div>/is', $str, $matches);
$target_content = $matches[0][0] ?? '';
// 階段 2:抓取所有 a 標籤的 href 與文字
preg_match_all('/<a\s+href="([^"]+)">(.*?)<\/a>/is', $target_content, $matches_detail);
$href_content = $matches_detail[1] ?? []; // 連結陣列
$detail_content = $matches_detail[2] ?? []; // 文字陣列
print_r($href_content); // Array ( [0] => shoes/running.html [1] => shoes/basketball.html )
print_r($detail_content); // Array ( [0] => 跑步鞋 [1] => 籃球鞋 )
在實際專案中,常需從 CMS 後台撈取版面內容並解析。下方示範一個可復用的私有方法 get_topic_data(),搭配 clearData() 清理換行/製表符,並支援多主題館(運動、女鞋、男鞋、戶外、童鞋)不同正則規則:
private function get_topic_data($topic, $pattern1, $pattern2) {
$data = [];
if (empty($topic) || empty($pattern1) || empty($pattern2)) return $data;
// 1. 依部門名找頁面 ID
$modpage_id = 0;
foreach ($this->Modpage_Model->getAllPages() as $page) {
if ($page['department'] === $topic) { $modpage_id = $page['id']; break; }
}
// 2. 取得目前版本內容
$version = $this->Modpage_Model->getCurrentPageVersion($modpage_id);
$raw_data = $this->Modpage_Model->getVersionData($modpage_id, $version['version']);
// 3. 依模組類型挑選解析字串
$parse_str = '';
foreach ($raw_data as $v) {
if ($topic === 'children') {
if ($v['module_id'] == 1 && $v['desc'] === '快速找寶貝') { $parse_str = $v['content']; break; }
} else {
if ($v['module_id'] == 4) { $parse_str = $v['content']; break; }
}
}
// 4. 兩階段正則 + 清洗 + 域名替換
$parse_str = $this->clearData($parse_str);
preg_match_all($pattern1, $parse_str, $m);
$target = $m[0][0] ?? '';
$target = str_replace($this->website, TOUCH_URL, $target);
$target = $this->clearData($target);
preg_match_all($pattern2, $target, $md);
$data['href_content'] = $md[1] ?? [];
$data['detail_content'] = $md[2] ?? [];
return $data;
}
private function clearData($str) {
if (empty($str)) return '';
return trim(str_replace(["\r", "\n", "\t"], '', $str));
}
public function index() {
$topic_map = [
'sports' => ['sport', '/<div class="sport">(.*?)<\/div>/is', '/<a href="([^"]+)">(.*?)<\/a>/is'],
'womenshoes' => ['women', '/<div class="women">(.*?)<\/div>/is', '/<a href="([^"]+)">(.*?)<\/a>/is'],
'menshoes' => ['man', '/<div class="man">(.*?)<\/div>/is', '/<a href="([^"]+)">(.*?)<\/a>/is'],
'outdoor' => ['outdoor', '/<div class="outdoor">(.*?)<\/div>/is', '/<a href="([^"]+)">(.*?)<\/a>/is'],
'children' => ['children', '/<div class="children">(.*?)<\/div>/is', '/<a href="([^"]+)">(.*?)<\/a>/is'],
];
$data['data'] = [];
foreach ($topic_map as $type => $patterns) {
$res = $this->get_topic_data($type, $patterns[1], $patterns[2]);
$data['data'][$patterns[0]] = $res;
}
// 載入視圖...
}
s (DOTALL) 讓 . 能匹配換行,避免多行 HTML 斷裂。() 只取需要的部分,$matches[1]、$matches[2] 直接對應 href 與文字。DOMDocument 或 QueryList 等解析器,更穩健。把這套「先切塊、再細抓」的模式記住,下次遇到同類需求就能開工即寫,不再重複踩坑。