首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 截取 HTML 字串並自動補全標籤:三種實用函數完整實作

     編輯:PHP 相關知識

問題背景

文章摘要、列表預覽常需截取 HTML 內容前 N 字元,若直接 mb_substr 會導致標籤未閉合、版面崩潰。本文提供三種成熟解法:堆疊法、正則分割法、DOMDocument 法。

方案一:堆疊法(效能最佳、無外部依賴)

<?php
/**
 * 截取 HTML 並自動補全閉合標籤
 * @param string $html 原始 HTML
 * @param int $length 目標字元數(含標籤內文字)
 * @param string $suffix 截斷後綴
 * @return string
 */
function subHtml(string $html, int $length, string $suffix = '...'): string {
    $result = '';
    $tagStack = [];
    $textLen = 0;

    // 分割:保留標籤作為獨立元素
    $tokens = preg_split('~(<[^>]+?>)~si', $html, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);

    foreach ($tokens as $token) {
        if (trim($token) === '') continue;

        // 自閉合標籤 
等 if (preg_match('~<([a-z0-9]+)[^>]*?/>~si', $token)) { $result .= $token; continue; } // 結束標籤 if (preg_match('~</([a-z0-9]+)[^>]*?>~si', $token, $m)) { if ($tagStack && end($tagStack) === $m[1]) { array_pop($tagStack); $result .= $token; } continue; } // 開始標籤 if (preg_match('~<([a-z0-9]+)[^>]*?>~si', $token, $m)) { $tagStack[] = $m[1]; $result .= $token; continue; } // 註解 if (preg_match('~<!--.*?-->~si', $token)) { $result .= $token; continue; } // 純文字 $tokenLen = mb_strlen($token, 'UTF-8'); if ($textLen + $tokenLen <= $length) { $result .= $token; $textLen += $tokenLen; } else { $remain = $length - $textLen; if ($remain > 0) { $result .= mb_substr($token, 0, $remain, 'UTF-8'); } break; } } // 補全未閉合標籤(逆序) while ($tag = array_pop($tagStack)) { $result .= "</$tag>"; } return $result . ($textLen >= $length ? $suffix : ''); } ?>

方案二:正則分割 + 字元計數(原始碼風格)

<?php
function phpos_chsubstr_ahtml(string $str, int $num, bool $more = false): string {
    if (mb_strlen($str, 'UTF-8') <= $num) return $str;

    $stag = []; $etag = []; $sp = 0; $ep = 0;
    $len = mb_strlen($str, 'UTF-8');
    $word = 0; $i = 0;

    while ($word < $num && $i < $len) {
        $ch = $str[$i];
        $ord = ord($ch);

        if ($ord > 127) { // 多位元組字元(UTF-8 中文約 3 bytes)
            $i += 3; $word++;
        } elseif ($ch === '<') {
            // 解析標籤
            $isEnd = ($str[$i+1] ?? '') === '/';
            $tag = '';
            $j = $i + ($isEnd ? 2 : 1);
            while ($j < $len && $str[$j] !== ' ' && $str[$j] !== '>') {
                $tag .= $str[$j++];
            }
            if ($isEnd) $etag[$ep++] = $tag;
            else $stag[$sp++] = $tag;
            // 跳過完整標籤
            while ($j < $len && $str[$j] !== '>') $j++;
            $i = $j + 1;
        } else {
            $i++; $word++;
        }
    }

    // 配對移除已閉合標籤
    foreach ($etag as $tag) {
        $key = array_search($tag, $stag);
        if ($key !== false) unset($stag[$key]);
    }
    // 移除自閉合標籤
    foreach ($stag as $k => $tag) {
        if (in_array($tag, ['br', 'img', 'hr', 'input'])) unset($stag[$k]);
    }

    $stag = array_reverse($stag);
    $endTags = '</' . implode('></', $stag) . '>';
    return mb_substr($str, 0, $i, 'UTF-8') . $endTags . ($more ? '...' : '');
}
?>

方案三:DOMDocument 正規化法(最穩健、支援 HTML5 需配合 libxml)

<?php
function truncateHtmlDom(string $html, int $maxTextLen, string $suffix = '...'): string {
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML('<meta charset="utf-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
    libxml_clear_errors();

    $xpath = new DOMXPath($dom);
    $textNodes = iterator_to_array($xpath->query('//text()'));
    $total = 0;
    $cutNode = null;
    $cutOffset = 0;

    foreach ($textNodes as $node) {
        $len = mb_strlen($node->nodeValue, 'UTF-8');
        if ($total + $len >= $maxTextLen) {
            $cutNode = $node;
            $cutOffset = $maxTextLen - $total;
            break;
        }
        $total += $len;
    }

    if ($cutNode) {
        // 截斷文字節點
        $cutNode->nodeValue = mb_substr($cutNode->nodeValue, 0, $cutOffset, 'UTF-8');
        // 移除後續所有節點
        $next = $cutNode->nextSibling;
        while ($next) {
            $toRemove = $next;
            $next = $next->nextSibling;
            $toRemove->parentNode->removeChild($toRemove);
        }
        // 同時移除後續祖先的後續兄弟
        $parent = $cutNode->parentNode;
        while ($parent && $parent !== $dom) {
            $next = $parent->nextSibling;
            while ($next) {
                $toRemove = $next;
                $next = $next->nextSibling;
                $toRemove->parentNode->removeChild($toRemove);
            }
            $parent = $parent->parentNode;
        }
    }

    $result = $dom->saveHTML();
    return $result . ($cutNode ? $suffix : '');
}
?>

三方案對比

維度堆疊法正則分割法DOMDocument 法
效能★★★★★★★★★☆★★☆☆☆
容錯率(壞 HTML)★★★☆☆★★☆☆☆★★★★★
程式碼複雜度中高低
UTF-8 支援原生需手動處理原生
推薦場景高流量、純 PHP、HTML 相對乾淨舊專案移植、無 DOM 擴充CMS 後台、使用者輸入不可控、需嚴格輸出

實戰建議

  1. 列表頁摘要:使用 堆疊法,快速且記憶體友善。
  2. RSS/Atom 輸出:使用 DOMDocument,確保輸出合法 XML。
  3. 編輯器預覽:結合 contenteditable 前端即時截取,後端僅做二次驗證。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved