我的目标是将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);
}
}
为了它的价值,我尝试将CSV转换为sqlite数据库,但整体速度并没有显着差异。
我猜测重要部分是在循环内部,因为我们创建/添加/更改了一个越来越大的XML文件的DOM。
是否有节省执行时间的想法?我应该研究多线程吗?我的电脑确实有一个四核处理器,只使用一个。我应该更改图书馆/语言吗?我只是在抛出想法,而且我对任何建议持开放态度,因为目前我无法真正依赖这个脚本来处理这些大文件。
答案 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标记,具体取决于您要分配的内存量)。