Cree una API extraída de la Web con Express y Cheerio

El crecimiento de la red mundial en las últimas dos décadas ha llevado a que se recopile una enorme cantidad de datos y se coloquen en páginas web a lo largo de...

El crecimiento de la red mundial en las últimas dos décadas ha llevado a que se recopile una enorme cantidad de datos y se coloquen en páginas web a través de Internet. Un corolario de esta producción y distribución hiperbólica de contenido en la web es la conservación de una gran cantidad de información que se puede utilizar de innumerables formas si se puede extraer y agregar de manera eficaz.

Las formas más comunes de recopilar y agregar datos disponibles en la web son (1) solicitarlos desde una API con tecnologías como DESCANSAR o JABÓN y (2) para escribir un programa para analizarlo o extraerlo de datos poco estructurados, como HTML. El primero es, con mucho, el método preferible para el programador que consume la información, pero a menudo no es una posibilidad debido al tiempo de desarrollo y los recursos necesarios por parte del productor. Por lo tanto, la mayoría de las veces, el único medio disponible para obtener los datos preciados es rasparlos.

Este artículo presentará una técnica para crear una aplicación Node.js independiente que recopila (extrae) datos de impuestos para el estado de Nebraska y presenta esa información al usuario o calcula los impuestos en función de una ciudad y la cantidad suministrada.

Las tecnologías a utilizar son:

  • Node.js: un tiempo de ejecución de JavaScript basado en el motor V8 de Chrome.
  • Express: un marco web Node.js
  • Cheerio: una biblioteca de análisis de HTML que refleja la conocida API de la biblioteca jQuery

El código fuente se puede encontrar en GitHub aquí.

Configuración del proyecto base

Este tutorial utilizará Node Package Manager (npm) para inicializar el proyecto, instalar bibliotecas y administrar dependencias. Antes de comenzar, asegúrese de tener npm configurado para su entorno.

Inicialice el proyecto aceptando las opciones básicas por defecto:

{.img-responsive}

Instalar dependencias:

{.img-responsive}

Estructura básica del proyecto:

{.img-responsive}

Expresar

Usaremos Expresar para desarrollar nuestra API RESTful para la aplicación de cálculo de impuestos. Express es un marco de aplicación web para aplicaciones Node que es flexible en el sentido de que impone pocas restricciones en la forma en que desarrolla sus aplicaciones pero muy potente porque proporciona varias características útiles que se utilizan en una multitud de aplicaciones web.

Configuración de Express

En server.js incluiremos un código estándar de configuración Express que creará la aplicación Express, luego registraremos el módulo de rutas que haremos en la siguiente subsección. Al final del archivo, indicaremos a la aplicación Express que escuche el puerto proporcionado o 3500, que es un puerto codificado.

En server.js copia y pega el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'use strict';

const express = require('express');
const app = express();
const port = process.env.PORT || 3500;

const routes = require('./api/routes');
routes(app);

app.listen(port);

console.log("Node application running on port " + port);

Rutas

Configuraremos el enrutamiento en nuestra aplicación para responder a las solicitudes realizadas a rutas URI específicas y significativas. ¿Qué quiero decir con significativo que puede estar preguntando? Bueno, en el paradigma REST, las rutas de ruta están diseñadas para exponer los recursos dentro de la aplicación de manera autodescriptiva.

En el archivo route/index.js copia y pega el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'use strict';

const taxCtrl = require('../controllers');

module.exports = (app) => {
    app.use(['/calculate/:stateName/:cityName/:amount', '/taxrate/:stateName/:cityName'], (req, res, next) => {
        const state = req.params.stateName;
        if (!taxCtrl.stateUrls.hasOwnProperty(state.toLowerCase())) {
            res.status(404)
                    .send({message: `No state info found for ${state}`});
        } else {
            next();
        }
    });

    app.route('/taxrate/:stateName/:cityName')
        .get(taxCtrl.getTaxRate);

    app.route('/calculate/:stateName/:cityName/:amount')
        .get(taxCtrl.calculateTaxes);
  
    app.use((req, res) => {
        res.status(404)
            .send({url: `sorry friend, but url ${req.originalUrl} is not found`});
    });
}

Las dos rutas que se definen en este módulo son /taxrate/:stateName/:cityName y /calculate/:stateName/:cityName/:amount. Están registrados con el objeto app que se pasó al módulo desde el script server.js descrito anteriormente llamando al método de ruta en la app. Dentro del método de ruta, se especifica la ruta y luego se llama al método get, o se encadena, en el resultado de llamar a la ruta. Dentro del método get encadenado hay una función de devolución de llamada que discutiremos más adelante en la sección sobre controladores. Este método de definición de rutas se conoce como "encadenamiento de rutas".

La primera ruta describe un punto final que mostrará las tasas de impuestos estatales y municipales en respuesta a una solicitud GET correspondiente a :stateName y :cityName, respectivamente. En Express, especifica lo que se conoce como "parámetros de ruta" precediendo una sección de una ruta delimitada entre barras diagonales con dos puntos para indicar un marcador de posición para un parámetro de ruta significativo. La segunda ruta /calculate/:stateName/:cityName/:amount describe un punto final que calculará los montos de los impuestos municipales y estatales, así como el monto total en función del parámetro de monto de la ruta.

Las otras dos invocaciones del objeto app especifican el middleware. Intermediario Express.js es una característica increíblemente útil que tiene muchas aplicaciones que fácilmente podrían garantizar su propia serie de artículos, por lo que no profundizaré aquí. . Solo sepa que el middleware son funciones que pueden conectarse, acceder y modificar la solicitud, la respuesta, el error y los siguientes objetos de un ciclo de solicitud-respuesta de Express.

Registra una función de middleware llamando al método use en el objeto app y pasando combinaciones únicas de rutas y funciones de devolución de llamada. El primer middleware declarado en el objeto de la aplicación especifica nuestras dos URL dentro de una matriz y una función de devolución de llamada que verifica si el estado que se pasa para solicitar información fiscal está disponible.

Esta aplicación de demostración solo se desarrollará para responder a la solicitud de las ciudades de Nebraska, pero alguien podría ampliarla fácilmente con otros estados, dado que tienen una página web estática disponible públicamente con información similar. El segundo middleware sirve como captura de todas las rutas de URL solicitadas que no se especifican.

Controladores

Los controladores son la parte de una aplicación Express que maneja las solicitudes reales realizadas a las rutas definidas y devuelve una respuesta. Estrictamente hablando, los controladores no son un requisito para desarrollar aplicaciones Express. Se puede usar una función de devolución de llamada, anónima o de otro tipo, pero el uso de controladores conduce a un código mejor organizado y a la separación de preocupaciones. Dicho esto, usaremos controladores, ya que siempre es una buena idea seguir las mejores prácticas.

En su archivo controllers/index.js, copie y pegue el siguiente código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
'use strict';

const svc = require('../services');

const getTaxRate = (req, res) => {
    const state = req.params.stateName;
    svc.scrapeTaxRates(state, stateUrls[state.toLowerCase()], (rates) => {
        const rate = rates.find(rate => {
            return rate.city.toLowerCase() === req.params.cityName.toLowerCase();
        });
        res.send(rate);
    });
}

const calculateTaxes = (req, res) => {
    const state = req.params.stateName;
    svc.scrapeTaxRates(state, stateUrls[state.toLowerCase()], (rates) => {
        const rate = rates.find(rate => {
            return rate.city.toLowerCase() === req.params.cityName.toLowerCase();
        });
        res.send(rate.calculateTax(parseFloat(req.params.amount)));
    });
}


const stateUrls = {
    nebraska: 'http://www.revenue.nebraska.gov/question/sales.html';
};

module.exports = {
    getTaxRate,
    calculateTaxes,
    stateUrls
};

Lo primero que ve que se importa y se declara en el módulo de controladores es una constante llamada svc, que es la abreviatura de "servicio". Este objeto de servicio sirve como una funcionalidad reutilizable para solicitar una página web y analizar el HTML resultante. Profundizaré más en la sección sobre Cheerio y los servicios sobre lo que sucede detrás de escena con este objeto de servicio, pero por ahora solo sepa que analiza el HTML en busca de los bits significativos que nos interesan (es decir, las tasas de impuestos).

Las dos funciones que más nos interesan son getTaxRate y calculateTaxes. Ambas funciones se pasan en objetos de solicitud y respuesta (req y res) a través de los métodos route.get(...) en el módulo de rutas. La función getTaxRate accede al parámetro de ruta stateName desde el objeto params del objeto de solicitud.

El nombre del estado y su URL de destino correspondiente (en este caso, solo Nebraska y su página web del gobierno que muestra información sujeta a impuestos) se pasan al método del objeto de servicio scrapeTaxRates. Se pasa una función de devolución de llamada como tercer parámetro para filtrar y responder con la información de la ciudad correspondiente al parámetro cityName que se encuentra en la ruta de la ruta.

La segunda función del controlador, calculateTaxes, nuevamente usa el método de servicio scrapeTaxRates para solicitar y analizar el HTML, pero esta vez calcula los impuestos a través de un método dentro de la clase TaxRate, que discutiremos a continuación en el sección de modelos.

Modelos

Al igual que los controladores, los modelos no son algo estrictamente necesario para una aplicación Express. Sin embargo, los modelos son bastante útiles cuando queremos encapsular datos (estado) y comportamiento (acciones) dentro de nuestras aplicaciones de manera organizada.

En su archivo models/index.js, copie y pegue el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict'

class TaxRate {
    constructor(state, city, localRate, stateRate) {
        this.state = state;
        this.city = city;
        this.localRate = localRate;
        this.stateRate = stateRate;
    }

    calculateTax (subTotal) {
        const localTax = this.localRate * subTotal;
        const stateTax = this.stateRate * subTotal;
        const total = subTotal + localTax + stateTax;
        return {
            localTax,
            stateTax,
            total
        };
    }
}

module.exports = TaxRate;

El único modelo (o dicho más correctamente: clase) que definiremos en nuestra aplicación es TaxRate. TaxRate contiene campos de miembros para almacenar datos sobre el estado, la ciudad, la tasa de impuestos local y la tasa de impuestos estatal. Estos son los campos de clase que componen el estado del objeto. Solo hay un método de clase, calculateTax(...), que toma el parámetro que representa un monto subtotal pasado a la ruta /calculate/:stateName/:cityName/:amount ruta y devolverá un objeto que representa el las cantidades de impuestos calculadas y el importe total final.

Saludos

Cheerio es una biblioteca de JavaScript liviana que implementa el núcleo jQuery para acceder, seleccionar y consultar HTML en aplicaciones del lado del servidor. En nuestro caso, usaremos Cheerio para analizar el HTML en la página web estática que solicitamos del sitio web del gobierno de Nebraska que muestra información fiscal.

Servicios

En nuestra pequeña aplicación, utilizaremos un módulo de servicios personalizados para implementar la solicitud de la página HTML del sitio web del gobierno de Nebraska, así como el análisis del HTML resultante para extraer los datos que deseamos.

En su archivo services/index.js copie y pegue el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
'use strict';

const http = require('http');
const cheerio = require('cheerio');
const TaxRate = require('../models');

const scrapeTaxRates = (state, url, cb) => {
    http.get(url, (res) => {
        let html = '';
  
        res.on('data', chunk => {
            html += chunk;
        });
  
        res.on('end', () => {
            const parser = new Parser(state);
            const rates = parser.parse(html);
            cb(rates);
        });
    });
};

class Parser {
    constructor(state) {
        this.state = state;
    }

    parse(html) {
        switch(this.state.toLowerCase()) {
            case 'nebraska':
                return this.parseNebraska(html);
            default:
                return null;
        }
    }

    parseNebraska(html) {
        const $ = cheerio.load(html);
        let rates = [];
        $('tr').each((idx, el) => {
            const cells = $(el).children('td');
            if (cells.length === 5 && !$(el).attr('bgcolor')) {
                const rawData = {
                    city: $(cells[0]).first().text(),
                    cityRate: $(cells[1]).first().text(),
                    totalRate: $(cells[2]).first().text()
                };
                rawData.cityRate = parseFloat(rawData.cityRate.replace('%', ''))/100;
                rawData.totalRate = parseFloat(rawData.totalRate.substr(0, rawData.totalRate.indexOf('%')))/100;
                rawData.stateRate = rawData.totalRate - rawData.cityRate;
                rates.push(new TaxRate('Nebraska', rawData.city, rawData.cityRate, rawData.stateRate));
            }
        });
        return rates;
    }
}

module.exports = {
    scrapeTaxRates;
};

Las primeras tres líneas están importando (a través de require()) algunos objetos de nivel de módulo http, cheerio y TaxRate. TaxRate se describió en la sección anterior sobre módulos, por lo que no venceremos al proverbial caballo muerto y analizaremos su uso con demasiado detalle, así que basta con decir que se usa para almacenar datos de tasa de impuestos y calcular impuestos.

El objeto http es un módulo de nodo que se utiliza para realizar solicitudes desde el servidor a otro recurso de la red, que en nuestro caso es la página web de tasa de impuestos del gobierno de Nebraska. El restante es Cheerio, que se usa para analizar el HTML usando la conocida API jQuery.

El módulo de servicios solo expone una función disponible públicamente llamada scrapeTaxRates, que toma una cadena de nombre de estado, una cadena de URL (para la página del estado que muestra las tasas de impuestos) y una función de devolución de llamada para procesar las tasas de impuestos de formas únicas especificadas por el código del cliente llamante.

Dentro del cuerpo de la función scrapeTaxRates, se llama al método get del objeto http para solicitar la página web en la URL especificada. La función de devolución de llamada pasada al método http.get(...) maneja el procesamiento de la respuesta. En el procesamiento de la respuesta, se construye una cadena de HTML y se almacena en una variable llamada html. Esto se hace de forma incremental a medida que se activa el evento data y se devuelve una porción de datos almacenados en el búfer de la respuesta.

Tras la activación del evento end, se invoca una función de devolución de llamada final. Dentro de esta devolución de llamada, se crea una instancia de la clase Parser y se llama al método parse para analizar el HTML y extraer la información específica de la estructura y el diseño de la página web de Nebraska. Los datos analizados se cargan en una serie de objetos TaxRate almacenados en una matriz y se pasan a la función de devolución de llamada para ejecutar la lógica especificada en el código del cliente que llama (en nuestro caso, en las funciones del controlador descritas anteriormente). Es en este último paso que los datos se serializan y envían como respuesta a la persona que llama a la API REST.

Conclusión

En este breve artículo, investigamos cómo diseñar una aplicación Node.js simple y liviana que obtenga información útil sobre la tasa de impuestos de un sitio web del gobierno, lo que podría ser útil para las aplicaciones de comercio electrónico. Los dos propósitos principales de la aplicación son recopilar tasas de impuestos y mostrar esa información para una ciudad determinada o calcular impuestos según un estado, una ciudad y un subtotal.

Por ejemplo, a continuación encontrará capturas de pantalla de la aplicación que muestran los impuestos de la ciudad de Omaha y calculan los impuestos para un subtotal de $1000. Para probar esta aplicación cd en el directorio raíz y escriba $ node server.js en la consola. Verá un mensaje que dice, "Aplicación de nodo ejecutándose en el puerto 3500".

{.img-responsive}

{.img-responsive}

Espero que este artículo lo inspire a investigar más a fondo el mundo del raspado de datos para que pueda crear aplicaciones útiles y productos de datos significativos. Como siempre, doy la bienvenida a todos y cada uno de los comentarios a continuación.