免责声明:一般来说,正则表达式不是最好的工具parsingHTML。然而,PCRE 库(由 PHP 使用)preg_*()
函数族),确实允许解决诸如此类的重要数据抓取问题(有一些限制和警告 - 见下文)。单独使用正则表达式解决上述问题特别复杂,并且如下所示的正则表达式解决方案并不适合所有人,应该never由正则表达式新手尝试。要正确理解下面的答案,需要对几种高级正则表达式结构和技术有相当深入的理解。
考虑到这一点,如果您想了解如何设计高级正则表达式来解决这个问题(对于除少数(不太可能)特殊情况之外的所有情况 - 请参阅下面的示例),请继续阅读...
高级递归正则表达式解决方案:正如韦斯·哈德克(Wes Hardaker)正确指出的那样,DIV
s 可以(并且经常)嵌套。然而,他说的并不是100%正确“在正确的 之前你无法构建一个匹配的”。事实是,使用 PHP,you can!(有一些限制 - 见下文)。与 Perl 和 .NET 一样,PHP 中的 PCRE 正则表达式引擎提供递归表达式(即(?R)
, (?1)
, (?2)
等),允许将嵌套结构匹配到任意深度(仅受内存限制)。例如,您可以轻松地将平衡嵌套括号与以下表达式匹配:'/\((?:[^()]++|(?R))*+\)/'
。如果您有任何疑问,请运行这个简单的测试:
$text = 'zero(one(two)one(two(three)two)one)zero';
if (preg_match('/\((?:[^()]++|(?R))*+\)/', $text, $matches)) {
print_r($matches);
}
因此,如果我们都同意 PHP 正则表达式确实可以匹配嵌套结构,那么让我们继续解决当前的问题。这个特殊的问题由于最外层的事实而变得复杂DIV
必须有id="content"
属性,但任何嵌套DIV
可能会也可能不会。因此,我们不能使用(?R)
递归匹配整个表达式构造,因为匹配外部 DIV 的子表达式与匹配内部 DIV 所需的子表达式不同DIV
s。在这种情况下,我们需要有一个捕获组(在本例中为组 2),它将用作“递归子程序”,匹配内部、嵌套DIV
的。这是一个经过测试的 PHP 代码片段,具有高级功能不是为了胆小的人,而是为了让你能够真正理解它正则表达式,它正确匹配(在大多数情况下 - 见下文),aDIV
having id="content"
,它本身可能包含嵌套DIV
s:
$re = '% # Match a DIV element having id="content".
<div\b # Start of outer DIV start tag.
[^>]*? # Lazily match up to id attrib.
\bid\s*+=\s*+ # id attribute name and =
([\'"]?+) # $1: Optional quote delimiter.
\bcontent\b # specific ID to be matched.
(?(1)\1) # If open quote, match same closing quote
[^>]*+> # remaining outer DIV start tag.
( # $2: DIV contents. (may be called recursively!)
(?: # Non-capture group for DIV contents alternatives.
# DIV contents option 1: All non-DIV, non-comment stuff...
[^<]++ # One or more non-tag, non-comment characters.
# DIV contents option 2: Start of a non-DIV tag...
| < # Match a "<", but only if it
(?! # is not the beginning of either
/?div\b # a DIV start or end tag,
| !-- # or an HTML comment.
) # Ok, that < was not a DIV or comment.
# DIV contents Option 3: an HTML comment.
| <!--.*?--> # A non-SGML compliant HTML comment.
# DIV contents Option 4: a nested DIV element!
| <div\b[^>]*+> # Inner DIV element start tag.
(?2) # Recurse group 2 as a nested subroutine.
</div\s*> # Inner DIV element end tag.
)*+ # Zero or more of these contents alternatives.
) # End 2$: DIV contents.
</div\s*> # Outer DIV end tag.
%isx';
if (preg_match($re, $text, $matches)) {
printf("Match found:\n%s\n", $matches[0]);
}
正如我所说,这个正则表达式非常复杂,但请放心,它确实有效!除了下面提到的一些不太可能发生的情况 - (如果您能找到,我可能会非常感激)。尝试一下,亲自看看!
我应该用这个吗?在必须以 100% 可靠性和准确性解析数百或数千个文档的生产环境中使用此正则表达式解决方案是否合适?当然不是。它对于某些 HTML 文件的有限一次性运行有用吗? (例如,可能是问这个问题的人?)可能。这取决于人们对高级正则表达式的适应程度。如果上面的正则表达式看起来像是用外语编写的(确实如此),并且/或者让您感到害怕,那么答案可能是否定的。
有用?是的。例如,给定以下测试数据,上面的正则表达式正确地挑选出DIV
拥有id="content"
(or id='content'
or id=content
对于这个问题):
<!DOCTYPE HTML SYSTEM>
<html>
<head><title>Test Page</title></head>
<body>
<div id="non-content-div">
<h1>PCRE does recursion!</h1>
<div id='content'>
<h2>First level matched</h2>
<!-- this comment </div> is tricky -->
<div id="one-deep">
<h3>Second level matched</h3>
<div id=two-deep>
<h4>Third level matched</h4>
<div id=three-deep>
<h4>Fourth level matched</h4>
</div>
<p>stuff</p>
</div>
<!-- this comment <div> is tricky -->
<p>stuff</p>
</div>
<p>stuff</p>
</div>
<p>stuff</p>
</div>
<p>stuff</p>
</body></html>
CAVEATS:那么这个解决方案在哪些场景下不起作用呢?出色地,DIV
开始标签的任何属性中都不能有任何尖括号(可以消除此限制,但这会给代码增加相当多的内容)。以及以下内容CDATA
跨度,其中包含特定的DIV
我们正在寻找的开始标记(极不可能)将导致正则表达式失败:
<style type="text/css">
p:before {
content: 'Unlikely CSS string with <div id=content> in it.';
}
</style>
<p title="Unlikely attribute with a <div id=content> in it">stuff</p>
<script type="text/javascript">
alert("evil script with <div id=content> in it">");
</script>
<!-- Comment with <div id="content"> in it -->
<![CDATA[ a CDATA section with <div id="content"> in it ]]>
我非常想了解其他人。
去阅读 MRE3正如我之前所说,要真正掌握这里发生的事情,需要对几种先进技术有相当深入的了解。这些技术并不明显或直观。据我所知,获得这些技能的方法只有一种,那就是坐下来学习:掌握正则表达式(第三版)作者:杰弗里·弗里德尔 (MRE3)。 (你会很高兴你这么做了!)
我可以诚实地说这是我一生中读过的最有用的书!
编辑2013-04-30修复了正则表达式。此前它不允许非DIV
紧随其后的标签DIV
开始标记。