Манипулировать строкой длиной 30 миллионов символов - PullRequest
11 голосов
/ 27 августа 2009

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

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

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

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

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

Ответы [ 7 ]

51 голосов
/ 27 августа 2009

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

  • вы не можете хранить все это в памяти
  • решение будет использовать 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)


Несколько заметок:

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

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

18 голосов
/ 27 августа 2009

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

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

12 голосов
/ 15 ноября 2014

Комментарий Даррена Кука к ответу Паскаля МАРТИНА действительно интересен. В современных версиях 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.");
?>
5 голосов
/ 27 августа 2009

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

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

3 голосов
/ 27 августа 2009
  1. Увеличение memory_limit в php.ini.
  2. Чтение данных с использованием fopen() и fgets().
2 голосов
/ 27 августа 2009

Спул в файл. Не пытайтесь одновременно хранить все эти данные в памяти.

0 голосов
/ 05 февраля 2010

Примечание:

"Обычно, если вы откроете файл с помощью fopen, закройте его, а затем отсоедините его, это работает отлично. Но если между fopen и fclose, вы даете дескриптор файла чтобы сделать запись в файл, то разорвать связь не удастся. Зачем это происходит вне меня. Я думаю, что это может быть связано с ошибкой # 48676 "

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

Так что будьте осторожны, если вы используете старую версию PHP. На этой странице есть простое исправление для двойного закрытия файлового ресурса:

fclose($fp);
if (is_resource($fp))
    fclose($fp);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...