CTOs, desarrolladores: ¿cómo elegir una buena API externa?

Hoy en día, encontrar una API externa que nos permita mejorar la calidad de nuestro servicio es muy fácil. Cada día mas empresas ponen a disposición APIs. Problema: numerosos desarrolladores/CTOs empiezan la integración, ¡mientras que debería de ser la ultima etapa! Antes de eso usted tiene que determinar si la calidad de la API basta. Aquí esta como hago yo. Espero que todo ello ayudara a otros CTOs y desarrolladores.

Calidad de los datos

Muchas APIs proveen datos que le permiten enriquecer su propio sistema (no es el caso de todas las APIs por supuesto, Stripe no es una API de enriquecimiento por ejemplo). Es indispensable que se asegure de la calidad de estos datos. Va a tomarle mucho tiempo, ¡y ya sé que no le gustan las pruebas! Yo tampoco, pero no puede evitar la creación de un escenario de prueba riguroso aquí. Si se da cuenta de que la calidad de sus datos no esta bastante buena, solo dos semanas después de haber terminado su integración, se arrepentirá…

Documentación

Recientemente me encontré una API que proveía datos de alta calidad (mucho mejor que lo que la competencia proponía para mí), pero su documentación era… ¡una pesadilla! En realidad no había documentación. Además la API no respetaba todas la convenciones REST. ¿Cómo puede lograrlo integrar una API externa si los códigos de error no están correctamente documentados? Entonces la única solución que queda es probar mucho la API para entender su funcionamiento. La ingeniería inversa puede ser graciosa pero necesita mucho tiempo. Recuerde que en lo que concierne una API no tiene repositorio GitHub a explorar ya que el código fuente no esta disponible… Una mala documentación le hace perder mucho tiempo al dev y trae sorpresas desagradables a medio plazo.

Bibliotecas

¿Es posible integrar la API gracias a una biblioteca disponible en su lenguaje favorito? Como desarrollador Python y Go siempre estoy encantado cuando me encuentro una API que ofrece una lib Python (sé que puedo olvidar Go por el momento). Le puede hacer ganar mucho tiempo, pero ante todo asegúrese de que la biblioteca esté madura y que cubra todas la funcionalidades de la API (no siempre es el caso).

Notoriedad de la empresa

La notoriedad puede ayudarle a aclarar su elección y evitar las sorpresas desagradables con su API en el futuro. Por “sorpresa desagradable” entiendo interrupción de servicio, regresión, o incluso la suspensión definitiva del servicio… Puede en parte evitar estas trampas preguntándose lo siguiente:

  • ¿Esta API es popular en Internet (en general si encuentra poca información, huya)? ¿Encuentra muchos artículos/tutoriales que hablan de la API? Estos artículos son elogiosos?
  • ¿Empresas populares la utilizan?
  • Si la empresa ha desarrollado bibliotecas dedicadas, ¿estan evaluadas de forma positiva en GitHub? ¿Los problemas reportados en GitHub están tratados con regularidad?
  • ¿Hubieron actualizaciones recientes de la API o la ultima actualización tuvo lugar hace mucho tiempo?

Soporte técnico

Asegúrese de que alguien responda a sus preguntas rápidamente por email cuando usted encuentra un problema y que la respuesta es relevante. Si vive en Europa y la API esta proveída por una empresa americana, asegúrese de que el desfase horario no sea un problema.

Respeto de las convenciones

Para mi, las APIs serias hoy tienen que ser RESTful. Si la API que le gusta no respeta la convenciones REST, pues desconfíe de esta API. Sin embargo tenga presente que el estándar REST no es perfectamente claro y que cada API puede tener sur propia variante (códigos HTTP, codificación de las consultas POST, …). A pesar de todo, tiene que leer la documentación atentamente y asegurarse de que no nota cosas demasiado originales. Originalidad le ralentizara…

Precio

Claro el precio es muy importante. Pero cuidase, la tarificación de una API no siempre es fácil de entender. ¿Va a pagar cada mes por un numero ilimitado de consultas? ¿Pagar por cada consulta? En este segundo caso, ¿va a pagar dos veces por dos consultas idénticas (caso de una API de enriquecimiento)?, ¿o la segunda consulta será gratis? ¿Va a pagar por una consulta que no retorna ningún resultado (HTTP 404)? Asegúrese de que lo entiende bien todo.

Calidad de servicio (QoS)

La calidad de servicio importa mucho. Su meta es trabajar con una API la mas rápida posible y con pocas interrupciones. Lamentablemente no se trata de desempeños fáciles de probar. En efecto la calidad de servicio cambia mucho con el tiempo, y numerosas APIs ofrecen dos niveles de QoS diferentes según utiliza la versión gratis o pagada… A veces incluso puede elegir diferentes suscripciones con diferentes tiempos de respuesta.

Soporte de las consultas paralelas

Según la manera de integrar la API, quizás tenga ganas de acelerar las cosas consultando la API con varias consultas simultaneas en lugar de la configuración secuencial clásica. Yo utilizo Golang con ese fin. Pero cuídese: muchas APIs no soportan las consultas paralelas y cuando las soportan imponen sistemáticamente una limite. En este caso asegúrese de pedir que es esta limite (no siempre esta mencionado en la doc) y adapte su programa.

Este articulo sera un buen memo para mi, ¡espero que para usted también!

Also available in English | Existe aussi en français
Utilización de una API REST: Go vs Python

Se encuentran API en todas partes hoy en día. Imagine: quiere recoger información sobre sus clientes potenciales gracias a sus emails. Bueno hay una API para hacer esto. ¿Necesita geocodar una dirección? También existe una API para hacer esto. Como desarrollador, integro regularmente nuevas API en mi sistema, en Python o en Go. Los dos métodos son bastante diferentes. Comparemolos trabajando sobre un caso “borderline”: enviar datos JSON en el body de una consulta POST.

Un ejemplo real

Recientemente utilicé la API NameAPI.org para separar un nombre entero en nombre y apellido, y conocer el género de la persona.

Su API espera datos JSON puestos en el body de una consulta POST. Ademas, el Content-Type de la consulta tiene que ser application/json y no multipart/form-data. Se trata de un caso especial porque en general se envían los datos POST a través del header de la consulta, y si se quiere enviarlos en el body de la consulta (en el caso de datos JSON complejos por ejemplo) el Content-Type común es multipart/form-data.

Aquí esta el JSON que se quiere enviar:

{
  "inputPerson" : {
    "type" : "NaturalInputPerson",
    "personName" : {
      "nameFields" : [ {
        "string" : "Petra",
        "fieldType" : "GIVENNAME"
      }, {
        "string" : "Meyer",
        "fieldType" : "SURNAME"
      } ]
    },
    "gender" : "UNKNOWN"
  }
}

Se puede hacerlo fácilmente con cURL:

curl -H "Content-Type: application/json" \
-X POST \
-d '{"inputPerson":{"type":"NaturalInputPerson","personName":{"nameFields":[{"string":"Petra Meyer","fieldType":"FULLNAME"}]}}}' \
http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?apiKey=<API-KEY>

Y aquí esta la respuesta JSON de NameAPI.org:

{
"matches" : [ {
  "parsedPerson" : {
    "personType" : "NATURAL",
    "personRole" : "PRIMARY",
    "mailingPersonRoles" : [ "ADDRESSEE" ],
    "gender" : {
      "gender" : "MALE",
      "confidence" : 0.9111111111111111
    },
    "addressingGivenName" : "Petra",
    "addressingSurname" : "Meyer",
    "outputPersonName" : {
      "terms" : [ {
        "string" : "Petra",
        "termType" : "GIVENNAME"
      },{
        "string" : "Meyer",
        "termType" : "SURNAME"
      } ]
    }
  },
  "parserDisputes" : [ ],
  "likeliness" : 0.926699401733102,
  "confidence" : 0.7536487758945387
}

¡Ahora veamos como hacer esto en Go y en Python!

Realización en Go

Código

/*
Fetch the NameAPI.org REST API and turn JSON response into a Go struct.

Sent data have to be JSON data encoded into request body.
Send request headers must be set to 'application/json'.
*/

package main

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)

// url of the NameAPI.org endpoint:
const (
    url = "http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?" +
        "apiKey=<API-KEY>"
)

func main() {

    // JSON string to be sent to NameAPI.org:
    jsonString := `{
        "inputPerson": {
            "type": "NaturalInputPerson",
            "personName": {
                "nameFields": [
                    {
                        "string": "Petra",
                        "fieldType": "GIVENNAME"
                    }, {
                        "string": "Meyer",
                        "fieldType": "SURNAME"
                    }
                ]
            },
            "gender": "UNKNOWN"
        }
    }`
    // Convert JSON string to NewReader (expected by NewRequest)
    jsonBody := strings.NewReader(jsonString)

    // Need to create a client in order to modify headers
    // and set content-type to 'application/json':
    client := &http.Client{}
    req, err := http.NewRequest("POST", url, jsonBody)
    if err != nil {
        log.Println(err)
    }
    req.Header.Add("Content-Type", "application/json")
    resp, err := client.Do(req)

    // Proceed only if no error:
    switch {
    default:
        // Create a struct dedicated to receiving the fetched
        // JSON content:
        type Level5 struct {
            String   string `json:"string"`
            TermType string `json:"termType"`
        }
        type Level41 struct {
            Gender     string  `json:"gender"`
            Confidence float64 `json:"confidence"`
        }
        type Level42 struct {
            Terms []Level5 `json:"terms"`
        }
        type Level3 struct {
            Gender           Level41 `json:"gender"`
            OutputPersonName Level42 `json:"outputPersonName"`
        }
        type Level2 struct {
            ParsedPerson Level3 `json:"parsedPerson"`
        }
        type RespContent struct {
            Matches []Level2 `json:"matches"`
        }

        // Decode fetched JSON and put it into respContent:
        respContentBytes, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            log.Println(err)
        }
        var respContent RespContent
        err = json.Unmarshal(respContentBytes, &respContent)
        if err != nil {
            log.Println(err)
        }
        log.Println(respContent)
    case err != nil:
        log.Println("Network error:", err)
    case resp.StatusCode != 200:
        log.Println("Bad HTTP status code:", err)
    }

}

Explicaciones

Nos enfrentamos a 2 problemas:

  • tenemos que utilizar http.Client, NewRequest(), client.Do(req), y req.Header.Add("Content-Type", "application/json") par poner datos en el body y cambiar el Content-Type. Son muchas etapas.
  • recibir el JSON de NameAPI en Go es difícil porque tenemos que crear un struct que tenga la misma estructura que el JSON.

Realización en Python

Código

"""
Fetch the NameAPI.org REST API and turn JSON response into Python dict.

Sent data have to be JSON data encoded into request body.
Send request headers must be set to 'application/json'.
"""

import requests

# url of the NameAPI.org endpoint:
url = (
    "http://rc50-api.nameapi.org/rest/v5.0/parser/personnameparser?"
    "apiKey=<API-KEY>"
)

# Dict of data to be sent to NameAPI.org:
payload = {
    "inputPerson": {
        "type": "NaturalInputPerson",
        "personName": {
            "nameFields": [
                {
                    "string": "Petra",
                    "fieldType": "GIVENNAME"
                }, {
                    "string": "Meyer",
                    "fieldType": "SURNAME"
                }
            ]
        },
        "gender": "UNKNOWN"
    }
}

# Proceed, only if no error:
try:
    # Send request to NameAPI.org by doing the following:
    # - make a POST HTTP request
    # - encode the Python payload dict to JSON
    # - pass the JSON to request body
    # - set header's 'Content-Type' to 'application/json' instead of
    #   default 'multipart/form-data'
    resp = requests.post(url, json=payload)
    resp.raise_for_status()
    # Decode JSON response into a Python dict:
    resp_dict = resp.json()
    print(resp_dict)
except requests.exceptions.HTTPError as e:
    print("Bad HTTP status code:", e)
except requests.exceptions.RequestException as e:
    print("Network error:", e)

Explicaciones

¡La biblioteca Request lo hace casi todo en una sola linea: resp = requests.post(url, json=payload)!

Recibir el JSON retornado por NameAPI se hace en una linea también: resp_dict = resp.json().

Conclusión

Python es el ganador. La simplicidad de Python y su cantidad de bibliotecas disponibles nos ayudan mucho.

Aquí no hablamos del desempeño. Si el desempeño de la integracion que hace es importante para usted, Go puede ser una muy buena elección. Pero simplicidad y desempeño no están compatibles…

Also available in English | Existe aussi en français
¿Cómo acelerar el web scraping con Go (Golang) y concurrencia?

Desarrollo web scrapers en Python desde varios años. La simplicidad de Python permite realizar prototipos rápidos y sus numerosas bibliotecas son muy útiles para el scraping y el parsing de los resultados (Requests, Beautiful Soup, Scrapy, …). Sin embargo, cuando se empieza a interesarse al desempeño de su scraper, Python tiene limites y Go nos ayuda mucho.

¿Porqué Go?

Cuando se trata de acelerar la recuperación de información desde el web (para scraping HTML, como para fetching de API), dos posibilidades de optimización principales existen:

  • acelerar la recuperación del recurso web (p. ej. descargar la pagina http://example.com/hello.html)
  • acelerar el parsing del información recuperado (p. ej. recuperar todas las url contenidas en hello.html)

Se puede mejorar el desempeño del parsing mejorando su código, utilizando un parser mas rápido como lxml, o le asignando más recursos maquina a nuestro scraper. A pesar de todo, se da cuenta de que a menudo la optimización del parsing es insignificante y que el cuello de botella es el acceso red (es decir el descargamiento de la pagina web).

Pues la solución es paralelizar el descargamiento de las paginas web. ¡Por eso Go está una bueno elección!

La programación concurrente es un ámbito muy complicado y Go puede hacerlo bastante fácil. Go es un lenguaje moderno que fue pensado para la concurrencia desde el principio. Al contrario, Python es un lenguaje más antiguo que, a pesar de numerosos esfuerzos recientes, es más complejo cuando se quiere programar un scraper concurrente.

¡Hay otras ventajas utilizar Go, pero vamos a hablar de esto en otro momento!

Instale Go

Ya realicé un pequeño tuto sobre la instalación de Go en Ubuntu.

Si quiere instalar Go en otra plataforma, puede utilizar la doc oficial.

Un scraper concurrente simple

Nuestro scraper se contenta de abrir una lista de paginas web que se le da primero. Después averigua que obtiene un código HTTP 200 (significa que el servidor retorno la pagina HTML sin error). No hay parsing HTML aquí porque la meta está focalizarse sobre el desempeño del acceso a la red. ¡A usted le toca escribir más!

Código final


/*
Open a series of urls.

Check status code for each url and store urls I could not
open in a dedicated array.
Fetch urls concurrently using goroutines.
*/

package main

import (
    "fmt"
    "net/http"
)

// -------------------------------------

// Custom user agent.
const (
    userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) " +
        "AppleWebKit/537.36 (KHTML, like Gecko) " +
        "Chrome/53.0.2785.143 " +
        "Safari/537.36"
)

// -------------------------------------

// fetchUrl opens a url with GET method and sets a custom user agent.
// If url cannot be opened, then log it to a dedicated channel.
func fetchUrl(url string, chFailedUrls chan string, chIsFinished chan bool) {

    // Open url.
    // Need to use http.Client in order to set a custom user agent:
    client := &http.Client{}
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("User-Agent", userAgent)
    resp, err := client.Do(req)

    // Inform the channel chIsFinished that url fetching is done (no
    // matter whether successful or not). Defer triggers only once
    // we leave fetchUrl():
    defer func() {
        chIsFinished <- true
    }()

    // If url could not be opened, we inform the channel chFailedUrls:
    if err != nil || resp.StatusCode != 200 {
        chFailedUrls <- url
        return
    }

}

func main() {

    // Create a random urls list just as an example:
    urlsList := [10]string{
        "http://example1.com",
        "http://example2.com",
        "http://example3.com",
        "http://example4.com",
        "http://example5.com",
        "http://example10.com",
        "http://example20.com",
        "http://example30.com",
        "http://example40.com",
        "http://example50.com",
    }

    // Create 2 channels, 1 to track urls we could not open
    // and 1 to inform url fetching is done:
    chFailedUrls := make(chan string)
    chIsFinished := make(chan bool)

    // Open all urls concurrently using the 'go' keyword:
    for _, url := range urlsList {
        go fetchUrl(url, chFailedUrls, chIsFinished)
    }

    // Receive messages from every concurrent goroutine. If
    // an url fails, we log it to failedUrls array:
    failedUrls := make([]string, 0)
    for i := 0; i < len(urlsList); {
        select {
        case url := <-chFailedUrls:
            failedUrls = append(failedUrls, url)
        case <-chIsFinished:
            i++
        }
    }

    // Print all urls we could not open:
    fmt.Println("Could not fetch these urls: ", failedUrls)

}


Explicaciones

Este código está un poco más largo que lo que podría hacer con un lenguaje como Python, pero es muy razonable. Go es un lenguaje estático, pues declarar las variables toma un poquito más tiempo. ¡Pero mida el tiempo de ejecución de este programa, y va a ver la recompensa!

Tomamos 10 url al azar para el ejemplo.

Aquí las palabras claves están go, chan, y select:

  • go permite crear una nueva goroutine, es decir que fetchUrl sera ejecutado cada vez en una nueva goroutine concurrente.
  • chan es el tipo que representa un channel. Se utiliza los channels para comunicar entre goroutines (main también es una goroutine).
  • select ... case es un switch ... case dedicado a recibir los mensajes enviados por los channels. El programa continua solo cuando todas las goroutines han enviado un mensaje (sea para decir que el fetching de la url esta terminado, o sea para decir que el fetching fracasó).

Se habría podido crear ningún channel para este scraper, es decir solo crear goroutines y nos esperar información a cambio (por ejemplo si cada goroutine termina almacenando el resultado en base de datos). En este caso es posible que nuestra goroutine principal se termine mientras que otras goroutines todavía trabajan (no es necesariamente un problema ya que Go ejecuta todas la goroutines enteramente incluso si la main paró). Pero en realidad, es casi siempre necesario utilizar los channels para hacer comunicar nuestras goroutines.

¡Piense en limitar la velocidad!

Aquí la velocidad máximum es lo que se busca, en particular porque se hace scraping de urls todas diferentes. Sin embargo, si necesita descargar varias veces la misma url (en el caso del fetching de una API externa por ejemplo), tendrá que no superar un numero máximum de consultas concurrentes por segundo. En este caso se necesita implementar un contador (¡quizás en un próximo articulo!).

¡Enjoy el scraping!

Also available in English | Existe aussi en français
Instalar Go (Golang) 1.9 en Ubuntu 17.10

Aquí esta un pequeño memorando para los que quieren instalar Go (1.9) en su maquina Ubuntu (17.10). Cabe recordar Go es un lenguaje compilado, pues ne se necesita instalar Go en la maquina que va a alojar la aplicación final.

Actualizar los repositories, por si:

sudo apt-get update
sudo apt-get -y upgrade

Descargar y instalar Go :

sudo curl -O https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz  # Descargar el archivo. Cambie el nombre del archivo si necesita otra versión de Go o otra arquitectura
sudo tar -xvf go1.9.linux-amd64.tar.gz  # Extraer el archivo
sudo mv go /usr/local  # Desplazar los binarios hacia /usr/local
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile  # Se actualiza su perfil bash para que Go este en el PATH
source ~/.profile  # Tomar cambios en cuento

Ahora, Go esta instalado. Cree sur proyecto y inicialize las variables de entorno:

mkdir $HOME/mi_proyecto
mkdir $HOME/mi_proyecto/src
mkdir $HOME/mi_proyecto/src/mi_app
export GOPATH=$HOME/mi_proyecto

Luego cree sur app:

vim $HOME/mi_proyecto/src/mon_appli/mi_app.go

Que contiene lo siguiente:

package main

import "fmt"

func main() {
    fmt.Printf("hello world\n")
}

Compile la aplicación:

go install mi_app

Un ejecutable fue generado en una nueva carpeta bin. Ejecútelo:

$HOME/mi_proyecto/bin/mi_app

Debería obtener:

hello world

Para entender las diferencias entre go install, go build, y go run vaya por allá. Y si no quiere/puede instalar Go en su maquina, eche un vistazo a esta imagen Docker.

Enjoy !

Also available in English | Existe aussi en français
¿Por qué crear un blog como desarollador?

Ya esta, doy el paso. Hace un momento que tengo ganas de lanzar un blog. Quería absolutamente un blog multilingüe, por eso necesité solucionar un montón de problemas técnicos en cuento a la gestión de las idiomas. Como desarrollador pienso que este blog tendrá varios beneficios.

Compartir

La mayoría de los desarrolladores piensan que no tienen bastante experiencia para publicar en Internet. ¡Es una prueba de humildad pero no siempre es una buena análisis! El mundo del desarrollo informático es tan vasto que siempre se encuentra gente mas principiante que sí-mismo en muchos ámbitos. Están tranquilizados por el hecho de que otras personas tuvieran el mismo problema en el pasado.

Promocionar las idiomas locales

Yo soy muy triste que haya tan poco contenido cuanto al desarrollo traducido en español. Sé que el inglés se volvió la lingua franca en lo que concierne el informático pero mucha gente no puede aprovechar los blogs ingleses eficazmente. Que lastima porque personas competentes y motivadas por el desarrollo se enfrentan a esta barrera (no se aprende el ingles de la noche a la mañana…). ¡Defender su propia idioma es muy importante!

Bueno internacionalizar una app web es mucho trabajo adicional, y las herramientas actuales son difíciles de utilizar para mí (voy a hablar de esto en un otro articuló), pues se comprende que la gente no quiera hacerlo.

Estructurar sus ideas

Parece ser que escribir sus problemas técnicos en un blog ayuda mucho estructurar su pensamiento y entender mejor. ¡Vamos a ver! Pero es verdad que yo personalmente me gusta mucho documentar mi código porque me permite pensar conceptualizar mas y entender mejor. ¡Entonces el blog es como documentación!

Una pequeña cita:

Lo que se entiende bien, lo puede explicarse claramente.

– Nicolas Boileau

Also available in English | Existe aussi en français