我可以减少将元素添加到大型XML文件的脚本的执行时间吗?

时间:2018-02-13 10:21:49

标签: php xml performance csv simplexml

我的目标是将XML元素添加到现有XML文件(相当大,约90MB),同时解析CSV文件以了解我应该定位哪个元素。困难的部分是XML和CSV文件都很大。 CSV文件有720k行,因此我的脚本需要多天才能完成,这是不可接受的。

经过一番搜索,我找到了一种方法去除"删除"通过使用生成器代替在内存中构建720k元素数组来解析CSV文件解析的瓶颈(如果存在)。这种方式可能更好,但总的来说它仍然太慢。

以下是代码示例:

<?php 
$xml = simplexml_load_file('input/xmlfile.xml');
$csv = Utils::parseCSVWithGenerator('input/csvfile.csv', '$');
/* CSV SAMPLE
Header: id_1$id_2$id_3$id_4
l1:     521$103$490$19
Only 2 columns are necessary
*/

foreach ($csv as $key => $line) {
    var_dump('key: '.$key);
    $target = $xml->xpath('/Root/Book/Part/Part/Child[@id="'.$line['id_2'].'"]')[0];
    if (empty($target)) {
        var_dump($line['id_2']);
    } else {
        // If $target exists, $indexChild exists too, and we need to retrieve the Name element from it
        $indexChild = $xml->xpath('/Root/Child[@id="'.$line['id_3'].'"]')[0];
        $newElement = new SimpleXMLElement('<newElement id="'.$line['id_3'].'"></newElement>');
        $newElement->addChild('Name', (string) $indexChild->Name);
        Utils::simplexml_append($newElement, $target);
    }
}

class Utils {
    public static function parseCSVWithGenerator($filename, $delimiter) {
        $fp = fopen($filename, 'r');
        $csv = [];
        $header = fgetcsv($fp, 0, $delimiter);
        $key = 0;

        while( ($data = fgetcsv($fp, 0, $delimiter)) !== FALSE ) {
            $key++;
            yield $key => array_combine($header, $data);
        }
        fclose($fp);
    }

    public static function simplexml_append(SimpleXMLElement $child, SimpleXMLElement $parent) {
        $parent_dom = dom_import_simplexml($parent);
        $child_dom = dom_import_simplexml($child);

        $child_dom = $parent_dom->ownerDocument->importNode($child_dom, TRUE);

        return $parent_dom->appendChild($child_dom);
    }
}

为了它的价值,我尝试将CS​​V转换为sqlite数据库,但整体速度并没有显着差异。

我猜测重要部分是在循环内部,因为我们创建/添加/更改了一个越来越大的XML文件的DOM。

是否有节省执行时间的想法?我应该研究多线程吗?我的电脑确实有一个四核处理器,只使用一个。我应该更改图书馆/语言吗?我只是在抛出想法,而且我对任何建议持开放态度,因为目前我无法真正依赖这个脚本来处理这些大文件。

1 个答案:

答案 0 :(得分:1)

对于CSV文件中的每一行,您正在构建和评估表单

的XPath表达式
/Root/Child[@id="'....'"]')[0]

第一个显而易见的低效率是你真的不想每次都编译一个新的XPath表达式;您应该使用与参数相同的编译表达式。 (我不太详细了解PHP API,我只是看一般原则。)

但即便如此,这个表达式可能需要花费时间与XML文档的大小成比例。你需要某种索引。

我会告诉你我将如何做到这一点。你可能不喜欢这个解决方案,但它可能会给你一些想法。

我会用XSLT 3.0编写它(PHP用户可以通过Saxon / C产品获得)。我会编写转换,以便它首先将CSV文件中的条目作为映射索引,然后处理XML文件中的所有记录,检查每个记录以查看CSV输入中是否有相应的条目。像这样:

<xsl:param name="csvFileName" as="xs:string"/>
<xsl:variable name="csvMap" as="map(*)">
  <xsl:map>
    <xsl:for-each select="unparsed-text-lines($csvFileName)">
      <xsl:variable name="fields" select="tokenize(., ',')"/>
      <xsl:map-entry key="$fields[1]" select="$fields"/>
    </xsl:for-each>
  </xsl:map>
</xsl:variable>

<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="/Root/Child[map:contains($csvMap, @id)]">
  <xsl:variable name="csvRecord" select="$csvMap(@id)"/>
  <xsl:copy>
    <newElement id="{@id}"
      x="{$csvRecord[2]}" y="{$csvRecord[3]}"/>
  </xsl:copy>
</xsl:template>  

当然这只是要点:没​​有看到输入文件的详细结构或所需的输出,这是我能做的最好的。

如果您更喜欢使用PHP附带的XSLT 1.0处理器,那么也可以这样做,但它会更加冗长:您必须在调用应用程序中将CSV文件转换为XML,并且您可以使用XSLT键来有效地访问它,而不是构建地图。

请注意,以这种方式进行连接可能更好,因为CSV文件是两者中较小的一个,所以地图结构就这么小了;使用XSLT 3.0,XML文件的处理可以完全流式传输,因此它可以毫不费力地超出当前的90Mb大小(流量开始变得非常重要,大约200Mb标记,具体取决于您要分配的内存量)。