Jul 112015
 
Artículo PHP

La distribución base del intérprete PHP incluye un función json_decode que hace muy sencillo el proceso de ficheros en formato JSON. Pero esta función trabaja sobre un string que debe haber sido completamente en memoria. Esto puede ser un problema si se trabaja con ficheros de varios cientos de megabytes, o incluso de más de un gigabyte, como es el caso en determinados escenarios (por ejemplo, cuando se procesan datos geográficos en formato GeoJSON). En este artículo se explica el uso de una librería “jsonstreamingparser” para PHP, que permite procesar los objetos contenidos en el fichero conforme se van leyendo, evitando un consumo excesivo de los recursos de memoria disponibles.

Descarga e instalación de la librería jsonstreamingparser

La librería se puede descargar desde Github: https://github.com/salsify/jsonstreamingparser Una vez descargado y descomprimido el paquete, la instalación se realiza con composer. En primer lugar, instalamos composer localmente en el directorio jsonstreamingparser-master:

curl -sS https://getcomposer.org/installer | php

y a continuación procedemos a la instalación del paquete jsonstreamingparser y sus dependencias:

webmaster@openalfa:~/jsonstreamingparser-master$ php composer.phar install
Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing symfony/yaml (v2.7.1)
    Downloading: 100%         

  - Installing phpunit/php-text-template (1.2.1)
    Downloading: 100%         

  - Installing phpunit/phpunit-mock-objects (1.2.3)
    Downloading: 100%         

  - Installing phpunit/php-timer (1.0.6)
    Downloading: 100%         

  - Installing phpunit/php-token-stream (1.2.2)
    Downloading: 100%         

  - Installing phpunit/php-file-iterator (1.4.0)
    Loading from cache

  - Installing phpunit/php-code-coverage (1.2.18)
    Downloading: 100%         

  - Installing phpunit/phpunit (3.7.38)
    Downloading: 100%         

phpunit/php-code-coverage suggests installing ext-xdebug (>=2.0.5)
phpunit/phpunit suggests installing phpunit/php-invoker (~1.1)
Writing lock file
Generating autoload files
jwebmaster@openalfa:~/sonstreamingparser-master$

Comprobación de la instalación

En el directorio “example” existe un ejemplo que podemos ejecutar para comprobar que la instalación se ha realizado sin problemas:

webmaster@openalfa:~/jsonstreamingparser-master$ cd example
webmaster@openalfa:~/jsonstreamingparser-master/example$ php example.php 
array(2) {
 [0]=>
 array(6) {
 ["name"]=>
 string(58) "example document for wicked fast parsing of huge json docs"
 ["integer"]=>
 int(123)
 ["totally sweet scientific notation"]=>
 float(-1.23123)
 ["unicode? you betcha!"]=>
 string(17) "ú™£¢∞§♥"
 ["zero character"]=>
 string(1) "0"
 ["null is boring"]=>
 NULL
 }
...

Bajo el directorio “example” hay también un subdirectorio “geojson” que contiene un ejemplo de proceso de un fichero en formato GeoJSON:

Implementación de un Listener para procesar los datos recibidos

Para procesar un documento JSON, debemos crear una clase “MiProcesadorJSON” que implemente el interfaz “\JsonStreamingParser\Listener”. El script abre el fichero a procesar con una llamada a fopen(), y le pasa a una instancia de la clase “MiProcesadorJSON” el descriptor del fichero obtenido:

<?php

require_once 'MiProcesadorJSON.php';

$stream = fopen('documento.json', 'r');
$listener = new MiProcesadorJSON();
try {
  $parser = new JsonStreamingParser_Parser($stream, $listener);
  $parser->parse();
} catch (Exception $e) {
  fclose($stream);
  throw $e;
}

La clase MiProcesadorJSON debe implementar los métodos definidos en el interfaz “\JsonStreamingParser\Listener”:

<?php

require_once 'JsonStreamingParser/Parser.php';

class MiProcesadorJSON implements \JsonStreamingParser\Listener {
    public function start_document() {
        ...
    }
    public function end_document() {
        ...
    }
    public function start_object() {
        ...
    }
    public function end_object() {
        ...
    }
    public function start_array() {
        ...
    }
    public function end_array() {
        ...
    }
    public function key($key) {
        ...
    }
    public function value($value) {
        ...
    }
    public function whitespace($whitespace) {
        ...
    }
}

El parser se encarga de ir leyendo el fichero caracter a carácter, y llamar a las funciones definidas en el listener cada vez que se produce un evento. Normalmente, el listener utiliza una serie de variables privadas en las que guarda el estado del proceso.

Ejemplo: Proceso de un array de objetos sencillos

Un caso frecuente es el de un fichero JSON que consiste en un array con un gran número de objetos:

[
 {objeto1},
 {objeto2},
 ... 
]

Cada objeto es una serie de pares (clave, valor):

{
  "clave1": valor1,
  "clave2": valor2,
  ...
}

En este ejemplo, queremos obtener cada uno de los objetos como una estructura PHP, para procesarlos a medida que se van leyendo del fichero. Cada uno de los valores puede ser un valor primitivo ( un número, una cadena de texto, un valor booleano). Pero también puede ser un array de valores, o un objeto, dando lugar a una estructura de varios niveles:

{
  "clave1": "cadena de texto",
  "clave2": [
                "array de dos entradas",
                "segunda entrada del array"
            ],
  "clave3": {
               "descripcion": "El valor de la clave3 es un objeto"
            }
  ...
}

Para poder recoger estas estructuras anidadas en una variable PHP, deberemos utilizar una pila (en forma de un array PHP en la variable $_stack) en donde guardaremos la jerarquía de elementos que están siendo leidos. También utilizaremos una pila (en la variable $_keys) para guardar la jerarquía de claves, y una variable $_nivel para guardar el nivel en el que nos encontramos dentro de la jerarquía de elementos:

    private $_stack;
    private $_keys;
    private $_nivel;
    public function start_document() {
        $this->_nivel = 0;
        $this->_stack = array();
        $this->_keys = array();
    }
    public function start_object() {
        $this->_start_element('objeto');
    }
    public function start_array() {
        $this->_start_element('array');
    }
    private function _start_element($tipo) {
        if ($this->_nivel > 0) {
            $current_item = array('type' => $tipo, 'value' => array());
            $this->_stack[] = $current_item;
        }
        $this->_nivel++;
    }

Como vemos, estas variables se inicializan en la función start_document(), y se actualizan cuando comienza a leerse un nuevo array u objeto. Por otra parte, cuando se lee una clave, se almacena en la pila de claves:

    public function key($key) {
        $this->_keys[] = $key;
    }

Y cuando se lee un valor: – Si el elemento que se está procesando es un array, se añade al mismo. – Si el elemento que se está procesando es un objeto, se extrae la última clave de la pila de claves, y se añade el par (clave, valor) al último objeto de la pila de objetos:

    public function value($value) {
        $this->_inserta_valor($value);
    }
    private function _inserta_valor($value) {
        if ($this->_nivel > 1) {
            $current_item = array_pop($this->_stack);

            if ($current_item['type'] === 'objeto') {
                  $current_item['value'][array_pop($this->_keys)] = $value;
            } else {
                $current_item['value'][] = $value;
            }
            $this->_stack[] = $current_item;
        }

    }

También, cuando finaliza la lectura de un objeto o de un array, el elemento leído es tratado de la misma forma que si fuera un valor simple, insertándolo en el array u objeto del nivel anterior en la jerarquía:

    public function end_array() {
        $current_item = array_pop($this->_stack);
        $this->_inserta_valor($current_item['value']);
        $this->_nivel--;
    }
    public function end_object() {
        $current_item = array_pop($this->_stack);
        $this->_nivel--;
        if ($this->_nivel > 1) {
            $this->_inserta_valor($current_item['value']);
        } else {
            // Procesa el objeto de primer nivel
            echo "\n\nOBJETO: \n";
            var_dump($current_item["value"]);
        }
    }

Como vemos, en el caso de un objeto, comprobamos si se trata de un objeto de primer nivel, en cuyo caso procedemos a procesarlo como sea necesario (p.ej., insertándolo en una base de datos). En el ejemplo, simplemente volcamos el objeto a standard output con una llamada a var_dump().

Proceso de un fichero GeoJSON

El formato GeoJSON es un formato JSON en donde el elemento del primer nivel es un objeto que contiene una serie de pares (clave,valor). Entre ellas, está la clave “features”, que tiene asignado como valor un array de objetos que puede llegar a tener un número muy elevado de elementos. Para procesar este tipo de ficheros, debemos modificar ligeramente la clase MiProcesadorJSON. El cambio más sencillo consiste en modificar la función end_object() para que el proceso de un elemento se realice si el nivel es mayor que 2:

    public function end_object() {
        $current_item = array_pop($this->_stack);
        $this->_nivel--;
        if ($this->_nivel > 2) {
            $this->_inserta_valor($current_item['value']);
        } else {
            // Procesa el objeto de segundo nivel
            echo "\n\nOBJETO: \n";
            var_dump($current_item["value"]);
        }
    }

Y eso es todo!

Indice de artículos sobre programación en PHP

 Publicado por en 7:04 pm

 Deja un comentario

(requerido)

(requerido)