xml 文件中节点的修改和替换

Modification and replacement of nodes in xml file

我每天都会收到一个 .xml 文件,但需要对元素值进行一些额外的编辑。我已经通过数据透视表在 Excel 中完成了我需要的工作,但该解决方案是不可接受的,因为这会创建新结构,我无法将其保存为 xml,我需要相同的 xml结构作为输出。由于我安装了 PHP Composer,我认为 simpleXML 库是最简单的解决方案。上传原始 .xml 文件并通过 simplexml_load_string 函数进行修改。问题是,我对这种语法不是很精通,我需要一些帮助。这是我的 .xml 文件:

<?xml version="1.0" encoding="ISO-8859-2"?>
<ZCOLL>
  <IDOC BEGIN="1">
    <EDI_DC40 SEGMENT="1">
        <DOCNUM>0000000008857855</DOCNUM>
        <SERIAL>20191025143123</SERIAL>
    </EDI_DC40>
    <Z1COLL_AGENCY SEGMENT="1">
        <GPART>0000000101</GPART>
        <EMAIL>domain@domain.com</EMAIL>                        
        <Z1COLL_HEADER SEGMENT="1">
            <VKONT>200000541301</VKONT>
            <GPART>1000447089</GPART>
            <VKONA>22611402001</VKONA>
            <INKNO>00000000000101953558</INKNO>
            <INKBP>0000000101</INKBP>
            <INKDAT>20191025</INKDAT>
            <INKENDAT>20200123</INKENDAT>
            <BANKRUPTDAT>00000000</BANKRUPTDAT>
            <CLOSED/>
            <UPDATED_DATE>00000000</UPDATED_DATE>
            <COLLREFNR>200005413019-024-6</COLLREFNR>
            <NAME_ORG1>SOME OTHER NAME</NAME_ORG1>
            <LEG_CITY1>SOME OTHER CITY</LEG_CITY1>
            <LEG_POST_CODE1>105677</LEG_POST_CODE1>
            <LEG_STREET>ADDRESS 2</LEG_STREET>
            <LEG_HOUSE_NUM1/>
            <BU_SORT2>02226696981</BU_SORT2>
            <MAIL_CITY1>CITY 1</MAIL_CITY1>
            <MAIL_POST_CODE1>35220</MAIL_POST_CODE1>
            <MAIL_STREET>MAIL STREET 1</MAIL_STREET>
            <MAIL_HOUSE_NUM1/>
               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>000210625857</OPBEL>
                   <XBLNR>0000198653579124</XBLNR>
                   <FAEDN>20190916</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>         80.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>         80.00</OPENAMNT>
                   <INVAMNT>         80.00</INVAMNT>
                   <WAERS>HRK</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>

               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>000210625857</OPBEL>
                   <XBLNR>0000198653579124</XBLNR>
                   <FAEDN>20191016</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>         80.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>         80.00</OPENAMNT>
                   <INVAMNT>         80.00</INVAMNT>
                   <WAERS>HRK</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>

               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>019183828875</OPBEL>
                   <XBLNR>2261140200119081</XBLNR>
                   <FAEDN>20190816</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>        159.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>        159.00</OPENAMNT>
                   <INVAMNT>        159.00</INVAMNT>
                   <WAERS>CURRENCY</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>

               <MSISDNS>
                   <MSISDN>381653490012</MSISDN>
               </MSISDNS>
               <MCD_DATA>
                   <MCD_MONTHS/>
                   <MCD_AMOUNT/>
               </MCD_DATA>
            <PHONE_NUM/>
        </Z1COLL_HEADER>            
     </Z1COLL_AGENCY>
  </IDOC>
</ZCOLL>

我需要的是,如果 OPBEL 和 XBLNR 元素值相同,则删除该节点并将其替换为 OPBEL 和 XBLNR 的单个重复值以及 INITAMNT、PAYAMNT、WRTOFFAMNT、OPENAMNT 和 INVAMNT 的汇总值,以及 return .xml 具有相同结构的文件。

这是我的代码,我卡在一半了:

if (isset($_POST['submit'])) {
    //echo "<pre>";
    //print_r($_FILES['file']['name']);
    //echo "</pre>";
    if(isset($_FILES['file']['name'])) {
        $get = file_get_contents($_FILES['file']['tmp_name']);
        $arr = simplexml_load_string($get);

    foreach ($arr->IDOC->Z1COLL_AGENCY->Z1COLL_HEADER as $element) {
        $delete_node = array();
        foreach($element->Z1COLL_ITEM as $item) {   
            $doubles_XBLNR = array_count_values($item->XBLNR);
            $doubles_OPBEL = array_count_values($item->OPBEL);
            $doubles_no_XBLNR = count($doubles_XBLNR);
            $doubles_no_OPBEL = count($doubles_OPBEL);
                if (($doubles_no_XBLNR > 2) && ($doubles_no_OPBEL > 2)){
                    $item->INITAMNT += $item->INITAMNT;
                    $item->PAYAMNT += $item->PAYAMNT;
                    $item->WRTOFFAMNT += $item->WRTOFFAMNT;
                    $$item->OPENAMNT += $item->OPENAMNT;
                    $item->INVAMNT += $item->INVAMNT;
                }                   
        }
        echo "<br />";  
    }

这是需要的 xml 输出:

<?xml version="1.0" encoding="ISO-8859-2"?>
<ZCOLL>
  <IDOC BEGIN="1">
    <EDI_DC40 SEGMENT="1">
        <DOCNUM>0000000008857855</DOCNUM>
        <SERIAL>20191025143123</SERIAL>
    </EDI_DC40>
    <Z1COLL_AGENCY SEGMENT="1">
        <GPART>0000000101</GPART>
        <EMAIL>domain@domain.com</EMAIL>                    
        <Z1COLL_HEADER SEGMENT="1">
            <VKONT>200000541301</VKONT>
            <GPART>1000447089</GPART>
            <VKONA>22611402001</VKONA>
            <INKNO>00000000000101953558</INKNO>
            <INKBP>0000000101</INKBP>
            <INKDAT>20191025</INKDAT>
            <INKENDAT>20200123</INKENDAT>
            <BANKRUPTDAT>00000000</BANKRUPTDAT>
            <CLOSED/>
            <UPDATED_DATE>00000000</UPDATED_DATE>
            <COLLREFNR>200005413019-024-6</COLLREFNR>
            <NAME_ORG1>SOME OTHER NAME</NAME_ORG1>
            <LEG_CITY1>SOME OTHER CITY</LEG_CITY1>
            <LEG_POST_CODE1>105677</LEG_POST_CODE1>
            <LEG_STREET>ADDRESS 2</LEG_STREET>
            <LEG_HOUSE_NUM1/>
            <BU_SORT2>02226696981</BU_SORT2>
            <MAIL_CITY1>CITY 1</MAIL_CITY1>
            <MAIL_POST_CODE1>35220</MAIL_POST_CODE1>
            <MAIL_STREET>MAIL STREET 1</MAIL_STREET>
            <MAIL_HOUSE_NUM1/>
               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>000210625857</OPBEL>
                   <XBLNR>0000198653579124</XBLNR>
                   <FAEDN>20190916</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>         160.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>         160.00</OPENAMNT>
                   <INVAMNT>         160.00</INVAMNT>
                   <WAERS>CURRENCY</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>

               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>019183828875</OPBEL>
                   <XBLNR>2261140200119081</XBLNR>
                   <FAEDN>20190816</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>        159.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>        159.00</OPENAMNT>
                   <INVAMNT>        159.00</INVAMNT>
                   <WAERS>CURRENCY</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>
               <MSISDNS>
                   <MSISDN>381653490012</MSISDN>
               </MSISDNS>
               <MCD_DATA>
                   <MCD_MONTHS/>
                   <MCD_AMOUNT/>
               </MCD_DATA>
               <PHONE_NUM/>
         </Z1COLL_HEADER>           
    </Z1COLL_AGENCY>
  </IDOC>
 </ZCOLL>

感谢任何帮助。谢谢。

希望下面的代码能回答您的问题。

您也可以使用 domxpath 来解析 xml。我会创建一个搜索数组,其中包含 OPBELXBLNR 值的出现索引。

然后您可以使用array_unique, array_diff_assoc, array_keys and array_intersect, also see this answer找到重复的节点索引。

现在,您可以删除重复的节点并插入一个具有聚合值的新节点。这是代码,其中 data/input.xml 是您提供的 xml 文件。

$xml = file_get_contents(__DIR__ . '/data/input.xml');
$dom = new \DOMDocument('1.0', 'UTF-8');
@$dom->loadXML($xml);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//Z1COLL_ITEM');

$search = [];
$index = 0;

foreach ($nodes as $node) {
    $OPBEL = $xpath->query('OPBEL', $node)->item(0)->nodeValue;
    $XBLNR = $xpath->query('XBLNR', $node)->item(0)->nodeValue;

    $search[$index] = $OPBEL . $XBLNR;
    $index++;
}

// Unique values
$unique = array_unique($search);

// Duplicates
$duplicates = array_diff_assoc($search, $unique);

// Get duplicate keys
$duplicateIndeces = array_keys(array_intersect($search, $duplicates));

$aggregate = [];

$firstNode = $xpath->query('//Z1COLL_ITEM')->item($duplicateIndeces[0]);

// Iterate through the duplicated nodes
foreach (array_slice($duplicateIndeces, 1) as $duplicateIndex) {
    $node = $xpath->query('//Z1COLL_ITEM')->item($duplicateIndex);

    // Update the desired values for the first occurrence
    $xpath->query('INITAMNT', $firstNode)->item(0)->nodeValue += $xpath->query('INITAMNT', $node)->item(0)->nodeValue;
    $xpath->query('PAYAMNT', $firstNode)->item(0)->nodeValue += $xpath->query('PAYAMNT', $node)->item(0)->nodeValue;
    $xpath->query('WRTOFFAMNT', $firstNode)->item(0)->nodeValue += $xpath->query('WRTOFFAMNT', $node)->item(0)->nodeValue;
    $xpath->query('OPENAMNT', $firstNode)->item(0)->nodeValue += $xpath->query('OPENAMNT', $node)->item(0)->nodeValue;
    $xpath->query('INVAMNT', $firstNode)->item(0)->nodeValue += $xpath->query('INVAMNT', $node)->item(0)->nodeValue;

    // Remove the duplicated node
    $node->parentNode->removeChild($node);
}

echo $dom->saveXML();

生成的输出如下:

<?xml version="1.0" encoding="ISO-8859-2"?>
<ZCOLL>
  <IDOC BEGIN="1">
    <EDI_DC40 SEGMENT="1">
        <DOCNUM>0000000008857855</DOCNUM>
        <SERIAL>20191025143123</SERIAL>
    </EDI_DC40>
    <Z1COLL_AGENCY SEGMENT="1">
        <GPART>0000000101</GPART>
        <EMAIL>domain@domain.com</EMAIL>                        
        <Z1COLL_HEADER SEGMENT="1">
            <VKONT>200000541301</VKONT>
            <GPART>1000447089</GPART>
            <VKONA>22611402001</VKONA>
            <INKNO>00000000000101953558</INKNO>
            <INKBP>0000000101</INKBP>
            <INKDAT>20191025</INKDAT>
            <INKENDAT>20200123</INKENDAT>
            <BANKRUPTDAT>00000000</BANKRUPTDAT>
            <CLOSED/>
            <UPDATED_DATE>00000000</UPDATED_DATE>
            <COLLREFNR>200005413019-024-6</COLLREFNR>
            <NAME_ORG1>SOME OTHER NAME</NAME_ORG1>
            <LEG_CITY1>SOME OTHER CITY</LEG_CITY1>
            <LEG_POST_CODE1>105677</LEG_POST_CODE1>
            <LEG_STREET>ADDRESS 2</LEG_STREET>
            <LEG_HOUSE_NUM1/>
            <BU_SORT2>02226696981</BU_SORT2>
            <MAIL_CITY1>CITY 1</MAIL_CITY1>
            <MAIL_POST_CODE1>35220</MAIL_POST_CODE1>
            <MAIL_STREET>MAIL STREET 1</MAIL_STREET>
            <MAIL_HOUSE_NUM1/>
               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>000210625857</OPBEL>
                   <XBLNR>0000198653579124</XBLNR>
                   <FAEDN>20190916</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>160</INITAMNT>
                   <PAYAMNT>0</PAYAMNT>
                   <WRTOFFAMNT>0</WRTOFFAMNT>
                   <OPENAMNT>160</OPENAMNT>
                   <INVAMNT>160</INVAMNT>
                   <WAERS>HRK</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>



               <Z1COLL_ITEM SEGMENT="1">
                   <OPBEL>019183828875</OPBEL>
                   <XBLNR>2261140200119081</XBLNR>
                   <FAEDN>20190816</FAEDN>
                   <AGDAT>20191025</AGDAT>
                   <INITAMNT>        159.00</INITAMNT>
                   <PAYAMNT>          0.00</PAYAMNT>
                   <WRTOFFAMNT>          0.00</WRTOFFAMNT>
                   <OPENAMNT>        159.00</OPENAMNT>
                   <INVAMNT>        159.00</INVAMNT>
                   <WAERS>CURRENCY</WAERS>
                   <RECINKDAT>00000000</RECINKDAT>
               </Z1COLL_ITEM>

               <MSISDNS>
                   <MSISDN>381653490012</MSISDN>
               </MSISDNS>
               <MCD_DATA>
                   <MCD_MONTHS/>
                   <MCD_AMOUNT/>
               </MCD_DATA>
            <PHONE_NUM/>
        </Z1COLL_HEADER>            
     </Z1COLL_AGENCY>
  </IDOC>
</ZCOLL>

使用 DOM 文档操作更容易,因为每个部分都由了解其上下文的节点对象表示。对于您的情况,您可以建立一个节点索引,该节点按从 OPBEL 和 XBLNR 值生成的键分组。然后合并分组的节点。

$document = new DOMDocument();
$document->loadXML(getXML());
$xpath = new DOMXPath($document);

// iterate all header elements (do not merge items from different headers)
foreach ($xpath->evaluate('//Z1COLL_HEADER ') as $header) {
    $groups = [];
    // iterate the items
    foreach ($xpath->evaluate('Z1COLL_ITEM', $header) as $item) {
        // combine keys into a single string
        $groupKey = $xpath->evaluate('concat(OPBEL, "|", XBLNR)', $item);
        if (!isset($groups[$groupKey])) {
            $groups[$groupKey] = [];
        }
        // add the current item to a group defined by the generated key
        $groups[$groupKey][] = $item;
    } 
    // now filter for groups with multiple items
    $groups = array_filter($groups, function($group) { return count($group) > 1; });

    // iterate the groups with multiple items
    foreach ($groups as $group) {
        // extract the first item node from the array
        $firstItem = array_shift($group);
        // iterate the other items of the group
        foreach ($group as $item) {
            $merges = ['INITAMNT', 'PAYAMNT', 'WRTOFFAMNT', 'OPENAMNT', 'INVAMNT'];
            // iterate the child node names to merge
            foreach ($merges as $merge) {
                // get the node of the first item
                $target = $xpath->evaluate($merge, $firstItem)->item(0);
                // get the node of the current item
                $source = $xpath->evaluate($merge, $item)->item(0);
                // if here are both
                if ($target && $source) {
                   // sum the values and format them 
                   $target->textContent = number_format(
                       $target->textContent + $source->textContent, 2
                   );
                } elseif ($source) {
                   // if the child does not exists in the first node move it over 
                   $firstItem->appendChild($source);    
                }
            }
            $item->parentNode->removeChild($item);
        }
    }
}

echo $document->saveXML();

为此我推荐 XSLT。在我看来(在 XSLT 3.0 中):

<xsl:transform version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  expand-text="yes">

<xsl:mode on-no-match="shallow-copy"/>

<xsl:template match="Z1COLL_HEADER">
  <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:apply-templates select="* except Z1COLL_ITEM"/>
    <xsl:for-each-group select="Z1COLL_ITEM" group-by="OPBEL, XBLNR" composite="yes">
       <xsl:copy-of select="current-group()[1]/(OPBEL,XBLNR, FAEDN, AGDAT)"/>
       <INITAMNT>{sum(current-group()/INITAMT)}</INITAMNT>
       <PAYAMNT>{sum(current-group()/PAYAMNT)}</PAYAMNT>
       <WRTOFFAMNT>{sum(current-group()/WRTOFFAMNT)}</WRTOFFAMNT>
       <OPENAMNT>{sum(current-group()/OPENAMNT)}</OPENAMNT>
       <INVAMNT>{sum(current-group()/INVAMNT)}</INVAMNT>
       <xsl:copy-of select="current-group()[1]/(WAERS, RECINKDAT)"/>
    </xsl:for-each-group>
  </xsl:copy>
</xsl:template>

</xsl:transform>

早期 XSLT 版本的解决方案也是可能的,但有点冗长。

此解决方案使用 SimpleXML 并基本上跟踪到目前为止找到的元素,然后如果它找到另一个类似的记录,只需将值添加到找到的第一个记录。然后它使用(在本例中)unset($entry[0]); 从原始文档中删除重复条目...

$xml = simplexml_load_file($_FILES['file']['tmp_name']);
$existing = [];

foreach ( $xml->xpath("//Z1COLL_ITEM") as $entry )  {
    $index = $entry->OPBEL."#".$entry->XBLNR;
    if ( isset ($existing[$index]) )    {
        $existing[$index]->INITAMNT += $entry->INITAMNT;
        $existing[$index]->PAYAMNT += $entry->PAYAMNT;
        $existing[$index]->WRTOFFAMNT += $entry->WRTOFFAMNT;
        $existing[$index]->OPENAMNT += $entry->OPENAMNT;
        $existing[$index]->INVAMNT += $entry->INVAMNT;
        unset($entry[0]);
    }
    else {
        $existing[$index] = $entry;
    }
}