使用XMLReader读取大于1GB的文件时出现问题

4
XMLReader能处理的最大文件大小是多少?
我正在尝试处理一个大约3GB的XML源。脚本运行良好,在运行后成功加载到数据库中,没有PHP错误。
对于较小的测试源(1GB及以下),脚本也能完美地运行。然而,在处理较大的源时,脚本在读取了大约1GB的XML文件后停止读取,但其余部分继续运行。
有人遇到过类似的问题吗?如果有,你是如何解决的?
谢谢您的帮助。

2
您确定没有任何PHP错误吗?在您所能观察到的范围内,究竟是什么因素导致工作与否?“脚本”看起来是怎么样的,除了遍历XML之外还有什么其他操作吗? - salathe
在伪代码中,脚本大致如下:$this->downloadFeed(); try{ $this->writeXMLFeedToCSV(); }catch(e){ //处理异常 } $this->uploadCSVToDatabaseTable();如果由于PHP错误而导致脚本失败,则不会上传到数据库。但它目前可以。XML格式也正确,因为如ircmaxell所建议的那样,当脚本被分解时,它可以正常工作。然而,这个过程很繁琐,希望找到解决方案。对不起,由于信息的特性,我无权分享脚本。 - A boy named Su
你在测试时使用哪个 a) 操作系统 b) 文件系统 c) PHP 版本 d) PHP 构建版本? - VolkerK
6个回答

2

我最近也遇到了同样的问题,想分享一下我的经验。

问题似乎出在PHP编译方式上,无论是是否支持64位文件大小/偏移量进行编译。

使用32位,您只能寻址4GB的数据。您可以在这里找到一个有点混乱但好的解释:http://blog.mayflower.de/archives/131-Handling-large-files-without-PHP.html

我必须使用Perl实用程序xml_split来拆分文件,您可以在此处找到它:http://search.cpan.org/~mirod/XML-Twig/tools/xml_split/xml_split

我用它将巨大的XML文件拆分成可管理的块。该工具的好处是它可以将XML文件分割为整个元素。不幸的是,它的速度不太快。

我只需要做一次就行了,并且它适合我的需求,但我不建议反复使用它。拆分后,我在大小约为1GB的较小文件上使用了XMLReader。


1

将文件拆分肯定会有所帮助。其他尝试的方法包括...

  1. 在php.ini中调整memory_limit变量。http://php.net/manual/en/ini.core.php
  2. 使用SAX重新编写解析器 -- http://php.net/manual/en/book.xml.php 。这是一种流导向的解析器,不需要解析整个树形结构。内存利用率更高,但编程稍微复杂一些。

取决于您的操作系统,RAM块的分配可能也会有2GB的限制。如果您正在运行32位操作系统,则很可能出现此问题。


XMLReader 接口旨在像 SAX 解析器一样按顺序处理大型文档,即它不会(必须)将整个文档加载到内存中。 - VolkerK
谢谢。我已经调整了内部存储。VolkerK 也是正确的。XMLReader 的读取方式与 SAX 解析器类似。如果其他方法都失败了,我会尝试使用 SAX,但我宁愿不重写脚本。 - A boy named Su

1

需要注意的是,PHP通常有一个最大文件大小限制。PHP不允许使用无符号整数或长整数,这意味着您的整数被限制在2^31(或64位系统的2^63)以内。这很重要,因为PHP使用整数作为文件指针(您在读取文件时的位置),这意味着它无法处理大于2^31字节的文件。

然而,这应该超过1GB。我遇到了两个GB的问题(如预期的那样,因为2^31大约是20亿)。


0

你有没有遇到任何错误?

libxml_use_internal_errors(true);
libxml_clear_errors();

// your parser stuff here....    
$r = new XMLReader(...);
// ....


foreach( libxml_get_errors() as $err ) {
   printf(". %d %s\n", $err->code, $err->message);
}

当解析器提前停止时?


不,没有获取到任何东西。我正在组合一个独立的脚本副本,可能会更好地解决问题,但我非常确定这不是XML或PHP脚本本身的问题。只要文件小于1GB,它就可以按预期运行,没有任何问题。即使更大,它也可以正常运行,只是不能读取所有的XML。感谢您的建议。 - A boy named Su
只是为了确认一下:libxml_get_errors() 的问题并不意味着 XML 或 PHP 脚本本身有问题。我认为 libxml 可能会抱怨文件查找失败或文本节点超过允许的最大值(默认为10MB)之类的问题。如果你在没有 libxml_get_errors() 返回错误的情况下遇到了问题,那么这个想法就行不通了 :( - VolkerK
我知道你的意思。我不敏感 - 我没有防御性。如果我表现得这样,对不起。 - A boy named Su

0

使用WindowsXP,NTFS文件系统和php 5.3.2,这个测试脚本没有任何问题。

<?php
define('SOURCEPATH', 'd:/test.xml');

if ( 0 ) {
  build();
}
else {
  echo 'filesize: ', number_format(filesize(SOURCEPATH)), "\n";
  timing('read');
}

function timing($fn) {
  $start = new DateTime();
  echo 'start: ', $start->format('Y-m-d H:i:s'), "\n";
  $fn();
  $end = new DateTime();
  echo 'end: ', $start->format('Y-m-d H:i:s'), "\n";
  echo 'diff: ', $end->diff($start)->format('%I:%S'), "\n";
}

function read() {
  $cnt = 0;
  $r = new XMLReader;
  $r->open(SOURCEPATH);
  while( $r->read() ) {
    if ( XMLReader::ELEMENT === $r->nodeType ) {
      if ( 0===++$cnt%500000 ) {
        echo '.';
      }
    }
  }
  echo "\n#elements: ", $cnt, "\n";
}

function build() {
  $fp = fopen(SOURCEPATH, 'wb');

  $s = '<catalogue>';
  //for($i = 0; $i < 500000; $i++) {
  for($i = 0; $i < 60000000; $i++) {
    $s .= sprintf('<item>%010d</item>', $i);
    if ( 0===$i%100000 ) {
      fwrite($fp, $s);
      $s = '';
      echo $i/100000, ' ';
    }
  }

  $s .= '</catalogue>';
  fwrite($fp, $s);
  flush($fp);
  fclose($fp);
}

输出:

filesize: 1,380,000,023
start: 2010-08-07 09:43:31
........................................................................................................................
#elements: 60000001
end: 2010-08-07 09:43:31
diff: 07:31

(你可以看到,我搞砸了结束时间的输出,但我不想再运行这个脚本7分钟以上;-)

这在你的系统上也有效吗?


作为一则附注:相应的C#测试应用程序只花费了41秒,而不是7.5分钟。而且我的慢硬盘可能是这种情况下的一个限制因素。
filesize: 1.380.000.023
start: 2010-08-07 09:55:24
........................................................................................................................

#elements: 60000001

end: 2010-08-07 09:56:05
diff: 00:41

以及源代码:

using System;
using System.IO;
using System.Xml;

namespace ConsoleApplication1
{
  class SOTest
  {
    delegate void Foo();
    const string sourcepath = @"d:\test.xml";
    static void timing(Foo bar)
    {
      DateTime dtStart = DateTime.Now;
      System.Console.WriteLine("start: " + dtStart.ToString("yyyy-MM-dd HH:mm:ss"));
      bar();
      DateTime dtEnd = DateTime.Now;
      System.Console.WriteLine("end: " + dtEnd.ToString("yyyy-MM-dd HH:mm:ss"));
      TimeSpan s = dtEnd.Subtract(dtStart);
      System.Console.WriteLine("diff: {0:00}:{1:00}", s.Minutes, s.Seconds);
    }

    static void readTest()
    {
      XmlTextReader reader = new XmlTextReader(sourcepath);
      int cnt = 0;
      while (reader.Read())
      {
        if (XmlNodeType.Element == reader.NodeType)
        {
          if (0 == ++cnt % 500000)
          {
            System.Console.Write('.');
          }
        }
      }
      System.Console.WriteLine("\n#elements: " + cnt + "\n");
    }

    static void Main()
    {
      FileInfo f = new FileInfo(sourcepath);
      System.Console.WriteLine("filesize: {0:N0}", f.Length);
      timing(readTest);
      return;
    }
  }
}

0

我在解析大型文档时遇到了类似的问题。最后我使用文件系统函数将源数据分成较小的块,然后对这些较小的块进行解析...因此,如果您正在解析一堆<record>标记,请将它们作为流使用字符串函数进行解析,并且当您在缓冲区中获得完整记录时,请使用xml函数进行解析... 这很糟糕,但它非常有效(并且非常节省内存,因为您在任何时候最多只有1条记录在内存中)...


谢谢,是的,我也最终采用了这种方法。但正如你所提到的,这真的很糟糕。你是否确切知道 XML 读取器能读取的文件大小是否有上限呢? - A boy named Su
再次感谢您的建议,我找到了错误的源头,并找到了一个迄今为止有效的解决方案,我觉得您可能能够实施它。事实证明,在我的文档类型中,喂食中有一个垂直制表符(^K或字符11),这不是一个无效的字符,但对于我使用的文档类型来说是无效的。在处理喂食之前,我通过sed查找和替换运行了喂食,并且此后已经能够解析大于2GB的字段。感谢其他人的建议。 - A boy named Su

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接