【问题标题】:Modification and replacement of nodes in xml filexml文件中节点的修改和替换
【发布时间】:2019-11-12 12:04:31
【问题描述】:

我每天都会收到一个 .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 的汇总值,并返回 .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>

感谢任何帮助。谢谢。

【问题讨论】:

    标签: php xml simplexml


    【解决方案1】:

    使用 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();
    

    【讨论】:

    • 有趣的是使用 DOMDocument 和 SimpleXML 有多大的不同。
    • 不同观点:SimpleXML 是一种尝试将 XML 映射到类/对象结构的抽象。它将数据视为代码。 DOM 本身是表示数据结构的对象树。
    • 我更多的是在讨论 API 之间的差异,它们都使用 DOM 来解释如何访问内容。我知道 DOMDocument 更强大,更精确地说明了它是如何实现这一点的,但是 SimpleXML(恕我直言)像这个例子一样在纯数据处理中占有一席之地。
    【解决方案2】:

    下面的代码可能会回答你的问题。

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

    然后您可以使用array_uniquearray_diff_assocarray_keysarray_intersect 找到重复的节点索引,另请参阅此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>
    

    【讨论】:

    • 嗯,没有错误,但没有回显值,var_dump 给出 string(39) " "
    • 好的,解决了,这是我编辑某些部分的错误,现在我无法摆脱“未捕获的错误:调用成员函数 removeChild()”
    • @Greedy 对我来说,使用您的输入 xml 的结果很好。变量$search$nodes$duplicateIndeces$firstNode 的输出是什么?
    • 好的,我想通了,代码按预期工作。谢谢。
    【解决方案3】:

    我会为此推荐 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 版本的解决方案也是可行的,但有点冗长。

    【讨论】:

      【解决方案4】:

      这个解决方案使用 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;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2014-03-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-27
        相关资源
        最近更新 更多