【问题标题】:Manipulate a string that is 30 million characters long操作一个 3000 万个字符长的字符串
【发布时间】:2010-11-23 11:26:21
【问题描述】:

我正在从另一台服务器下载一个 CSV 文件作为来自供应商的数据馈送。

我正在使用 curl 获取文件的内容并将其保存到名为 $contents 的变量中。

我可以很好地到达那部分,但我尝试通过 \r\n 展开以获取行数组,但它失败并出现“内存不足”错误。

echo strlen($contents) 大约有 3050 万个字符。

我需要操作这些值并将它们插入数据库。我需要怎么做才能避免内存分配错误?

【问题讨论】:

    标签: php memory-management


    【解决方案1】:

    正如其他答案所说:

    • 你不能在记忆中拥有所有这些
    • 解决方案是使用CURLOPT_FILE

    但是,您可能不想真正创建一个文件,您可能希望使用内存中的数据...只要它“到达”就使用它。

    一种可能的解决方案可能是定义您自己的流包装器,并使用这个包装器,而不是一个真实的文件,CURLOPT_FILE

    首先,请看:


    现在,让我们举个例子。

    首先,让我们创建我们的流包装类:

    class MyStream {
        protected $buffer;
    
        function stream_open($path, $mode, $options, &$opened_path) {
            // Has to be declared, it seems...
            return true;
        }
    
        public function stream_write($data) {
            // Extract the lines ; on y tests, data was 8192 bytes long ; never more
            $lines = explode("\n", $data);
    
            // The buffer contains the end of the last line from previous time
            // => Is goes at the beginning of the first line we are getting this time
            $lines[0] = $this->buffer . $lines[0];
    
            // And the last line os only partial
            // => save it for next time, and remove it from the list this time
            $nb_lines = count($lines);
            $this->buffer = $lines[$nb_lines-1];
            unset($lines[$nb_lines-1]);
    
            // Here, do your work with the lines you have in the buffer
            var_dump($lines);
            echo '<hr />';
    
            return strlen($data);
        }
    }
    

    我要做的是:

    • 在数据块到达时处理它们(我使用 var_dump,但你会做通常的事情)
    • 请注意,您不会得到“完整行”:行尾是块的开头,同一行的开头是前一个块的结尾,因此,您必须保留一些部分对stream_write 的调用之间的一个块


    接下来,我们注册这个流包装器,与伪协议“test”一起使用:

    // Register the wrapper
    stream_wrapper_register("test", "MyStream")
        or die("Failed to register protocol");
    


    而且,现在,我们执行 curl 请求,就像我们在写入“真实”文件时所做的那样,就像其他答案建议的那样:

    // Open the "file"
    $fp = fopen("test://MyTestVariableInMemory", "r+");
    
    // Configuration of curl
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "http://www.rue89.com/");
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_BUFFERSIZE, 256);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FILE, $fp);    // Data will be sent to our stream ;-)
    
    curl_exec($ch);
    
    curl_close($ch);
    
    // Don't forget to close the "file" / stream
    fclose($fp);
    

    请注意,我们不使用真实文件,而是使用我们的伪协议。


    这样,每次数据块到达时,MyStream::stream_write 方法将被调用,并且将能够处理少量数据(当我测试时,我总是得到 8192 字节,无论我使用什么值CURLOPT_BUFFERSIZE)


    几点说明:

    • 显然,你需要比我做更多的测试
    • 如果行长于 8192 字节,我的 stream_write 实现可能无法正常工作;-)
    • 这只是一些提示,而不是一个完全有效的解决方案:您必须(再次)测试,并且可能编写更多代码!

    不过,我希望这会有所帮助 ;-)
    玩得开心!

    【讨论】:

    • 为此+1!我只想补充一点,在处理二进制数据时,您希望直接输出 $data 而根本不要触摸它,因为这很可能会损坏它。
    • 聪明。由于 curl 7.9.7 CURLOPT_FILE 已重命名为 CURLOPT_WRITEDATA,我认为您现在可以使用 CURLOPT_WRITEFUNCTION 执行类似的操作,这是一个类似于您的 stream_write($data) 的回调,并且无需流包装器。见curl.haxx.se/libcurl/c/curl_easy_setopt.html
    【解决方案2】:

    PHP 因内存不足而窒息。不要让 curl 使用文件的内容填充 PHP 变量,而是使用

    CURLOPT_FILE
    

    将文件保存到磁盘的选项。

    //pseudo, untested code to give you the idea
    
    $fp = fopen('path/to/save/file', 'w');
    curl_setopt($ch, CURLOPT_FILE, $fp);
    curl_exec ($ch);
    curl_close ($ch);
    fclose($fp);
    

    然后,一旦文件被保存,而不是使用 filefile_get_contents 函数(这会将整个文件加载到内存中,再次杀死 PHP),使用 fopenfgets 来读取文件一行一行。

    【讨论】:

    【解决方案3】:

    Darren Cook 对 Pascal MARTIN 回应的评论非常有趣。在现代 PHP+Curl 版本中,可以设置 CURLOPT_WRITEFUNCTION 选项,以便 CURL 为每个接收到的数据“块”调用回调。具体来说,“callable”将接收两个参数,第一个带有调用 curl 对象,第二个带有数据块。该函数应返回 strlen($data) 以便 curl 继续发送更多数据。

    可调用对象可以是 PHP 中的方法。使用所有这些,我开发了一个可能的解决方案,我发现它比以前的解决方案更具可读性(尽管 Pascal Martin 的反应非常好,但从那时起情况发生了变化)。为了简单起见,我使用了公共属性,但我相信读者可以适应和改进代码。您甚至可以在达到多行(或字节)时中止 CURL 请求。我希望这对其他人有用。

    <?
    class SplitCurlByLines {
    
        public function curlCallback($curl, $data) {
    
            $this->currentLine .= $data;
            $lines = explode("\n", $this->currentLine);
            // The last line could be unfinished. We should not
            // proccess it yet.
            $numLines = count($lines) - 1;
            $this->currentLine = $lines[$numLines]; // Save for the next callback.
    
            for ($i = 0; $i < $numLines; ++$i) {
                $this->processLine($lines[$i]); // Do whatever you want
                ++$this->totalLineCount; // Statistics.
                $this->totalLength += strlen($lines[$i]) + 1;
            }
            return strlen($data); // Ask curl for more data (!= value will stop).
    
        }
    
        public function processLine($str) {
            // Do what ever you want (split CSV, ...).
            echo $str . "\n";
        }
    
        public $currentLine = '';
        public $totalLineCount = 0;
        public $totalLength = 0;
    
    } // SplitCurlByLines
    
    // Just for testing, I will echo the content of Stackoverflow
    // main page. To avoid artifacts, I will inform the browser about
    // plain text MIME type, so the source code should be vissible.
    Header('Content-type: text/plain');
    
    $splitter = new SplitCurlByLines();
    
    // Configuration of curl
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "http://stackoverflow.com/");
    curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($splitter, 'curlCallback'));
    
    curl_exec($ch);
    
    // Process the last line.
    $splitter->processLine($splitter->currentLine);
    
    curl_close($ch);
    
    error_log($splitter->totalLineCount . " lines; " .
     $splitter->totalLength . " bytes.");
    ?>
    

    【讨论】:

      【解决方案4】:

      您可能需要考虑将其保存到一个临时文件中,然后使用fgetsfgetcsv 一次读取一行。

      这样您就可以避免因爆炸如此大的字符串而得到的初始大数组。

      【讨论】:

        【解决方案5】:
        1. php.ini 中增加memory_limit
        2. 使用fopen()fgets()读取数据。

        【讨论】:

          【解决方案6】:

          将其假脱机到一个文件中。不要试图一次将所有数据保存在内存中。

          【讨论】:

            【解决方案7】:

            注意:

            “基本上,如果你用fopen打开一个文件,fclose它然后取消链接, 它工作正常。但是如果在 fopen 和 fclose 之间,你给文件句柄 到 cURL 对文件进行一些写入,然后取消链接失败。为什么 正在发生的事情超出了我的范围。我认为这可能与 Bug #48676“有关”

            http://bugs.php.net/bug.php?id=49517

            因此,如果您使用的是旧版本的 PHP,请务必小心。此页面上有一个简单的修复方法来双重关闭文件资源:

            fclose($fp);
            if (is_resource($fp))
                fclose($fp);
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2021-11-23
              • 2011-05-09
              • 1970-01-01
              • 1970-01-01
              • 2021-03-23
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多