PHP y archivos de texto de más de 50 Mb

Hola, escribo este Post, porque recientemente me enfrente a un problema al tratar de trabajar con archivos de texto de mas de 50 mb a través de una aplicación Web desarrollada en PHP.

Espero que esta experiencia pueda ser útil para la comunidad que siga este post. No pretendo enseñar nada, ya que es evidente que allá afuera hay expertos en PHP, pero quizás haya otros que no lo sean tanto y esto pueda ser de mucha utilidad.

El objetivo

El objetivo de este proceso parte de la aplicación era buscar un valor alfanumérico en un archivo de texto de 746,885 lineas. El archivo de texto tenia una tamaño en de 54 Mb. Este archivo tiene una estructura de campos separados por “pipes”, en total 5 campos por renglón.

Las diferentes alternativas de solución

La primera opción que paso por mi mente, fue convertir ese archivo en una tabla de una base de datos en MySQL, que me permitiera hacer las consultas de forma mas cómoda. Un punto importante en esto es que ese archivo se actualiza cada día lo cual implicaba un proceso diario de eliminar los registros de la tabla y volver a insertar todos los registros del archivo.

En el primer intento y después de unos cálculos me di cuenta que ese proceso podía tardar aproximadamente 12 horas. Por lo que inmediatamente descarte esa posibilidad, no me pareció tan práctico.

De ahí pasamos a la segunda alternativa de solución, buscar directamente en el archivo el valor dado.

Esta solución parecía fácil de implementar. Explicare de que forma lo hice funcionar en primera instancia.

Existen dos formas de trabajar con archivos en PHP, y usaremos alguna de ellas dependiendo del requerimiento.

La primera forma en que hice uso del archivo fue, a través de la función file() la cual lee por completo un archivo y construye un arreglo con una linea por elemento del archivo.

Ejemplo

$file = file("ruta/del/archivo.txt");

con esta sencilla instrucción convertimos a la variable $file en un arreglo que contiene cada una de las lineas del archivo.

Existen dos argumentos opcionales que permiten especificar que ignore, lineas nuevas y/o lineas vacías, quedando la sintaxis de la siguiente manera.

$file = file("ruta/del/archivo.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

Ok, después había que buscar el valor, en cada una de las lineas hasta encontrarlo y obtener los campos de esa linea.

Para lograr esto hice uso de la estructura de control foreach.

foreach ($file as $numlinea => $linea):

Esta validación es para no usar la primer linea del archivo que contiene solo el nombre de los campos.

if ($numlinea > 0):

Con esta linea convertimos cada campo en un elemento de un arreglo que se guarda en la variable $campos la función, explode nos permite partir en elementos de un arreglo una linea tomando como separador el símbolo que indicamos en el primer parámetro de la función

$campos = explode("|", $linea);

En la siguiente linea hacemos la validación del primer campo de la linea, preguntando si este es igual al valor que buscamos, que esta alojado en la variable de entorno $argv en la posición uno. La variable $argv almacena los valores que son enviados a un script en PHP, es útil cuando el script será ejecutado desde la linea de comandos y no a través de una solicitud HTTP.

Adicionalmente validamos que el último campo de la linea se valor “A”, que para este caso sería un valor de “Aprobado” y al cumplirse ambas condiciones nos aseguramos que el valor buscado se encuentra en el archivo y que mantiene un estado de “Aprobado”

if ($campos[0] == $argv[1] && $campos[4] == "A"):

En la variable $output guardamos cada uno de los campos separados por una coma y en formato de una cadena de texto. Esto es así específicamente para el uso que se le dará mas adelante.

$output = "$campos[0],$campos[1],$campos[2],$campos[3],Aprobado";

Con el comando break; rompemos el foreach para no seguir buscando en el resto de las lineas del archivo.

break;

Y finalmente damos salida a la variable $output

echo $output;

Hasta aquí todo bien y sin muchas cosas nuevas. Pero el problema fue cuando a través de la aplicación intentaba correr esta función.

Invariablemente el navegador mandaba un error 500, y nunca logre identificar que provocaba este error. Al ejecutar el script desde consola, inmediatamente encontraba el valor y devolvía los datos, además lo hacia verdaderamente rápido.

Busque en algunos foros y encontré toda variedad de sugerencias, comentarios como que “PHP no esta diseñado para manejar grandes archivos” sugerencias a usar Python, Pearl, etc.

Sin embargo encontré una forma de obtener el resultado deseado.

Otra alternativa que busque fue, cambiar el método de leer todo el archivo a leerlo linea por linea, quizás eso evitaría el error 500 del navegador. Pero no fue así. Sin embargo explico en que consistió el cambio.

Básicamente abrí el archivo para lectura con:

$file = fopen("ruta/al/archivo.txt","r");

Y lo leí linea por linea con la función fgets:

$linea = fgets($file);

Esto me hizo pensar que podría evitar un overflow en la memoria, al tratarse de un archivo tan grande. Pero al momento de probarlo en el navegador se producía el mismo error 500.

En este punto ya estaba realmente desesperado. Pero pensé que si ambas opciones resultaban desde la consola, algo había que cambiar en la configuración del servidor, en este caso Apache2 o en la configuración de PHP a través del php.ini

Me perdí un rato en eso y no concluí nada.

La solución

Entonces pensé que tendría que ejecutar desde un script PHP, el script de búsqueda, como si fuera ejecutado desde la consola.

Y encontré que PHP cuenta no con una sino con varias funciones para este efecto. No haré mas grande este post tratando de explicar cada una, vamos al grano.

La función que me permitió obtener la solución fue:

shell_exec();

Esta función ejecuta comandos a través de un shell y devuelve en formato de cadena, el resultado de dicho comando.

Y lo utilicé de la siguiente manera.

En la siguiente linea lo que hacemos es asignar a la variable $output el resultado de shell_exec. El primer parámetro que  vemos es el comando a ejecutar, en este caso mandamos llamar al comando php con el argumento -f que sirve para ejecutar un script de PHP indicando el archivo de script, que en el ejemplo es el archivo “scriptbusqueda.php” que se encuentra en la ruta “/var/www/proveedores/php” y por ultimo una variable $cual, que en realidad aloja el valor que deseamos encontrar en el archivo de texto de mas de 50 Mb

$output = shell_exec("php -f /var/www/proveedores/php/scriptbusqueda.php $cual");

Una ves que es devuelto el resultado del script a través de la función shell_exec lo guardamos como arreglo en la variable $campos como se muestra a continuación. Si recuerdan lineas antes vimos que la búsqueda en el archivo nos devolvía una cadena con los campos de la linea, separados por comas. Ahora en esta parte de la aplicación tomamos esa cadena y la convertimos en un arreglo.

$campos = explode(",", $output);

Y la mejor parte fue que al probar la aplicación en su entorno de Navegador, todo funcionó, el error 500 desapareció y obtuve mi resultado sin problemas.

Ventajas

  1. Me ahorre 12 horas al día para mantener actualizada una tabla.
  2. Reduje el proceso a actualizar un archivo cada día desde un servidor FTP a través de wget en un servidor Linux, archivo de texto de mas de 50 Mb, pero que tarda no mas de 5 minutos en ser descargado
  3. Y la oportunidad de poder compartir con Uds esta experiencia.

Espero que en realidad sea útil y sientan la confianza de hacer preguntas y/o comentarios, sería muy bueno si alguno de Uds conoce una mejor manera de hacerlo.

Hasta la próxima.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s