首页 文章

PHP simplexml xpath在包含制表符分隔文本的ELEMENT中搜索值?

提问于
浏览
0

How to do a PHP simplexml xpath search for text value in a tab delimited ELEMENT and returning text from that same element at a different offset from where the search text offset?

假设我希望找到包含值为'2'的DATA元素并返回LongValue'Academy' .

xml文档采用以下格式

<METADATA Resource="Property" Lookup="Area"> 
    <COLUMNS>->fieldname *(->fieldname)-></COLUMNS>
    *(<DATA>->fielddata *(->fielddata)-></DATA>) 
    </METADATA>

   Note: ignore spaces
         *()  means 1 or more
         -> is tab chr(9)

在下面的示例中,COLUMNS元素包含三个列名称(LongValue,ShortValue,Value),它们可以按任何顺序排列 .

每个DATA元素都有3个相应的制表符分隔文本值,例如下面的第一个DATA元素包含

LongVlaue = 'Salado'  
    ShortValue = 'Sal' 
    Value = '5'

这是XML文档

<METADATA Resource="Property" Lookup="Area">
<COLUMNS>   LongValue   ShortValue  Value   </COLUMNS>
<DATA>  Salado  Sal 5   </DATA>
<DATA>  Academy Aca 2   </DATA>
<DATA>  Rogers  Rog 1   </DATA>
<DATA>  Bartlett    Bar 4   </DATA>
</METADATA>

注意:COLUMNS和DATA元素的文本选项卡分为3列,每列以一个选项卡开头,后跟文本,最后一个最后一个选项卡

这就是我的想法:

1.)在尝试从DATA元素中找到相应的文本之前,最好从COLUMNS元素中找到名为“Value”的列的偏移量,因为“Value”列可以按任何顺序排列,但DATA元素中的文本将是以该顺序 .

2.)在“值”列中搜索包含文本的DATA元素,并从“LongValue”返回文本 .

下面是一个xpath搜索的示例,其中一些有效但有缺陷,因为它没有考虑COLUMNS元素中Value列的偏移量,因此它可以正确地找到“Value”列中相应的(正确的)位置 . 数据元素 .

这是一个代码片段:

$xml_text = ‘the xml document above’;
$xml = simplexml_load_string($xml_text); //load the xml document
$resource = 'Property'; //value for the Resource attribute METADATA.
$lookup = 'Area'; //value for the Lookup attribute in METADATA
$value = '2'; //the needle we are looking for

$find = "\t" . $value . "\t";
/* 
 adding tabs before and after the $value may be flawed, although each 
 column starts with a tab followed by text, only the last column has 
 the an extra tab. Not sure this would work properly if the column 
 was in the middle, or if the ELEMENT happened to have multiple $value 
 in the same element. */

   /* 
     Search for a specific METADATA element with matching 
     Resource and Lookup attributes */


$node = $this->xml->xpath(
             "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']"
            ."/DATA[contains(., '{$find}')]"
        ); 

    $x = explode("\t", (string) trim($node[0])); //convert the tab delimited 
                                                 //string to an array

    echo print_r($x,true); //this shows what the array would look like, 
                           //with out the trim there would be empty 
                           //first and last array elements

Array
(
    [0] => Academy
    [1] => Aca
    [2] => 2
)


    $LongValue = $x[0]; //assuming the LongValue is in the first column

    echo $LongValue; //this shows the LongValue retuned
    Academy

谢谢你的帮助!

更新...发布后,想出了这个......

//get index of 'Values' column from COLUMNS element
$node = $this->xml->xpath(
             "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']"
            ."/COLUMNS");
if($node) {

    //array of column names
    $columns = explode("\t", strtolower((string) trim($node[0]))); 

    $long_value_index = array_search('longvalue', $columns);

} else {
    echo 'not found';
    exit;
}

现在使用$ index,可以从正确的偏移量返回LongValue

$LongValue = $x[$long_value_index];

有什么想法吗

1 回答

  • 1

    你已经很远了,你已经很好地分析了你需要处理的数据 . 另外你怎么说你想解析数据对我来说非常好 . 唯一可能会有所改善的是你要注意不要一次做太多 .

    一种方法是将问题分成较小的问题 . 我将向您展示如何将代码放入多个函数和方法中 . 但是让我们从单个函数开始,这是逐步进行的,因此您可以尝试按照示例来构建它 .

    在PHP中分离问题的一种方法是使用函数 . 例如,编写一个函数来搜索XML文档,这使得代码看起来更好,更具说服力:

    /**
     * search metadata element
     *
     *
     * @param SimpleXMLElement $xml
     * @param string           $resource metadata attribute
     * @param string           $lookup   metadata attribute
     * @param string           $value    search value
     *
     * @return SimpleXMLElement
     */
    function metadata_search(SimpleXMLElement $xml, $resource, $lookup, $value) {
    
        $xpath = "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']"
                ."/DATA[contains(., '{$find}')]";
    
        list($element)= $xml->xpath($xpath);
    
        return $element;
    }
    

    所以现在您可以轻松搜索文档,命名和记录参数 . 所需要的就是调用函数并获取返回值:

    $data = metadata_search($xml, 'Property', 'Area', 2);
    

    这可能不是一个完美的功能,但它已经是一个例子 . 在函数旁边,您还可以创建对象 . 对象是具有自己的上下文的函数 . 这就是为什么那些函数被称为方法然后,它们属于对象 . 就像SimpleXMLElement的 xpath() 方法一样 .

    如果看到上面的函数,则第一个参数是 $xml 对象 . 然后执行xpath方法 . 最后,这个函数真正做的是根据输入变量创建和运行xpath查询 .

    如果我们可以将该函数直接引入 $xml 对象,我们就不需要再将它作为第一个参数传递 . 这是下一步,它通过扩展 SimpleXMLElement 来工作 . 我们只添加一个执行搜索的新方法,该方法与上面几乎完全相同 . 我们也从 SimpleXMLElement 延伸,这意味着我们创建了它的子类型:这就是它已经加上你添加的新方法:

    class MetadataElement extends SimpleXMLElement
    {
        /**
         * @param string           $resource metadata attribute
         * @param string           $lookup   metadata attribute
         * @param string           $value    search value
         *
         * @return SimpleXMLElement
         */
        public function search($resource, $lookup, $value) {
            $xpath = "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']"
                ."/DATA[contains(., '{$value}')]";
    
            list($element)= $this->xpath($xpath);
    
            return $element;
        }
    }
    

    为了实现这一点,我们需要在加载XML字符串时提供此类的名称 . 然后可以直接调用搜索方法:

    $xml  = simplexml_load_string($xmlString, 'MetadataElement');
    $data = $xml->search('Property', 'Area', 2);
    

    瞧,现在搜索的是SimpleXMLElement!

    但是这个怎么办 $data ?它只是一个XML元素,它仍然包含选项卡 .

    更糟糕的是,上下文丢失了:这属于哪个元数据列?那是你的问题 . 所以我们接下来要解决这个问题 - 但是如何解决?

    老实说,有很多方法可以做到这一点 . 我有一个想法是基于元数据元素从XML中创建一个表对象:

    list($metadata) = $xml->xpath('//METADATA[1]');
    $csv = new CsvTable($metadata);
    echo $csv;
    

    即使有很好的调试输出:

    +---------+----------+-----+
    |LongValue|ShortValue|Value|
    +---------+----------+-----+
    |Salado   |Sal       |5    |
    +---------+----------+-----+
    |Academ   |Aca       |2    |
    +---------+----------+-----+
    |Rogers   |Rog       |1    |
    +---------+----------+-----+
    |Bartlett |Bar       |4    |
    +---------+----------+-----+
    

    但是,如果您可能不熟悉编程对象,那么这在某种程度上是很有用的,因此构建一个完整的表模型可能有点多 .

    因此,我有了这样的想法:为什么不继续使用您已经使用的XML对象并在其中更改XML以使其以更好的格式用于您的目的 . 从:

    <METADATA Resource="Property" Lookup="Area">
      <COLUMNS>   LongValue   ShortValue  Value   </COLUMNS>
      <DATA>  Salado  Sal 5   </DATA>
    

    至:

    <METADATA Resource="Property" Lookup="Area" transformed="1">
        <COLUMNS>   LongValue   ShortValue  Value   </COLUMNS>
        <DATA>
            <LongValue>Salado</LongValue><ShortValue>Sal</ShortValue><Value>5</Value>
        </DATA>
    

    这样不仅可以搜索特定的列名,还可以搜索在数据元素中查找其他值 . 如果搜索返回 $data 元素:

    $xml  = simplexml_load_string($xmlString, 'MetadataElement');
    $data = $xml->search('Property', 'Area', 5);
    echo $data->Value;     # 5
    echo $data->LongValue; # Salado
    

    如果我们在metadata元素中留下一个额外的属性,我们可以在搜索时转换这些元素 . 如果找到一些数据并且元素尚未转换,则将转换它 .

    因为我们都在搜索方法中执行此操作,所以使用搜索方法的代码不能改变很多(如果不是甚至根本没有 - 取决于您的详细需求,我可能没有完全掌握这些,但我认为你得到这个想法) . 因此,让's put this to work. Because we don' t想要一次完成所有这些,我们创建了多个新方法:

    • 转换元数据元素

    • 在原始元素内搜索(这个代码我们已经,我们只是移动它)

    在此过程中,我们还将创建我们认为有用的方法,您会注意到这也是您已经编写的部分代码(如在search()中),它现在只是放在 $xml 对象中 - 它更自然地属于它 .

    最后,这些新方法将在现有的 search() 方法中放在一起 .

    首先,我们创建一个帮助器方法来将此选项卡行解析为数组 . 它基本上就是你的代码,你不需要在 trim 前面输入字符串,这是唯一的区别 . 因为此功能仅在内部需要,我们将其设为私有:

    private function asExplodedString() {
        return explode("\t", trim($this));
    }
    

    它的名字很清楚它的作用 . 它返回了自己的制表符分解数组 . 如果你还记得,我们在 $xml 里面,所以现在每个xml-element都有这个方法 . 如果您还没有完全理解这一点,那么继续,您可以在下面看到它是如何工作的,我们只添加一个方法作为帮助:

    public function getParent() {
        list($parent) = $this->xpath('..') + array(0 => NULL);
        return $parent;
    }
    

    此函数允许我们检索元素的父元素 . 这很有用,因为如果我们找到一个数据元素,我们想要转换作为父元素的元数据元素 . 而且因为这个功能很常用,所以我选择公开它 . 所以它也可以在外部代码中使用 . 它解决了一个常见问题,因此不像爆炸方法那样具有特定性质 .

    所以现在我们想要转换元数据元素 . 虽然上面的这两个辅助方法需要更多的代码行,但是由于这些事情并不复杂 .

    我们假设调用此方法的元素是元数据元素 . 我们不会在此处添加检查以保持代码较小 . 由于这是一个私有函数,我们甚至不需要检查:如果在错误的元素上调用此方法,则错误已在类本身内部完成 - 而不是来自外部代码 . 这也是我在这里使用私有方法的一个很好的例子,它更加具体 .

    所以我们现在使用元数据元素实际上非常简单:我们在里面获取列元素,爆炸列名,然后我们遍历每个数据元素,同样爆炸数据,然后只清空数据元素将列命名的子项添加到它 . 最后,我们添加一个属性来将元素标记为已转换:

    private function transform() {
        $columns = $this->COLUMNS->asExplodedString();
    
        foreach ($this->DATA as $data) {
            $values  = $data->asExplodedString();
            $data[0] = ''; # set the string of the element (make <DATA></DATA> empty)
            foreach ($columns as $index => $name) {
                $data->addChild($name, $values[$index]);
            }
        }
    
        $this['transformed'] = 1;
    }
    

    好的 . 现在给出了什么?我们来试试吧 . 为此,我们修改现有的搜索功能以返回转换的数据元素 - 通过添加一行代码:

    public function search($resource, $lookup, $value) {
        $xpath = "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']"
            . "/DATA[contains(., '{$value}')]";
    
        list($element) = $this->xpath($xpath);
    
        $element->getParent()->transform();
        ###################################
    
        return $element;
    }
    

    然后我们将其输出为XML:

    $data = $xml->search('Property', 'Area', 2);
    echo $data->asXML();
    

    现在这给出了以下输出(美化,它通常在一行上):

    <DATA>
      <LongValue>Academ</LongValue>
      <ShortValue>Aca</ShortValue>
      <Value>2</Value>
    </DATA>
    

    我们还要检查是否设置了新属性,并且还转换了该元数据表/块的所有其他数据元素:

    echo $data->getParent()->asXML();
    

    而输出(美化)也是:

    <METADATA Resource="Property" Lookup="Area" transformed="1">
      <COLUMNS> LongValue   ShortValue  Value   </COLUMNS>
      <DATA>
        <LongValue>Salado</LongValue>
        <ShortValue>Sal</ShortValue>
        <Value>5</Value>
      </DATA>
      ...
    

    这表明代码按预期工作 . 这可能已经解决了您的问题 . 例如 . 如果您始终搜索数字而其他列不包含数字,则您只需搜索每个元数据块一个 . 但是可能不是,因此需要更改搜索功能以在内部执行正确的搜索和转换 .

    这次我们再次使用 $this 将方法放在具体的XML元素上 . 两个新的方法:一个是根据它的属性获取元数据元素:

    private function getMetadata($resource, $lookup) {
        $xpath = "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']";
        list($metadata) = $this->xpath($xpath);
        return $metadata;
    }
    

    一个用于搜索元数据元素的特定列:

    private function searchColumn($column, $value) {
        return $this->xpath("DATA[{$column}[contains(., '{$value}')]]");
    }
    

    然后在主搜索方法中使用这两种方法 . 首先通过其属性查找元数据元素,稍微改变一下 . 然后将检查是否需要转换,然后通过值列进行搜索:

    public function search($resource, $lookup, $value)
    {
        $metadata = $this->getMetadata($resource, $lookup);
        if (!$metadata['transformed']) {
            $metadata->transform();
        }
    
        list($element) = $metadata->searchColumn('Value', $value);
    
        return $element;
    }
    

    现在,新的搜索方式终于完成了 . 它现在只搜索右列,转换将在飞:

    $xml = simplexml_load_string($xmlString, 'MetadataElement');
    $data = $xml->search('Property', 'Area', 2);
    echo $data->LongValue, "\n"; # Academ
    

    现在看起来不错,看起来好像很容易使用!所有的复杂性都进入了MetadataElement . 它一眼就看起来如何?

    /**
     * MetadataElement - Example for extending SimpleXMLElement
     *
     * @link http://stackoverflow.com/q/16281205/367456
     */
    class MetadataElement extends SimpleXMLElement
    {
        /**
         * @param string $resource metadata attribute
         * @param string $lookup   metadata attribute
         * @param string $value    search value
         *
         * @return SimpleXMLElement
         */
        public function search($resource, $lookup, $value)
        {
            $metadata = $this->getMetadata($resource, $lookup);
            if (!$metadata['transformed']) {
                $metadata->transform();
            }
    
            list($element) = $metadata->searchColumn('Value', $value);
    
            return $element;
        }
    
        private function getMetadata($resource, $lookup) {
            $xpath = "//METADATA[@Resource='{$resource}' and @Lookup='{$lookup}']";
            list($metadata) = $this->xpath($xpath);
            return $metadata;
        }
    
        private function searchColumn($column, $value) {
            return $this->xpath("DATA[{$column}[contains(., '{$value}')]]");
        }
    
        private function asExplodedString() {
            return explode("\t", trim($this));
        }
    
        public function getParent() {
            list($parent) = $this->xpath('..') + array(0 => NULL);
            return $parent;
        }
    
        private function transform() {
            $columns = $this->COLUMNS->asExplodedString();
    
            foreach ($this->DATA as $data) {
                $values  = $data->asExplodedString();
                $data[0] = ''; # set the string of the element (make <DATA></DATA> empty)
                foreach ($columns as $index => $name) {
                    $data->addChild($name, $values[$index]);
                }
            }
    
            $this['transformed'] = 1;
        }
    }
    

    也不错 . 许多小方法只需要一些代码,即(rel . )易于遵循!

    所以我希望这能带来一些灵感,我知道这是一个很难读的文字 . 玩得开心!

相关问题