2009-08-27 15 views
11

Estoy descargando un archivo CSV de otro servidor como fuente de datos de un proveedor.Manipular una cadena de 30 millones de caracteres

Estoy usando curl para obtener el contenido del archivo y guardarlo en una variable llamada $contents.

Puedo llegar a esa parte muy bien, pero intenté explotar por \r y \n para obtener una matriz de líneas pero falla con un error de "memoria insuficiente".

I echo strlen($contents) y tiene aproximadamente 30,5 millones de caracteres.

Necesito manipular los valores e insertarlos en una base de datos. ¿Qué debo hacer para evitar errores de asignación de memoria?

Respuesta

17

PHP se está ahogando porque se está quedando sin memoria. En lugar de tener rizo pueblan una variable PHP con el contenido del archivo, utilice la opción

CURLOPT_FILE 

para guardar el archivo en el disco en su lugar.

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

Entonces, una vez que el archivo se guarda, en lugar de utilizar los file o file_get_contents funciones (que se carga el archivo en la memoria, matando a PHP de nuevo), utilice fopen y fgets para leer el archivo de una línea a la hora.

+5

La respuesta en http://stackoverflow.com/a/1342760/4668 es mejor que la mía. –

2

Lo enrolla en un archivo. No intente contener toda esa información en la memoria a la vez.

3
  1. Aumento memory_limit en php.ini.
  2. Lea los datos usando fopen() y fgets().
5

Es posible que desee considerar la posibilidad de guardarlo en un archivo temporal, y después de leerlo una línea a la vez utilizando fgets o fgetcsv.

De esta manera se evita la gran matriz inicial que se obtiene al explotar una cadena tan grande.

47

Como otras respuestas dicho:

  • no se puede tener todo esto en memoria
  • una solución sería utilizar CURLOPT_FILE

embargo, puede que no desee para crear realmente una archivo ; es posible que desee trabajar con datos en la memoria ... Usarlo tan pronto como "llegue".

Una posible solución podría ser definind es el propietario envoltura de flujo, y el uso de éste, en lugar de un archivo real, con CURLOPT_FILE

En primer lugar, ver:


Y ahora, vamos con un ejemplo.

En primer lugar, vamos a crear nuestra clase de envoltura de secuencia:

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

Lo que hago es:

  • trabajo sobre los fragmentos de datos (yo uso var_dump, pero que lo haría su material habitual en su lugar) cuando llegan
  • Tenga en cuenta que no obtiene "líneas completas": el final de una línea es el comienzo de un fragmento, y el comienzo de esa misma línea se encontraba al final del fragmento anterior; Por lo tanto, usted tiene que mantener algunas partes de un chunck entre las llamadas a stream_write


A continuación, registrar esta envoltura de flujo, para ser utilizado con el pseudo-protocolo de "prueba":

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


Y, ahora, que hacemos nuestra solicitud rizo, como lo haría cuando escribiendo a un documento "real", al igual que otras respuestas sugeridas:

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

Tenga en cuenta que no trabajamos con un archivo real, pero con nuestro pseudo protocolo.


De esta manera, cada vez que un fragmento de datos llega, MyStream::stream_write método se llamará, y será capaz de trabajar en una pequeña cantidad de datos (cuando probé, siempre llegué 8192 bytes, cualquiera que sea el valor I utilizado para CURLOPT_BUFFERSIZE)


Unas pocas notas:

  • Es necesario poner a prueba esta más que yo, obviamente,
  • mi implementación stream_write probablemente no funcionará si las líneas son más largas que 8192 bytes; depende de usted parchearlo ;-)
  • Solo se trata de unos pocos indicadores, y no una solución totalmente funcional: debe probar (nuevamente), ¡y probablemente codifique un poco más!

Aún así, espero que esto ayuda ;-)
Que se diviertan!

+0

+1 para esto! Me gustaría añadir que cuando se trata de datos binarios, querrá generar los $ datos directamente y no tocarlos, ya que es muy probable que lo corrompan. – mekwall

+4

Inteligente. Desde curl 7.9.7 'CURLOPT_FILE' se cambió el nombre a' CURLOPT_WRITEDATA', y creo que ahora puede hacer algo similar con 'CURLOPT_WRITEFUNCTION', que es una devolución de llamada como su' stream_write ($ data) ', y guarda la necesidad de la transmisión envoltura. Consulte http://curl.haxx.se/libcurl/c/curl_easy_setopt.html –

+0

esta es una buena solución compatible con la memoria. – Lupus

0

NB:

"Básicamente, si abre un archivo con fopen, fclose y luego desvincularla, funciona bien, pero si entre fopen y fclose, le dan el archivo manejar para rizar a hacer. algo escrito en el archivo, entonces falla la unlink. ¿por qué está sucediendo esto está más allá de mí. Creo que puede estar relacionado con Bug # 48676"

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

así que ten cuidado si estás en una más antigua versión de PHP. Hay una solución simple en esta página a doble cerca del recurso de archivo:

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

Darren Cook, comentario a la respuesta Pascal Martin es realmente interesante. En las versiones modernas de PHP + Curl, la opción CURLOPT_WRITEFUNCTION se puede configurar para que CURL invoque una devolución de llamada para cada "porción" de datos recibida. Específicamente, el "invocable" recibirá dos parámetros, el primero con el objeto curl invocante y el segundo con el fragmento de datos. La función debe devolver strlen($data) para curl continuar enviando más datos.

Callables pueden ser métodos en PHP. Utilizando todo esto, he desarrollado una posible solución que me parece más legible que la anterior (aunque la respuesta de Pascal Martin es realmente genial, las cosas han cambiado desde entonces). Utilicé atributos públicos para simplificar, pero estoy seguro de que los lectores podrían adaptar y mejorar el código. Incluso puede cancelar la solicitud CURL cuando se ha alcanzado un número de líneas (o bytes). Espero que esto sea útil para otros.

<? 
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."); 
?> 
Cuestiones relacionadas