嵌套或遞歸結構通常超出解析的正則表達式的力量,並且您通常需要更強大的解析器。問題是你需要找到下一個令牌,這取決於先前的令牌,這不是正則表達式可以處理的(語言不再是常規的)。
但是,對於這樣一種簡單的語言,您不需要一個完整的具有正式語法的解析器生成器 - 您可以輕鬆地手動編寫簡單的解析器。你只有一個重要的狀態位 - 最後打開的標記。如果您的正則表達式與文本,新的打開標記或當前打開標記的相應關閉標記相匹配,則可以處理此任務。規則是:
- 如果您匹配文本,保存文本並繼續匹配。
- 如果您匹配打開的標籤,請保存打開的標籤,然後繼續匹配,直到找到打開的標籤或相應的關閉標籤。
- 如果您匹配關閉標籤,請停止查找當前打開的標籤並繼續匹配最後未關閉的標籤,文本或其他打開的標籤。
第二步是遞歸 - 每當您找到一個新的打開標記時,就會創建一個新的匹配上下文來查找相應的關閉標記。
這不是必需的,但通常解析器將生成一個簡單的樹結構來表示解析的文本 - 這被稱爲抽象語法樹。在生成語法所代表的內容之前,最好先生成一個語法樹。這使您可以靈活地操作樹或生成不同的輸出(例如,您可以輸出xml以外的內容)。
這是一個將這兩個想法結合起來並解析文本的解決方案。 (它還承認{{
或}}
爲轉義序列即:單一字面{
或}
。)
首先解析器:
class ParseError extends RuntimeException {}
function str_to_ast($s, $offset=0, $ast=array(), $opentag=null) {
if ($opentag) {
$qot = preg_quote($opentag, '%');
$re_text_suppl = '[^{'.$qot.']|{{|'.$qot.'[^}]';
$re_closetag = '|(?<closetag>'.$qot.'\})';
} else {
$re_text_suppl = '[^{]|{{';
$re_closetag = '';
}
$re_next = '%
(?:\{(?P<opentag>[^{\s])) # match an open tag
#which is "{" followed by anything other than whitespace or another "{"
'.$re_closetag.' # if we have an open tag, match the corresponding close tag, e.g. "-}"
|(?P<text>(?:'.$re_text_suppl.')+) # match text
# we allow non-matching close tags to act as text (no escape required)
# you can change this to produce a parseError instead
%ux';
while ($offset < strlen($s)) {
if (preg_match($re_next, $s, $m, PREG_OFFSET_CAPTURE, $offset)) {
list($totalmatch, $offset) = $m[0];
$offset += strlen($totalmatch);
unset($totalmatch);
if (isset($m['opentag']) && $m['opentag'][1] !== -1) {
list($newopen, $_) = $m['opentag'];
list($subast, $offset) = str_to_ast($s, $offset, array(), $newopen);
$ast[] = array($newopen, $subast);
} else if (isset($m['text']) && $m['text'][1] !== -1) {
list($text, $_) = $m['text'];
$ast[] = array(null, $text);
} else if ($opentag && isset($m['closetag']) && $m['closetag'][1] !== -1) {
return array($ast, $offset);
} else {
throw new ParseError("Bug in parser!");
}
} else {
throw new ParseError("Could not parse past offset: $offset");
}
}
return array($ast, $offset);
}
function parse($s) {
list($ast, $offset) = str_to_ast($s);
return $ast;
}
這將產生一個抽象語法樹是「節點」的列表,其中每個節點都是文本格式的array(null, $string)
或格式爲array('-', array(...))
(即類型代碼和另一個節點列表)的數組。
一旦你有了這棵樹,你就可以用它做任何你想做的事。例如,我們可以遞歸遍歷它來生成一個DOM樹:
function ast_to_dom($ast, DOMNode $n = null) {
if ($n === null) {
$dd = new DOMDocument('1.0', 'utf-8');
$dd->xmlStandalone = true;
$n = $dd->createDocumentFragment();
} else {
$dd = $n->ownerDocument;
}
// Map of type codes to element names
$typemap = array(
'*' => 'strong',
'/' => 'em',
'-' => 's',
'>' => 'small',
'|' => 'code',
);
foreach ($ast as $astnode) {
list($type, $data) = $astnode;
if ($type===null) {
$n->appendChild($dd->createTextNode($data));
} else {
$n->appendChild(ast_to_dom($data, $dd->createElement($typemap[$type])));
}
}
return $n;
}
function ast_to_doc($ast) {
$doc = new DOMDocument('1.0', 'utf-8');
$doc->xmlStandalone = true;
$root = $doc->createElement('body');
$doc->appendChild($root);
ast_to_dom($ast, $root);
return $doc;
}
下面是一些測試代碼更困難的測試案例:
$sample = "tëstïng 漢字/漢字 {{ testing -} {*strông
{/ëmphäsïs {-strïkë *}also strike-}/} also {|côdë|}
strong *} {*wôw*} 1, 2, 3";
$ast = parse($sample);
echo ast_to_doc($ast)->saveXML();
這將打印以下內容:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<body>tëstïng 漢字/漢字 {{ testing -} <strong>strông
<em>ëmphäsïs <s>strïkë *}also strike</s></em> also <code>côdë</code>
strong </strong> <strong>wôw</strong> 1, 2, 3</body>
如果你已經有一個DOMDocument
,你想添加一些解析文本,我建議創建一個DOMDocumentFragment
並直接將它傳遞給ast_to_dom
,然後將其附加到所需的容器元素。
你會發現很難用正則表達式解決很多這些問題。正則表達式不是用這種方式解析文檔的 - 你要找的是一個真正的解析器,可能就像[this](http://lime-php.sourceforge.net/) – 2013-04-07 03:03:32