基于分隔符切割HTML

14
我正在将Word文档即时转换成HTML,并需要基于分隔符来解析这些HTML。例如:
<div id="div1">
    <p>
        <font>
            <b>[[delimiter]]Start of content section 1.</b>
        </font>
    </p>
    <p>
        <span>More content in section 1</span>
    </p>
</div>
<div id="div2">
    <p>
        <b>
            <font>[[delimiter]]Start of section 2</font>
        </b>
    <p>
    <span>More content in section 2</span>
    <p><font>[[delimiter]]Start of section 3</font></p>
<div>
<div id="div3">
    <span><font>More content in section 3</font></span>
</div>
<!-- This continues on... -->

应当解析为:

第一部分:

<div id="div1">
    <p>
        <font>
            <b>[[delimiter]]Start of content section 1.</b>
        </font>
    </p>
    <p>
        <span>More content in section 1</span>
    </p>
</div>

第二节:

<div id="div2">
    <p>
        <b>
            <font>[[delimiter]]Start of section 2</font>
        </b>
    <p>
    <span>More content in section 2</span>
    <p></p>
<div>

第三节:

<div id="div2">
    <p>
        <b>

        </b>
    <p>
    <p><font>[[delimiter]]Start of section 3</font></p>
<div>
<div id="div3">
    <span><font>More content in section 3</font></span>
</div>
  1. 我不能简单地根据分隔符“explode”/slice,因为那会破坏HTML。每个文本内容都有许多父元素。

  2. 我无法控制HTML结构,有时它会根据Word文档的结构而更改。最终用户将导入他们的Word文档到应用程序中进行解析,因此在解析之前不会修改生成的HTML。

  3. 通常内容位于HTML的不同深度。

  4. 我不能依赖元素类或ID,因为它们在文档之间不一致。#div1、#div2和#div3仅用于我的示例说明。

  5. 我的目标是解析出内容,所以如果还有空元素,那没关系,我可以再次运行标记并删除空标签(p、font、b等)。

我的尝试:

我正在使用PHP DOM扩展来解析HTML并循环遍历节点。但我无法想出一个好的算法来解决这个问题。

$doc = new \DOMDocument();
$doc->loadHTML($html);
$body = $doc->getElementsByTagName('body')->item(0);

foreach ($body->childNodes as $child) {
    if ($child->hasChildNodes()) {
        // Do recursive call...
    } else {
        // Contains slide identifier?
    }
}

1
我认为这是不可实现的,除非你有某些可以定位的div,例如通过ID。如果你可以依赖于它,那么很容易获取特定ID(例如#div1#div2等)的开标签和闭标签之间的所有内容,这就是你想要的内容。然而,你不能只是查找任何 div,因为那是一个通用标签,甚至可能出现在其他div中等。你总是必须为内容的部分定义规则,如果你既不控制标记,也不能依赖于它永远不变,那是不可能的。 - Andy
为什么不要使用 strip_tags 然后将文本输出到某个模板 <div><p>text</><p>text</p></div> - vadim_hr
为什么不使用这个库:https://github.com/ATofighi/phpQuery? - online Thomas
你能够使用 XHTML 生成 XPath 查询吗? - John-Philip
2个回答

7
为了解决这样的问题,你首先需要确定获取解决方案所需的步骤,甚至在开始编码之前。
  1. 查找以[[delimiter]]开头的元素
  2. 检查其父级是否有下一个兄弟节点
  3. 没有?重复第2步
  4. 是的?那么这个下一个兄弟节点包含内容。
一旦你开始使用这个方法,你已经完成了90%。你只需要清理不必要的标签就可以了。
为了得到可以扩展的东西,不要构建一个大的、难以理解的代码堆,而是将所有需要的数据分成可处理的部分。
以下代码使用两个类来实现你所需的功能,并为你提供了一种漂亮的方式来遍历所有元素。它使用PHP Simple HTML DOM Parser而不是DOMDocument,因为我更喜欢它。
<?php
error_reporting(E_ALL);
require_once("simple_html_dom.php");

$html = <<<XML
<body>
        <div id="div1">
                <p>
                        <font>
                                <b>[[delimiter]]Start of content section 1.</b>
                        </font>
                </p>
                <p>
                        <span>More content in section 1</span>
                </p>
        </div>
        <div id="div2">
                <p>
                        <b>
                                <font>[[delimiter]]Start of section 2</font>
                        </b>
                </p>
                <span>More content in section 2</span>
                <p>
                        <font>[[delimiter]]Start of section 3</font>
                </p>
        </div>
        <div id="div3">
                <span>
                        <font>More content in section 3</font>
                </span>
        </div>
</body>
XML;



/*
 * CALL
 */

$parser = new HtmlParser($html, '[[delimiter]]');

//dump found
//decode/encode to only show public values
print_r(json_decode(json_encode($parser)));


/*
 * ACTUAL CODE
 */


class HtmlParser
{
    private $_html;
    private $_delimiter;
    private $_dom;

    public $Elements = array();

    final public function __construct($html, $delimiter)
    {
        $this->_html = $html;
        $this->_delimiter = $delimiter;
        $this->_dom = str_get_html($this->_html);

        $this->getElements();
    }

    final private function getElements()
    {
        //this will find all elements, including parent elements
        //it will also select the actual text as an element, without surrounding tags
        $elements = $this->_dom->find("[contains(text(),'".$this->_delimiter."')]");

        //find the actual elements that start with the delimiter
        foreach($elements as $element) {
            //we want the element without tags, so we search for outertext
            if (strpos($element->outertext, $this->_delimiter)===0) {
                $this->Elements[] = new DelimiterTag($element);
            }
        }

    }

}

class DelimiterTag
{
    private $_element;

    public $Content;
    public $MoreContent;

    final public function __construct($element)
    {
        $this->_element = $element;
        $this->Content = $element->outertext;


        $this->findMore();
    }

    final private function findMore()
    {
        //we need to traverse up until we find a parent that has a next sibling
        //we need to keep track of the child, to cleanup the last parent
        $child = $this->_element;
        $parent = $child->parent();
        $next = null;
        while($parent) {
            $next = $parent->next_sibling();

            if ($next) {
                break;
            }
            $child = $parent;
            $parent = $child->parent();
        }

        if (!$next) {
            //no more content
            return;
        }

        //create empty element, to build the new data
        //go up one more element and clean the innertext
        $more = $parent->parent();
        $more->innertext = "";

        //add the parent, because this is where the actual content lies
        //but we only want to add the child to the parent, in case there are more delimiters
        $parent->innertext = $child->outertext;
        $more->innertext .= $parent->outertext;

        //add the next sibling, because this is where more content lies
        $more->innertext .= $next->outertext;

        //set the variables
        if ($more->tag=="body") {
            //Your section 3 works slightly different as it doesn't show the parent tag, where the first two do.
            //That's why i show the innertext for the root tag and the outer text for others.
            $this->MoreContent = $more->innertext;
        } else {
            $this->MoreContent = $more->outertext;
        }

    }
}




?>

清理后的输出:
stdClass Object
(
  [Elements] => Array
  (
    [0] => stdClass Object
    (
        [Content] => [[delimiter]]Start of content section 1.
        [MoreContent] => <div id="div1">
                            <p><font><b>[[delimiter]]Start of content section 1.</b></font></p>
                            <p><span>More content in section 1</span></p>
                          </div>
    )

    [1] => stdClass Object
    (
        [Content] => [[delimiter]]Start of section 2
        [MoreContent] => <div id="div2">
                            <p><b><font>[[delimiter]]Start of section 2</font></b></p>
                            <span>More content in section 2</span>
                         </div>
    )

    [2] => stdClass Object
    (
        [Content] => [[delimiter]]Start of section 3
        [MoreContent] => <div id="div2">
                            <p><font>[[delimiter]]Start of section 3</font></p>
                         </div>
                         <div id="div3">
                            <span><font>More content in section 3</font></span>
                          </div>
    )
  )
)

不确定我们应该如何处理额外的内容。例如,如果在定界符之间有额外的<div>标签,它们是否应该是内容的一部分? - Nigel Ren
这对我很有帮助。 - Manish Champaneri
@NigelRen 在我处理开放办公室/Excel文件的经验中,愚蠢的用户很聪明地搞砸了很多东西。能够在不到100行代码和几乎没有任何异常的情况下正常运行的实际解析系统的几率接近于零。这就是为什么我用类来构建它,并将所有数据拆分成单独的类,以便更容易地扩展每个部分。因为就像你说的那样,在由Microsoft生成的HTML中,会有很多额外的<div><font>和其他标记。 - Hugo Delsing
2
我已经决定这是一个有趣的理论练习,但实际上是一场噩梦。你可能需要数百个示例来确保任何解决方案都有效,然后就像你所说的,有人会带来第101个示例,再次破坏代码。 - Nigel Ren
1
我想这听起来大致正确。但这取决于文档包含什么内容。如果您尝试解析简历,您将得到一千个不同版本,这很困难。如果您将表单作为Word文档发送给需要填写的人,您可能会获得非常高的成功率,并且可以节省大量时间。但是,与所有用户解析一样,它充满了例外情况。更不用说不同版本的Word之间存在的问题了。 - Hugo Delsing
2
说实话,没有原帖作者的输入,甚至很难验证关于这个文档所做的基本假设。即使是追踪文档向上的“父级有下一个兄弟”的逻辑的基础也可能过于简化了可能的组合情况。 - Nigel Ren

3
到目前为止我找到的最接近的是...
$html = <<<XML
<body>
    <div id="div1">
        <p>
            <font>
                <b>[[delimiter]]Start of content section 1.</b>
            </font>
        </p>
        <p>
            <span>More content in section 1</span>
        </p>
    </div>
    <div id="div2">
        <p>
            <b>
                <font>[[delimiter]]Start of section 2</font>
            </b>
        </p>
        <span>More content in section 2</span>
        <p>
            <font>[[delimiter]]Start of section 3</font>
        </p>
    </div>
    <div id="div3">
        <span>
            <font>More content in section 3</font>
        </span>
    </div>
</body>
XML;
$doc = new \DOMDocument();
$doc->loadHTML($html);
$xp = new DOMXPath($doc);
$div = $xp->query("body/node()[descendant::*[contains(text(),'[[delimiter]]')]]");

foreach ($div as $child) {
    echo "Div=".$doc->saveHTML($child).PHP_EOL;
}

echo "Last bit...".$doc->saveHTML($child).PHP_EOL;
$div = $xp->query("following-sibling::*", $child);
foreach ($div as $remain) {
    echo $doc->saveHTML($remain).PHP_EOL;
}

我认为我必须调整HTML以更正(希望如此)错误的缺少</div>

看到这个是否具有鲁棒性是很有趣的,但很难测试。

'last bit' 试图获取最后一个标记元素(在本例中为 div2),直到文档结尾(使用 following-sibling::*)。

还要注意,它假定body标签是文档基础。因此,需要根据您的文档进行调整。可能只需将其更改为 //body...

更新 具有更高的灵活性和处理同一整体段落中多个部分的能力...

$html = <<<XML
    <html>
    <body>
        <div id="div1">
            <p>
                <font>
                    <b>[[delimiter]]Start of content section 1.</b>
                </font>
            </p>
            <p>
                <span>More content in section 1</span>
            </p>
        </div>
        <div id="div1a">
            <p>
                <span>More content in section 1</span>
            </p>
        </div>
        <div id="div2">
            <p>
                <b>
                    <font>[[delimiter]]Start of section 2</font>
                </b>
            </p>
            <span>More content in section 2</span>
            <p>
                <font>[[delimiter]]Start of section 3</font>
            </p>
        </div>
        <div id="div3">
            <span>
                <font>More content in section 3</font>
            </span>
        </div>
    </body>
    </html>
XML;

$doc = new \DOMDocument();
$doc->loadHTML($html);
$xp = new DOMXPath($doc);
$div = $xp->query("//body/node()[descendant::*[contains(text(),'[[delimiter]]')]]");

$partCount = $div->length;
for ( $i = 0; $i < $partCount; $i++ )  {
    echo "Div $i...".$doc->saveHTML($div->item($i)).PHP_EOL;

    // Check for multiple sections in same element
    $count = $xp->evaluate("count(descendant::*[contains(text(),'[[delimiter]]')])",
            $div->item($i));
    if ( $count > 1 )   {
        echo PHP_EOL.PHP_EOL;
        for ($j = 0; $j< $count; $j++ ) {
            echo "Div $i.$j...".$doc->saveHTML($div->item($i)).PHP_EOL;
        }
    }
    $div = $xp->query("following-sibling::*", $div->item($i));
    foreach ($div as $remain) {
        if ( $i < $partCount-1 && $remain === $div->item($i+1)  )   {
            break;
        }
        echo $doc->saveHTML($remain).PHP_EOL;
    }

    echo PHP_EOL.PHP_EOL;
}

非常感谢您的回答!我会尝试一下。我之前不知道有这种查询能力。 - user8488500
我已经添加了一些新代码,但由于你说内容非常动态,所以仍然很难测试。但是这个版本试图将我的早期版本与某些可以提供所有中间内容的东西联系起来。使用“following-sibling”方法,直到它到达下一个被识别为具有部分分隔符的元素。 - Nigel Ren

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接