使用XPath和PHP解析HTML

Pet*_*ter 2 php regex xpath html-parsing

有没有办法(使用XPath和PHP)执行以下操作(没有外部XSLT文件)?

  • 删除所有表及其内容
  • 删除第一个h1标签后的所有内容
  • 仅保留段落(包括其内部HTML(链接,列表等))

在这里收到了一个XSLT答案,但我正在寻找不需要外部文件的XPATH查询.

目前,我通过以下方式将有问题的HTML加载到SimpleXmlElement中:

$doc = @DOMDocument::loadHTML($xml);
$data = simplexml_import_dom($doc);
Run Code Online (Sandbox Code Playgroud)

现在我需要帮助:

$data = $data->xpath('??????');
Run Code Online (Sandbox Code Playgroud)

一直在使用这个几天无济于事.我非常感谢你的帮助.

编辑:我并不特别在意段落内的内容,因为我可以使用strip_tags来消除我不想要的内容.我需要做的就是将段落与其他来源隔离开来.我想更具体,更准确的要求是:

仅返回未包含在表中且仅在第一个h1标记之前的段落(及其html内容)

编辑2:

我想我已经完成了大部分工作:
$query = $xpath->query('//p[not(ancestor::table) and not(preceding::h2)]');

唯一的问题是内部HTML的丢失.

Gor*_*don 8

要获得不在表中且仅在第一个h1之前的所有P元素,您可以这样做

$xp = new DOMXPath($dom);
$expression = '//p[not(preceding::h1[1]) and not(ancestor::table)]';
foreach ($xp->query($expression) as $node) {
    echo $dom->saveXml($node);
}
Run Code Online (Sandbox Code Playgroud)

在键盘上演示

通常,如果您知道文档中第一个h1的位置,则使用该元素的直接路径更为高效,而不是//在文档中的任何位置搜索的查询.例如,作为替代方案,您还可以在下面的注释中使用Alejandro提供的XPath:

/descendant::h1[1]/preceding::p[not(ancestor::table)]
Run Code Online (Sandbox Code Playgroud)

如果要从源文档中的节点创建新的DOM文档,则必须将节点导入新文档.

// src document
$dom = new DOMDocument;
$dom->loadXML($xml);

// dest document
$new = new DOMDocument;
$new->formatOutput = TRUE;

// xpath setup
$xp = new DOMXPath($dom);
$expr = '//p[not(preceding::h1[1]) and not(ancestor::table)]';

// importing nodes into dest document
foreach ($xp->query($expr) as $node) {
    $new->appendChild($new->importNode($node, TRUE));
}

// output dest document
echo $new->saveXML();
Run Code Online (Sandbox Code Playgroud)

在键盘上演示


还有一些补充

在您的示例中,您使用了错误抑制运算符.这是不好的做法.如果要忽略DOM中的任何解析错误,请使用

libxml_use_internal_errors(TRUE); // catch any DOM errors with libxml
$dom = new DOMDocument;           // remove the @ as it is bad practise
$dom->loadXML($xhtml);            // use loadHTML if it's not valid XHTML
libxml_clear_errors();            // disregards any DOM related errors
Run Code Online (Sandbox Code Playgroud)

使用DOM删除节点始终是相同的方法.找到要删除的节点.得到它parentNoderemoveChild使用要删除的节点作为参数调用它.

foreach ($dom->getElementsByTagName('foo') as $node) {
    $node->parentNode->removeChild($node);
}
Run Code Online (Sandbox Code Playgroud)

您还可以在没有XPath的情况下导航到兄弟节点(和子节点).以下是在第一个h1元素之后删除所有后续兄弟节点的方法

$firstH1 = $dom->getElementsByTagName('h1')->item(0);
while ($firstH1->nextSibling !== NULL) {
    $firstH1->parentNode->removeChild($firstH1->nextSibling);
}
echo $dom->saveXml();
Run Code Online (Sandbox Code Playgroud)

从中删除节点DOMDocumentDOMDocument立即影响.在上面的代码中,我们总是查询第一个h1的第一个兄弟.如果有,则将其从中删除DOMDocument.nextSibling然后指向刚删除的兄弟(如果有的话)之后的兄弟姐妹.


获取和打印所有段落同样容易.要获取outerXML,只需将您想要outerXML的节点传递给该saveXML方法.

foreach ($dom->getElementsByTagName('p') as $paragraph)
{
    echo $dom->saveXml($paragraph);
}
Run Code Online (Sandbox Code Playgroud)

无论如何,这应该让你去.我建议你熟悉DOM API.这并不困难.你会发现你要做的大部分事情都围绕着属性和方法DOMDocument,DOMNode并且DOMElement(它是它的子类DOMNode).

  • +1好答案.也许`/ descendant :: h1 [1]/preceding :: p [not(ancestor :: table)]`会更快(不测试每个`p`的所有前面的内容) (2认同)