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

时间:2013-04-29 14:28:14

标签: php xpath simplexml

如何在制表符分隔的ELEMENT中搜索文本值的PHP simplexml xpath,并从与搜索文本偏移量不同的偏移处返回同一元素的文本?

假设我希望找到包含值为'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”的相应(正确)位置DATA元素中的列。

这是一段代码片段:

$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 个答案:

答案 0 :(得分: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);

Voila,现在搜索 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元素中留下一个额外的属性,我们可以在搜索时转换这些元素。如果找到一些数据并且元素尚未转换,则会转换它。

因为我们都在里面搜索方法,所以使用搜索方法的代码不能改变很多(如果不是甚至根本没有 - 取决于你的详细需求,我可能会没有完全掌握这些,但我认为你明白了)。所以,让我们开始工作吧。因为我们不想一次性完成所有这些,所以我们创建了多种新方法:

  1. 转换元数据元素
  2. 在原始元素内搜索(我们已经使用此代码,我们只是移动它)
  3. 在此过程中我们还将创建我们认为有用的方法,您会注意到这也是您已编写的部分代码(如在search()中),它现在只是放在里面 $xml对象 - 它更自然地属于它。

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

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

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

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

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

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

    所以现在我们要转换元数据元素。虽然上面的这两个辅助方法需要更多的代码行,但多亏了这些东西并不复杂。

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

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

    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。)易于遵循!

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