【问题标题】:Find text between 2 tags with malformed XML and including nested tags使用格式错误的 XML 查找 2 个标签之间的文本,包括嵌套标签
【发布时间】:2014-07-10 19:08:36
【问题描述】:

有没有什么简单的方法可以在 2 个带有错误 XML 的标签之间查找文本并忽略嵌套?

鉴于此内容:

<div>
    Some content 1
    </
    <some:tag>
        Section 1
    </some:tag>
    <b>Some content 2
    <some:tag>
        Section 2
        <some:tag>
            Section 3
        </some:tag>
    </some:tag>
    Some content 3
    </p>
</div>

注意:它是故意格式错误的。我不能/不想使用正确的 HTML/XML 解析器,因为我的内容格式不正确,或者在某些情况下甚至不是 XML。同样,我不能/不想对其进行整理,因为它并不总是 HTML/XML。

所以我需要找到&lt;some:tag&gt;&lt;/some:tag&gt;之间的文字,包括嵌套标签。

以上内容会导致:

array (size=2)

  0 => string '<some:tag>
            Section 1
        </some:tag>' (length=52)

  1 => string '<some:tag>
            Section 2
            <some:tag>
                Section 3
            </some:tag>
        </some:tag>' (length=125)

你尝试过什么:

我曾尝试使用 strpos/substr 提取匹配项,但我对逻辑有点迷失:

function findSomeTag($str) {
    $result = [];
    $startTag = "<some:tag>";
    $endTag = "</some:tag>";
    $offset = 0;
    $start = strpos($str, $startTag, $offset);
    while ($start !== false) {
        $nextStart = strpos($str, $startTag, $start + 1);
        $nextEnd = strpos($str, $endTag, $start + 1);
        if ($nextStart === false || $nextEnd < $nextStart) {
            $result[] = substr($str, $start, $nextEnd - $start + strlen($endTag));
        }
        $start = $nextStart;
    }
    return $result;
}

(注意:上面的函数完全不起作用,可能会死循环。)

【问题讨论】:

    标签: php xml parsing xml-parsing html-parsing


    【解决方案1】:

    与我的其他答案不同,此版本将读取带有嵌套标签的标签:

    $text = "
    <div>
        Some content 1
        </
        <some:tag>
            Section 1
        </some:tag>
        <b>Some content 2
        <some:tag>
            Section 2
            <some:tag>
                Section 3
            </some:tag>
        </some:tag>
        Some content 3
        </p>
    </div>
    ";
    
    $parser = new Parser( new TextReader($text) );
    $found = $parser->findTags("<some:tag>", "</some:tag>");
    
    class TextReader {
        private $idx = 0;
        private $reading;
        private $lastIdx;
    
        public function __construct($reading) {
            $this->reading = $reading;
            $this->lastIdx = strlen($reading) - 1;
        }
    
        public function hasMore() {
            return $this->idx < $this->lastIdx;
        }
    
        public function nextChar() {
            if( !$this->hasMore() ) return null;
    
            return $this->reading[$this->idx++];
        }
    
        public function rewind($howFar) {
            $this->idx -= $howFar;
            if( $this->idx < 0 ) $this->idx = 0;
        }
    }
    
    
    class Parser {
        private $TextReader;
    
        public function __construct($TextReader) {
            $this->TextReader = $TextReader;
        }
    
        public function findTags($startTagName, $endTagName) {
            $found = array();
    
            while( ($next = $this->findNextTag($startTagName, $endTagName)) != null ) {
                $found[] = $next;
            }
    
            return $found;
        }
    
        public function findNextTag($startTagName, $endTagName) {
            // find the start of our first tag
            $junk = $this->readForTag($startTagName);
            if( $junk == null ) return null; // didn't find another tag
    
            $nests = 0;
            $started = false;
    
            $startLength = strlen($startTagName);
            $endLength = strlen($endTagName);
    
            $readSoFar = "";
    
            while($this->TextReader->hasMore()) {
                // found a start tag
                if( substr( $readSoFar, $readSoFarLength - $startLength ) == $startTagName ) {
                    $started = true;
                    $nests++;
                }
    
                // found an end tag
                if( substr( $readSoFar, $readSoFarLength - $endLength ) == $endTagName ) $nests--;
    
                $readSoFar .= $this->TextReader->nextChar();
    
                // if we've started, and we found as many starts as ends
                if( $started && $nests == 0 ) return $readSoFar;
            }
    
            return null;
        }
    
        /*
         * read the Text Reader until you find a certain tag, and
         * return what you read before finding the tag, including the tag itself
         *
         * Text Reader will be rewound to the beginning of the tag
         */
        private function readForTag($tagName) {
            $readSoFar = "";
    
            $tagNameLength = strlen($tagName);
    
            while($this->TextReader->hasMore()) {
                // if the last few characters read are the tag
                if( substr( $readSoFar, strlen($readSoFar) - $tagNameLength ) == $tagName ) {
                    // rewind
                    $this->TextReader->rewind($tagNameLength);
    
                    // return what we've read
                    return $readSoFar;
                }
    
                $readSoFar .= $this->TextReader->nextChar();
            }
    
            return null;
        }
    }
    

    【讨论】:

    【解决方案2】:

    要包含嵌套标签,您可以计算当前打开标签的数量。

    所以当$nextEnd &gt; $nextStart 增加$counter 时,只在$nextEnd &lt; $nextStart &amp;&amp; $counter == 1 时添加一个新结果(你有一个打开的标签)。如果$nextEnd &lt; $nextStart &amp;&amp; $counter &lt; 1递减$counter.

    【讨论】:

      【解决方案3】:

      我认为进行任何解析的最简单方法是使用状态机之类的东西。基本上,您定义了一组特定的状态以及您离开这些状态并进入其他状态的条件。

      假设您将文本放在某种文本阅读器中,该阅读器可以为您提供下一个字符并向前移动指针,还可以将指针倒回一定数量的字符。

      然后你可以创建一个类似这样的状态机(它原来是一个简单的状态机,只有一个状态基本上在自身内部循环):

      class StateMachine {
          private $TextReader;
      
          public function __construct($TextReader) {
              $this->TextReader = $TextReader;
          }
      
          public function getTagContents($startTagName, $endTagName) {
              $tagsFound = array();
      
              // read until we get to the start of a tag
              while( $this->stateReadForTag($startTagName) != null ) {
                  // now read until we find the end
                  $contents = $this->stateReadForTag($endTagName);
      
                  // didn't find the end
                  if( $contents == null ) break;
      
                  $tagsFound[] = $contents;
              }
      
              return $tagsFound;
          }
      
          /*
           * read the Text Reader until you find a certain tag, and
           * return what you read before finding the tag, including the tag itself
           *
           * Text Reader will be rewound to the beginning of the tag
           */
          private function stateReadForTag($tagName) {
              $readSoFar = "";
      
              $tagNameLength = strlen($tagName);
      
              while($this->TextReader->hasMore()) {
                  // if the last few characters read are the tag
                  if( substr( $readSoFar, strlen($readSoFar) - $tagNameLength ) == $tagName ) {
                      // rewind
                      $this->TextReader->rewind($tagNameLength);
      
                      // return what we've read
                      return $readSoFar;
                  }
      
                  $readSoFar .= $this->TextReader->nextChar();
              }
      
              return null;
          }
      }
      

      然后这样称呼它:

      $found = $myStateMachine->getTagContents("<some:tag>", "</some:tag>");
      

      TextReader 可能如下所示:

      class TextReader {
          private $idx = 0;
          private $reading;
          private $lastIdx;
      
          public function __construct($reading) {
              $this->reading = $reading;
              $this->lastIdx = strlen($reading) - 1;
          }
      
          public function hasMore() {
              return $this->idx < $this->lastIdx;
          }
      
          public function nextChar() {
              if( !$this->hasMore() ) return null;
      
              return $this->reading[$this->idx++];
          }
      
          public function rewind($howFar) {
              $this->idx -= $howFar;
              if( $this->idx < 0 ) $this->idx = 0;
          }
      }
      

      然后你会这样调用你的状态机:

      $myStateMachine = new StateMachine( new TextReader($myXmlFileContents) );
      $found = $myStateMachine->getTagContents("<some:tag>", "</some:tag>");
      

      【讨论】:

      • 似乎几乎可以工作,但在标签结束后它没有返回内容:viper-7.com/yR7xND
      • @Petah 它将一直读取,直到找到您要查找的标签的结尾。哦,我明白了,您想在原始标签中允许其他 &lt;some:tag&gt;
      • 我会添加一些东西来做到这一点。
      • @Petah 我用嵌套版本添加了另一个答案。它不是真正的状态机,所以我只是将其添加为另一个答案:)
      【解决方案4】:

      这样结束了:

      class TagExtractor {
      
          public $content;
          public $tag;
      
          public function getTagContent() {
              $result = [];
              $startTag = "<{$this->getTag()}>";
              $endTag = "</{$this->getTag()}>";
              $content = $this->getContent();
              $offset = strpos($content, $startTag);
              while ($offset !== false) {
                  $end = $this->findEnd($content, $offset, $startTag, $endTag);
                  $result[] = substr($content, $offset, $end - $offset);
                  $offset = strpos($content, $startTag, $end);
              }
              return $result;
          }
      
          public function findEnd($content, $offset, $startTag, $endTag, $counter = 1) {
              $offset++;
              $nextStart = strpos($content, $startTag, $offset);
              $nextEnd = strpos($content, $endTag, $offset);
              if ($nextEnd === false) {
                  $counter = 0;
              } elseif ($nextStart < $nextEnd && $nextStart !== false) {
                  $counter++;
                  $offset = $nextStart;
              } elseif ($nextEnd < $nextStart || ($nextStart === false && $nextEnd !== false)) {
                  $counter--;
                  $offset = $nextEnd;
              }
              if ($counter === 0) {
                  return $offset + strlen($endTag);
              }
              return $this->findEnd($content, $offset, $startTag, $endTag, $counter);
          }
      
          // <editor-fold defaultstate="collapsed" desc="Getters and setters">
          public function getContent() {
              return $this->content;
          }
      
          public function setContent($content) {
              $this->content = $content;
              return $this;
          }
      
          public function getTag() {
              return $this->tag;
          }
      
          public function setTag($tag) {
              $this->tag = $tag;
              return $this;
          }
          // </editor-fold>
      }
      

      【讨论】:

        猜你喜欢
        • 2016-09-27
        • 1970-01-01
        • 2016-06-30
        • 1970-01-01
        • 2013-07-02
        • 2016-01-10
        • 2020-12-04
        • 2020-11-21
        • 1970-01-01
        相关资源
        最近更新 更多