我在S3中存储了大约200万个包含各种HTML的HTML页面。我试图仅从这些存储的页面中提取内容,但我希望保留具有某些约束的HTML结构。这些HTML都是用户提供的输入,应该被认为是不安全的。因此,出于显示目的,我想只保留一些对属性和属性值有约束的HTML标记,但仍然保留所有正确编码的文本内容,甚至在不允许的标记中。
例如,我想只允许特定的标签,如<p>
, <h1>
, <h2>
, <h3>
, <ul>
, <ol>
, <li>
等。但是我还想保留在不允许的标记之间找到的任何文本并保持其结构。我还希望能够限制每个标签中的属性,或者强制某些属性应用于特定的标签。
例如,在下面的HTML中…
<div id="content">
Some text...
<p class="someclass">Hello <span style="color: purple;">PHP</span>!</p>
</div>
我希望结果是……
Some text...
<p>Hello PHP!</p>
因此剥离不需要的<div>
和<span>
标签,所有标签的不需要的属性,并仍然保持<div>
和<span>
内的文本。
简单地使用strip_tags()
在这里不起作用。所以我尝试用domdocument做以下操作。
$dom = new DOMDocument;
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
foreach($dom->childNodes as $node) {
if ($node->nodeName != "p") { // only allow paragraph tags
$text = $node->nodeValue;
$node->parentNode->nodeValue .= $text;
$node->parentNode->removeChild($node);
}
}
echo $dom->saveHTML();
在没有嵌套标签的简单情况下可以工作,但当HTML很复杂时显然会失败。
我不能准确地在每个节点的子节点上递归地调用这个函数,因为如果我删除节点,我将失去所有进一步嵌套的子节点。即使我将节点删除推迟到递归之后,文本插入的顺序也会变得棘手。因为我尝试深入并返回所有有效节点,然后开始将无效子节点的值连接在一起,结果非常混乱。
例如,假设我想在以下HTML
中允许<p>
和<em>
<p>Hello <strong>there <em>PHP</em>!</strong></p>
但是我不想允许<strong>
。如果<strong>
嵌套了<em>
,我的方法会变得非常混乱。因为我会得到像…
<p>Hello there !<em>PHP</em></p>
这显然是错误的。我意识到获得整个nodeValue
是一种糟糕的方式。因此,我开始研究其他的方法来一次一个节点地遍历整个树。只是发现很难推广这个解决方案,使其每次都能正常工作。
使用strip_tags()
的解决方案或这里提供的答案对我的用例没有帮助,因为前者不允许我控制属性,后者删除任何具有属性的标记。我不想删除任何带有属性的标签。我想显式地允许某些标签,但仍然可以扩展控制哪些属性可以在HTML中保留/修改
为了推广解决方案,这个问题似乎需要分成两个小步骤。
首先,遍历DOM树
为了得到一个有效的解决方案,我发现我需要有一个明智的方法来遍历DOM树中的每个节点,并检查它,以确定它是应该保持原样还是修改。
所以我用下面的方法作为一个简单的生成器从DOMDocument
扩展。
class HTMLFixer extends DOMDocument {
public function walk(DOMNode $node, $skipParent = false) {
if (!$skipParent) {
yield $node;
}
if ($node->hasChildNodes()) {
foreach ($node->childNodes as $n) {
yield from $this->walk($n);
}
}
}
}
这样做像foreach($dom->walk($dom) as $node)
给了我一个简单的循环来遍历整个树。当然,由于yield from
语法,这是PHP 7的唯一解决方案,但我对此很满意。
棘手的部分是弄清楚如何保留文本而不是标签,同时在循环中进行修改。因此,在与几种不同的方法进行斗争之后,我发现最简单的方法是构建要从循环内部删除的标记列表,然后稍后使用DOMNode::insertBefore()
将文本节点附加到树中删除它们。这样以后切除这些淋巴结就没有副作用了。
所以我在DOMDocument
的子类中添加了另一个通用的stripTags
方法。
public function stripTags(DOMNode $node) {
$change = $remove = [];
/* Walk the entire tree to build a list of things that need removed */
foreach($this->walk($node) as $n) {
if ($n instanceof DOMText || $n instanceof DOMDocument) {
continue;
}
$this->stripAttributes($n); // strips all node attributes not allowed
$this->forceAttributes($n); // forces any required attributes
if (!in_array($n->nodeName, $this->allowedTags, true)) {
// track the disallowed node for removal
$remove[] = $n;
// we take all of its child nodes for modification later
foreach($n->childNodes as $child) {
$change[] = [$child, $n];
}
}
}
/* Go through the list of changes first so we don't break the
referential integrity of the tree */
foreach($change as list($a, $b)) {
$b->parentNode->insertBefore($a, $b);
}
/* Now we can safely remove the old nodes */
foreach($remove as $a) {
if ($a->parentNode) {
$a->parentNode->removeChild($a);
}
}
}
这里的技巧是,因为我们使用insertBefore
,在不允许的标记的子节点(即文本节点)上,将它们移动到父标记,我们可以很容易地破坏树(我们正在复制)。一开始这让我很困惑,但看看这个方法的工作方式,它是有道理的。延迟节点的移动可以确保当更深的节点是允许的,但它的父节点不在允许的标签列表中时,我们不会破坏parentNode
引用。
<标题> 的完整解决方案这是我想到的更普遍地解决这个问题的完整解决方案。我将包含在我的回答中,因为我努力在其他地方找到许多使用DOMDocument这样做的边缘情况。它允许您指定允许哪些标记,并删除所有其他标记。它还允许您指定哪些属性是允许的,所有其他属性都可以删除(甚至强制某些标签上的某些属性)。
class HTMLFixer extends DOMDocument {
protected static $defaultAllowedTags = [
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'pre',
'code',
'blockquote',
'q',
'strong',
'em',
'del',
'img',
'a',
'table',
'thead',
'tbody',
'tfoot',
'tr',
'th',
'td',
'ul',
'ol',
'li',
];
protected static $defaultAllowedAttributes = [
'a' => ['href'],
'img' => ['src'],
'pre' => ['class'],
];
protected static $defaultForceAttributes = [
'a' => ['target' => '_blank'],
];
protected $allowedTags = [];
protected $allowedAttributes = [];
protected $forceAttributes = [];
public function __construct($version = null, $encoding = null, $allowedTags = [],
$allowedAttributes = [], $forceAttributes = []) {
$this->setAllowedTags($allowedTags ?: static::$defaultAllowedTags);
$this->setAllowedAttributes($allowedAttributes ?: static::$defaultAllowedAttributes);
$this->setForceAttributes($forceAttributes ?: static::$defaultForceAttributes);
parent::__construct($version, $encoding);
}
public function setAllowedTags(Array $tags) {
$this->allowedTags = $tags;
}
public function setAllowedAttributes(Array $attributes) {
$this->allowedAttributes = $attributes;
}
public function setForceAttributes(Array $attributes) {
$this->forceAttributes = $attributes;
}
public function getAllowedTags() {
return $this->allowedTags;
}
public function getAllowedAttributes() {
return $this->allowedAttributes;
}
public function getForceAttributes() {
return $this->forceAttributes;
}
public function saveHTML(DOMNode $node = null) {
if (!$node) {
$node = $this;
}
$this->stripTags($node);
return parent::saveHTML($node);
}
protected function stripTags(DOMNode $node) {
$change = $remove = [];
foreach($this->walk($node) as $n) {
if ($n instanceof DOMText || $n instanceof DOMDocument) {
continue;
}
$this->stripAttributes($n);
$this->forceAttributes($n);
if (!in_array($n->nodeName, $this->allowedTags, true)) {
$remove[] = $n;
foreach($n->childNodes as $child) {
$change[] = [$child, $n];
}
}
}
foreach($change as list($a, $b)) {
$b->parentNode->insertBefore($a, $b);
}
foreach($remove as $a) {
if ($a->parentNode) {
$a->parentNode->removeChild($a);
}
}
}
protected function stripAttributes(DOMNode $node) {
$attributes = $node->attributes;
$len = $attributes->length;
for ($i = $len - 1; $i >= 0; $i--) {
$attr = $attributes->item($i);
if (!isset($this->allowedAttributes[$node->nodeName]) ||
!in_array($attr->name, $this->allowedAttributes[$node->nodeName], true)) {
$node->removeAttributeNode($attr);
}
}
}
protected function forceAttributes(DOMNode $node) {
if (isset($this->forceAttributes[$node->nodeName])) {
foreach ($this->forceAttributes[$node->nodeName] as $attribute => $value) {
$node->setAttribute($attribute, $value);
}
}
}
protected function walk(DOMNode $node, $skipParent = false) {
if (!$skipParent) {
yield $node;
}
if ($node->hasChildNodes()) {
foreach ($node->childNodes as $n) {
yield from $this->walk($n);
}
}
}
}
那么如果我们有以下HTML
<div id="content">
Some text...
<p class="someclass">Hello <span style="color: purple;">P<em>H</em>P</span>!</p>
</div>
我们只允许<p>
和<em>
。
$html = <<<'HTML'
<div id="content">
Some text...
<p class="someclass">Hello <span style="color: purple;">P<em>H</em>P</span>!</p>
</div>
HTML;
$dom = new HTMLFixer(null, null, ['p', 'em']);
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
echo $dom->saveHTML($dom);
我们会得到这样的东西…
<>之前一些文本…& lt; p>你好术;em>H P ! & lt;/p>之前由于您也可以将其限制为DOM中的特定子树,因此解决方案可以更加一般化。
标题>您可以像这样使用strip_tags():
$html = '<div id="content">
Some text...
<p class="someclass">Hello <span style="color: purple;">PHP</span>!</p>
</div>';
$updatedHTML = strip_tags($text,"<p><h1><h2><h3><ul><ol><li>");
//in second parameter we need to provide which html tag we need to retain.
您可以在这里获得更多信息:http://php.net/manual/en/function.strip-tags.php