Cargando datos con API-REST

¿Qué es API-REST?

API es la Application Programming Interface, es decir nuestra forma de interactuar con una aplicación. REST quiere decir REpresentational State Transfer. Normalmente cuando hablamos de REST, estamos hablando de una interfaz para obtener datos usando directamente HTTP, es decir es un tipo de web API.

De manera sencilla podemos decir que es una Interfaz con la que se interactúa mediante URLs normalmente para obtener datos.

La principal diferencia entre esto y una URL común, es que la URL de una página web devolverá algo que tu navegador puede interpretar y mostrar de forma "bonita", mientras que una API web mandará datos o instrucciones útiles para ti o tu ordenador.


In [1]:
# preserve
from IPython.display import HTML

Veamos la respuesta a una página web clásica


In [2]:
HTML('<iframe src="https://developer.github.com/v3/" width="700" height="400"></iframe>')


Out[2]:

Veamos la respuesta de una web api


In [3]:
HTML(url="https://api.github.com/")


Out[3]:
{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}","commit_search_url":"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}","emails_url":"https://api.github.com/user/emails","emojis_url":"https://api.github.com/emojis","events_url":"https://api.github.com/events","feeds_url":"https://api.github.com/feeds","followers_url":"https://api.github.com/user/followers","following_url":"https://api.github.com/user/following{/target}","gists_url":"https://api.github.com/gists{/gist_id}","hub_url":"https://api.github.com/hub","issue_search_url":"https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}","issues_url":"https://api.github.com/issues","keys_url":"https://api.github.com/user/keys","notifications_url":"https://api.github.com/notifications","organization_repositories_url":"https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}","organization_url":"https://api.github.com/orgs/{org}","public_gists_url":"https://api.github.com/gists/public","rate_limit_url":"https://api.github.com/rate_limit","repository_url":"https://api.github.com/repos/{owner}/{repo}","repository_search_url":"https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}","current_user_repositories_url":"https://api.github.com/user/repos{?type,page,per_page,sort}","starred_url":"https://api.github.com/user/starred{/owner}{/repo}","starred_gists_url":"https://api.github.com/gists/starred","team_url":"https://api.github.com/teams","user_url":"https://api.github.com/users/{user}","user_organizations_url":"https://api.github.com/user/orgs","user_repositories_url":"https://api.github.com/users/{user}/repos{?type,page,per_page,sort}","user_search_url":"https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"}

En definitiva, cuando utilizamos una WEB API, están involucrados:

  • Cliente: realiza la petición a través de la url
  • Servidor: da una respuesta a la url recibida
  • Protocolo: se establece en la especificación de la API

¿Cómo utilizar una API?

El uso de WEB APIs permite: obtener información que sería cosotsa de obtener y procesar de otra manera (incluso en tiempo real).

En algunas ocasiones, la API es pública y cualquiera puede hacer una petición, pero en otras es necesario tener una api key que nos identifica. Por lo tanto, el proceso para obtener datos suele ser:

  1. Comprobar si existe una API-REST para obtener dichos datos
  2. Obtener una api key en caso de que sea necesaria
  3. Leer la especificación de la API para saber cómo componer la URL y como interpretar la respuesta
  4. Utilizar la API, en nuestro caso desde Python.

Ejemplo: descargando datos de AEMET

Esta es la página principal de su API:

https://opendata.aemet.es/centrodedescargas/inicio

Aquí podemos encontrar: información general, el lugar donde obtener nuestra API key, una interfaz para acceder a los datos para público general


In [4]:
HTML('<iframe src="https://opendata.aemet.es/centrodedescargas/inicio" width="1000" height="400"></iframe>')


Out[4]:

Módulo requests

Aunque existen otras formas y librería para trabajar con HTTP en Python, el módulo requests está específicamente pensado para trabajar con APIs web.

Como siempre hasta ahora, lo primero es importarlo:


In [5]:
import requests

Necesitaremos cargar nuesta API key, lo más cómodo es almacenarla en un fichero y cargarla desde ahí. Creemos una función para leerla:


In [6]:
# preserve
def load_api_key(file):
    """Returns the contents in the file without the final line break
    """
    with open(file, 'r') as f:
        api_key = f.read().rstrip()
    return api_key

In [7]:
# cargamos la api_key
api_key = load_api_key("../../apikey-aemet.txt")

Debemos saber cuál es la url a la que vamos a hacer la petición:


In [8]:
# Fijamos la url y los parámetros
# Predicción costera de Asturias, Cantabria y País Vasco debemos introducir
# https://opendata.aemet.es/opendata/api/prediccion/maritima/costera/costa/41

url = "https://opendata.aemet.es/opendata/api/prediccion/maritima/costera/costa/41"
querystring = {"api_key": api_key}

Por último, lanzamos la petición:


In [9]:
# Lanzamos la request
response = requests.get(url, params=querystring, verify=False)


/home/asaez/miniconda3/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py:852: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)

Comprobando la respuesta:


In [10]:
# Vemos la respuesta
response


Out[10]:
<Response [200]>

Vemos que hemos obtenido una respuesta con código 200, esto quiere decir, en el código habitual de las API-REST, que todo ha ido bien. De hecho, es conveniente ver que todo ha ido bien antes de hacer nada más:


In [11]:
# código de estado
response.status_code == requests.codes.ok


Out[11]:
True

Otra información que podemos obtener es, por ejemplo;


In [12]:
# preserve
# Tiempo en procesar la petición
print("Elapsed: ", response.elapsed)
# Información del servidor
print("Headers: ", response.headers)


Elapsed:  0:00:00.150871
Headers:  {'Date': 'Sun, 04 Jun 2017 21:05:01 GMT', 'Server': 'Apache/2.2.15 (CentOS)'}

Pero... ¿dónde están nuestros datos?


In [13]:
response.content


Out[13]:
b'{\n  "descripcion" : "exito",\n  "estado" : 200,\n  "datos" : "https://opendata.aemet.es/opendata/sh/43cdfb13",\n  "metadatos" : "https://opendata.aemet.es/opendata/sh/8846af34"\n}'

¡Parece que esto es un json!


In [14]:
content = response.json()

content


Out[14]:
{'datos': 'https://opendata.aemet.es/opendata/sh/43cdfb13',
 'descripcion': 'exito',
 'estado': 200,
 'metadatos': 'https://opendata.aemet.es/opendata/sh/8846af34'}

Efectivamente, la mayoría de las WEB APIs devuelven json o xml. Pero, otra vez... ¿dónde están nuestros datos?


In [15]:
r_meta = requests.get(content['metadatos'], verify=False)
r_data = requests.get(content['datos'], verify=False)

if r_meta.status_code == requests.codes.ok:
    metadata = r_meta.json()
    
if r_data.status_code == requests.codes.ok:
    data = r_data.json()


/home/asaez/miniconda3/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py:852: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
/home/asaez/miniconda3/lib/python3.5/site-packages/requests/packages/urllib3/connectionpool.py:852: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)

In [16]:
print(metadata[0].keys())
print(data[0].keys())


dict_keys(['formato', 'unidad_generadora', 'descripcion', 'periodicidad', 'campos'])
dict_keys(['nombre', 'origen', 'id', 'aviso', 'tendencia', 'situacion', 'prediccion'])

In [17]:
metadata[0]['periodicidad']


Out[17]:
'Dos veces al día (12:00 y 20:00) h.o.p'

In [18]:
data[0]['prediccion']


Out[18]:
{'fin': '2017-06-05',
 'inicio': '2017-06-04',
 'zona': [{'id': 8033010,
   'nombre': 'Aguas costeras de Asturias',
   'subzona': {'id': 8033010,
    'nombre': 'Aguas costeras de Asturias',
    'texto': 'VARIABLE, PRINCIPALMENTE OESTE, FUERZA 1 A 3 ARRECIANDO A SUROESTE FUERZA 4 O 5 AL OESTE DE PENAS A PARTIR DEL MEDIODIA. MAREJADILLA AUMENTANDO A MAREJADA AL OESTE DE PENAS A PARTIR DEL MEDIODIA. MAR DE FONDO DEL NOROESTE DE 1 A 2 METROS.'}},
  {'id': 8063910,
   'nombre': 'Aguas costeras de Cantabria',
   'subzona': {'id': 8063910,
    'nombre': 'Aguas costeras de Cantabria',
    'texto': 'VARIABLE FUERZA 1 A 4. MAREJADILLA, LOCALMENTE MAREJADA AL ESTE DE AJO. MAR DE FONDO DEL NOROESTE DE 1 A 2 METROS.'}},
  {'id': 8154810,
   'nombre': 'Aguas costeras de Bizkaia',
   'subzona': {'id': 8154810,
    'nombre': 'Aguas costeras de Bizkaia',
    'texto': 'VARIABLE, PRINCIPALMENTE NOROESTE, FUERZA 1 A 4. MAREJADILLA O MAREJADA. MAR DE FONDO DEL NOROESTE DE 1 A 2 METROS.'}},
  {'id': 8152010,
   'nombre': 'Aguas costeras de Gipuzkoa',
   'subzona': {'id': 8152010,
    'nombre': 'Aguas costeras de Gipuzkoa',
    'texto': 'VARIABLE, PRINCIPALMENTE NOROESTE, FUERZA 1 A 4. MAREJADILLA O MAREJADA. MAR DE FONDO DEL NOROESTE DE 1 A 2 METROS.'}}]}

En definitiva, podríamos reagrupar todo lo anterior como:


In [19]:
# preserve

from warnings import warn

def get_prediction_for_cantabria(api_key):
    url = "https://opendata.aemet.es/opendata/api/prediccion/maritima/costera/costa/41"
    querystring = {"api_key": api_key}
    
    response = requests.get(url, params=querystring, verify=False)
    
    prediction = None
     
    if response.status_code == requests.codes.ok:
        r_data = requests.get(content['datos'], verify=False)
        if r_data.status_code == requests.codes.ok:
            data = r_data.json()
            prediction = data[0]['prediccion']
    
    elif response.status_code == requests.codes.TOO_MANY_REQUESTS:
        warn('Too many requests')
        
    elif response.status.code == requests.codes.NOT_FOUND:
        warn('No data for the response')
        
    else:
        warn('Code error {}'.format(response.status_code))
            
    return prediction

Conclusiones

  • Si el proveedor de los datos dispone de una API-REST podemos aprovecharla para automatizar la obtención de los mismos
  • En la mayoría de los casos necesitaremos una api-key y conocer la especificación de la API
  • A través de la URL podemos configurar nuestra petición
  • El módulo requests nos permite realizar este proceso en Python de manera secilla


¡Síguenos en Twitter!


Este notebook ha sido realizado por: Álex Sáez y Francisco Navarro



<span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">Curso de introducción a Python: procesamiento y análisis de datos</span> por <span xmlns:cc="http://creativecommons.org/ns#" property="cc:attributionName">Juan Luis Cano Rodriguez, Alejandro Sáez Mollejo y Francisco J. Navarro Brull</span> se distribuye bajo una Licencia Creative Commons Atribución 4.0 Internacional.
La mayor parte de material de este curso es un resumen adaptado del magnífico Curso de AeroPython realizado por: Juan Luis Cano y Álex Sáez