Нарезка HTML на основе разделителя

Я конвертирую документы 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... -->

Следует проанализировать как:

Раздел 1:

<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>

Раздел 2:

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

Раздел 3:

<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>
  • Я не могу просто "взорвать" /срез на основе разделителя, потому что это нарушит HTML. Каждый бит текстового содержимого содержит много родительских элементов.

  • У меня нет контроля над структурой HTML, и она иногда изменяется в зависимости от структуры документа Word. Конечный пользователь импортирует свой документ Word для анализа в приложении, поэтому полученный HTML-код не будет изменен перед анализом.

  • Часто содержимое находится на разных глубинах в HTML.

  • Я не могу полагаться на классы элементов или идентификаторы, потому что они несовместимы с doc до doc. # div1, # div2 и # div3 предназначены только для иллюстрации в моем примере.

  • Моя цель состоит в том, чтобы разобрать содержимое, поэтому, если пустые элементы остались на этом ОК, я могу просто снова запустить разметку и удалить пустые теги (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

Чтобы решить проблему, подобную этой, вам сначала нужно выполнить шаги, необходимые для получения решения, прежде чем даже начать код.

  • Найдите элемент, начинающийся с [[разделитель]]
  • Проверьте, имеет ли он родительский элемент next sibling
  • Нет? Повторить 2
  • Да? Этот следующий родной брат содержит контент.

Теперь, когда вы положите это на работу, вы уже готовы на 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 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>
    )
  )
)

Ответ 2

Ближайший я до сих пор...

$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>.

Было бы интересно посмотреть, насколько это здорово, но сложно проверить.

"Последний бит" пытается взять элемент с последним маркером в (в данном случае 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;
}