Манипулировать строку длиной 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);