Я загружаю CSV-файл с другого сервера в качестве фида данных от поставщика.
Я использую curl для получения содержимого файла и сохранения его в переменной с именем $contents
.
Я могу добираться до этой части просто отлично, но я попытался взломать с помощью \r
и \n
чтобы получить массив строк, но с ошибкой «из памяти».
I echo strlen($contents)
и это около 30,5 миллионов символов.
Мне нужно манипулировать значениями и вставлять их в базу данных. Что мне нужно сделать, чтобы избежать ошибок выделения памяти?
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 для чтения файла по одной строке за раз.
Как говорили другие ответы:
CURLOPT_FILE
Но вы, возможно, не захотите действительно создать файл; вы можете захотеть работать с данными в памяти … Используя его, как только он «придет».
Одним из возможных решений может стать определение собственной потоковой оболочки и использование этого вместо реального файла с CURLOPT_FILE
Прежде всего, см.
stream_wrapper_register
The streamWrapper class
А теперь давайте рассмотрим пример.
Сначала давайте создадим класс оболочки потока:
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); } }
не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); } }
Что я делаю:
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
)
Несколько примечаний:
Тем не менее, я надеюсь, что это поможет 😉
Повеселись !
Комментарий Даррена Кука к ответу Паскаля МАРТИНА действительно интересен. В современных версиях PHP + Curl опция CURLOPT_WRITEFUNCTION
может быть установлена так, чтобы CURLOPT_WRITEFUNCTION
обратный вызов для каждого полученного «куска» данных. В частности, «вызываемый» получит два параметра, первый из которых содержит вызывающий объект 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."); ?>
Вы можете захотеть сохранить его во временный файл, а затем прочитать его по одной строке за раз, используя fgets
или fgetcsv
.
Таким образом, вы избегаете первоначального большого массива, который вы получаете от взрыва такой большой строки.
memory_limit
в php.ini
. fopen()
и fgets()
. Заправьте его в файл. Не пытайтесь хранить все эти данные в памяти сразу.
NB:
«В принципе, если вы открываете файл с fopen, fclose, а затем отключаете его, он работает нормально. Но если между fopen и fclose вы передаете дескриптор файла cURL, чтобы сделать запись в файл, тогда сбой невозможен. это происходит вне меня. Я думаю, это может быть связано с Bug # 48676 "
http://bugs.php.net/bug.php?id=49517
Поэтому будьте осторожны, если вы используете более старую версию PHP. На этой странице есть простое исправление для двойного закрытия файлового ресурса:
fclose($fp); if (is_resource($fp)) fclose($fp);