2009-08-27 9 views
11

Sto scaricando un file CSV da un altro server come feed di dati da un fornitore.Manipolare una stringa lunga 30 milioni di caratteri

Sto usando arricciatura per ottenere il contenuto del file e salvarlo in una variabile denominata $contents.

Posso arrivare a quella parte bene, ma ho provato ad esplodere per \r e \n per ottenere una matrice di linee ma non riesce con un errore di 'memoria insufficiente'.

I echo strlen($contents) e si tratta di circa 30,5 milioni di caratteri.

Ho bisogno di manipolare i valori e inserirli in un database. Cosa devo fare per evitare errori di allocazione della memoria?

risposta

17

PHP sta soffocando perché sta esaurendo la memoria. Invece di avere ricciolo popolano una variabile PHP con il contenuto del file, utilizzare l'opzione

CURLOPT_FILE 

per salvare il file sul disco, invece.

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

Quindi, una volta che il file viene salvato, invece di utilizzare i file o file_get_contents funzioni (che caricare l'intero file in memoria, a uccidere PHP), utilizzare fopen e fgets leggere il file una riga alla tempo.

+5

La risposta in http://stackoverflow.com/a/1342760/4668 è migliore della mia. –

2

Spoollo in un file. Non provare a tenere tutti i dati in memoria in una sola volta.

3
  1. Aumentare memory_limit in php.ini.
  2. Leggi dati utilizzando fopen() e fgets().
5

È possibile considerare di salvarlo in un file temporaneo e quindi di leggerlo una riga alla volta utilizzando fgets o fgetcsv.

In questo modo si evita il grande array iniziale derivante dall'esplosione di una stringa così grande.

47

Come altre risposte detto:

  • non si può avere tutto ciò che nella memoria
  • una soluzione sarebbe quella di utilizzare CURLOPT_FILE

Ma, non si potrebbe desiderare di creare davvero un file ; potresti voler lavorare con i dati in memoria ... Usarli non appena "arriva".

Una possibile soluzione potrebbe essere definind si possiede flusso involucro, e utilizzare questo, invece di un file vero e proprio, con CURLOPT_FILE

Prima di tutto, vedi:


E ora, facciamo un esempio.

In primo luogo, creiamo la nostra classe flusso avvolgitore:

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

Quello che faccio è:

  • lavoro sui blocchi di dati (io uso var_dump, ma farei la vostra solita roba invece) quando arrivano
  • Si noti che non si ottengono "linee complete": la fine di una linea è l'inizio di un blocco, e l'inizio di quella stessa linea era alla fine del blocco precedente; quindi, è necessario mantenere alcune parti di un chunck tra le chiamate al stream_write


Successivamente, registrare questo flusso di involucro, per essere utilizzato con la pseudo-protocollo "test":

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


E, ora, facciamo del nostro riccio richiesta, come faremmo quando scrittura di un file "reale", come le altre risposte hanno suggerito:

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

Nota che non lavoriamo con un file reale, ma con il nostro pseudo-protocollo.


In questo modo, ogni volta che un blocco di dati arriva, MyStream::stream_write metodo verrà chiamato, e sarà in grado di lavorare su una piccola quantità di dati (quando ho provato, ho sempre avuto 8192 byte, qualsiasi valore I usato per CURLOPT_BUFFERSIZE)


alcune note:

  • È necessario testare questo più che ho fatto, ovviamente
  • l'implementazione dello stream_write probabilmente non funzionerà se le righe sono più lunghe di 8192 byte; a te per aggiustarlo ;-)
  • Si tratta di poche indicazioni e non di una soluzione pienamente funzionante: devi testare (di nuovo) e probabilmente codificarne un po 'di più!

Eppure, spero che questo aiuta ;-)
Buon divertimento!

+0

+1 per questo! Vorrei solo aggiungere che quando si ha a che fare con i dati binari, si vorrebbe esportare direttamente i dati $ e non toccarli affatto, dato che probabilmente lo corromperanno. – mekwall

+4

Intelligente. Poiché curl 7.9.7 'CURLOPT_FILE' è stato rinominato in' CURLOPT_WRITEDATA', e penso che ora puoi fare qualcosa di simile usando 'CURLOPT_WRITEFUNCTION', che è un callback come il tuo' stream_write ($ data) ', e salva la necessità del flusso wrapper. Vedi http://curl.haxx.se/libcurl/c/curl_easy_setopt.html –

+0

questa è una buona soluzione adatta alla memoria. – Lupus

0

NB:

"In sostanza, se si apre un file con fopen, fclose e poi scollegare esso, funziona bene, ma se tra il fopen e fclose, vi darà il file gestire per arricciare fare. una scritta nel file, quindi l'unlink fallisce. Perché questo sta accadendo è oltre me. Penso che possa essere correlato a Bug # 48676"

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

quindi fate attenzione se siete su un vecchio versione di PHP. V'è una semplice correzione in questa pagina per doppio vicino la risorsa del file:

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

Darren Cook commento alla risposta Pascal MARTIN è davvero interessante. Nelle moderne versioni di PHP + Curl, l'opzione CURLOPT_WRITEFUNCTION può essere impostata in modo che CURL invochi una richiamata per ogni "blocco" di dati ricevuto. Nello specifico, il "callable" riceverà due parametri, il primo con l'oggetto riccio invocante e il secondo con il blocco dati. La funzione deve restituire strlen($data) per arricciare continuare a inviare più dati.

Le callebles possono essere metodi in PHP. Usando tutto questo, ho sviluppato una possibile soluzione che trovo più leggibile di quella precedente (sebbene la risposta di Pascal Martin sia davvero eccezionale, le cose sono cambiate da allora). Ho usato attributi pubblici per semplicità, ma sono sicuro che i lettori potrebbero adattare e migliorare il codice. Puoi anche interrompere la richiesta CURL quando sono state raggiunte un numero di linee (o di byte). Spero che questo sarebbe utile per gli altri.

<? 
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."); 
?> 
Problemi correlati