Манипулировать строку длиной 30 миллионов символов

Я загружаю CSV файл с другого сервера в качестве фида данных от поставщика.

Я использую curl для получения содержимого файла и сохранения его в переменной с именем $contents.

Я могу получить эту часть просто отлично, но я попытался взломать \r и \n, чтобы получить массив строк, но с ошибкой "из памяти".

I echo strlen($contents) и около 30,5 миллионов символов.

Мне нужно манипулировать значениями и вставлять их в базу данных. Что мне нужно сделать, чтобы избежать ошибок выделения памяти?

Ответ 1

PHP задыхается, потому что он исчерпывает память. Вместо того, чтобы закручивать переменную 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);

Затем, как только файл будет сохранен, вместо использования функций file или file_get_contents (которые будут загружать весь файл в память, снова убивая PHP), используйте fopen и fgets, чтобы прочитать файл по одной строке за раз.

Ответ 2

Как говорили другие ответы:

  • вы не можете иметь все это в памяти
  • решением было бы использовать 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, но вместо этого вы делаете свои обычные вещи), когда они придут
  • Обратите внимание, что вы не получаете "полные строки": конец строки - это начало фрагмента, и начало этой же строки было в конце предыдущего фрагмента; поэтому, вы должны держать некоторые части chunck между вызовами на stream_write


Затем мы регистрируем эту обертку потока, которая будет использоваться с псевдопротокольным "тестом":

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


И теперь мы делаем запрос на завивание, как мы это делали, когда писались в "настоящий" файл, как и другие предложенные ответы:

// 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)


Несколько примечаний:

  • Вам нужно проверить это больше, чем я, очевидно
  • моя реализация stream_write, вероятно, не будет работать, если строки длиннее 8192 байта; до вас, чтобы его исправить; -)
  • Это означает только несколько указателей, а не полностью работоспособное решение: вам нужно проверить (снова) и, вероятно, немного больше кода!

Тем не менее, я надеюсь, что это поможет;-)
Получайте удовольствие!

Ответ 3

Комментарий Даррена Кука к ответу Паскаля МАРТИНА действительно интересен. В современных версиях PHP + Curl параметр CURLOPT_WRITEFUNCTION может быть установлен таким образом, чтобы CURL вызывал обратный вызов для каждого полученного "куска" данных. В частности, "вызываемый" получит два параметра, первый из которых содержит вызывающий объект curl, а второй - блок данных. Функция должна возвращать strlen($data), чтобы завивать, продолжая отправлять больше данных.

Callables могут быть методами в PHP. Используя все это, я разработал возможное решение, которое я считаю более читаемым, чем предыдущий (хотя ответ Паскаля Мартина действительно велик, с тех пор все изменилось). Я использовал публичные атрибуты для простоты, но я уверен, что читатели смогут адаптировать и улучшить код. Вы можете даже прервать запрос 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

Вы можете захотеть сохранить его во временный файл, а затем прочитать его по одной строке за раз, используя fgets или fgetcsv.

Таким образом вы избегаете первоначального большого массива, который вы получаете от взрыва такой большой строки.

Ответ 5

  • Увеличить memory_limit в php.ini.
  • Прочитайте данные с помощью 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);