0

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

假设我希望找到包含“2”值的 DATA 元素并返回 LongValue“学院”。

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 元素包含三个列名(LongValueShortValueValue),它们可以是任何顺序。

每个 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.) 在“Value”列中搜索包含文本的 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

谢谢你的帮助!

更新...发布后,想出了这个...</p>

//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]; 

有什么想法吗

4

1 回答 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);

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

如果你看到上面的函数,第一个参数就是$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

如果我们为元数据元素保留一个附加属性,我们可以在搜索时转换这些元素。如果找到一些数据并且元素尚未转换,它将被转换。

因为我们都是在 search 方法里面做的,所以使用 search 方法的代码应该不会有太大的变化(如果甚至没有 - 取决于你的详细需求,我可能没有完全掌握这些,但我认为你明白了)。所以让我们开始工作吧。因为我们不想一次全部完成,所以我们创建了多个新方法来:

  1. 转换元数据元素
  2. 在原始元素中搜索(我们已经有了这段代码,我们只是移动它)

在此过程中,我们还将创建我们认为有用的方法,您会注意到这也是您已经编写的部分代码(如在 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;
}

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

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

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

所以我们现在对元数据元素所做的实际上非常简单:我们获取里面的列元素,分解列名,然后我们遍历每个数据元素,也分解数据,然后清空数据元素将列命名的子项添加到它。最后,我们添加一个属性来将元素标记为已转换:

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}')]]");
}

然后在主搜索方法中使用这两种方法。首先通过其属性查找元数据元素将对其进行轻微更改。然后将检查是否需要转换,然后按 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;
    }
}

也不算太坏。许多只有几行代码的小方法,即(相对)易于遵循!

所以我希望这能给你一些启发,我知道这是一个相当值得阅读的文本。玩得开心!

于 2013-05-04T08:30:02.850 回答