Skip to content

4 Implementación: Requests

Manuel Soto edited this page May 14, 2019 · 1 revision

Implementación: Requests

Nota: Todo el código de este trabajo está hecho en Python3. Lo he elegido debido a su versatilidad, rendimiento y, más importante, simpleza de cara a las funcionalidades a implementar.

En flight_search/requests se ubican esbozos de código de cómo buscar vuelos utilizando requests. Hablo de esbozos puesto que más adelante veremos que surgieron varios problemas.

HTTP Requests

requests es el paquete de Python más recomendado para realizar peticiones HTTP. Su facilidad en los argumentos de entrada para los métodos GET y POST, así como en el retorno de dichas funciones (que es el recurso pedido en texto plano), son motivos para implementar una spider con requests.

Desde el punto de vista de nuestro trabajo, usar requests para buscar un vuelo es "canónico" por diversos motivos:

  • Introduciríamos el mínimo de datos necesarios para obtener información de un vuelo.
  • Podríamos elegir si enviar las cookies o no añadiéndolas como argumento al GET o al POST.
  • Solo pasamos por los dominios justos y necesarios, sin terceros que se encarguen del marketing.

En el papel se ve bastante bien la planificación del trabajo, pero la realidad es que nos encontramos con un primer problema al intentar sintetizar nuestro tráfico con Iberia y Ryanair.

Primer Problema: Peticiones POST

Intentando empezar la fase de implementación con Iberia, vemos que la URL clave es https://www.iberia.com/web/dispatchSearchFormHOME.do. La diferencia con Ryanair es el hecho de que se hace request de dicha URL con un POST. Esto no supondría problema si no fuera por lo bastante "modelo de caja negra" que es dicha petición. Nos encontramos con una serie de argumentos indescifrables, parecidos a cuando analizábamos algunas cookies (consultar flight_search/requests/iberia_params.txt).

A continuación se muestran las funciones mediante las que realizábamos la petición POST, cuyos parámetros se leían de un .txt (incluido en el apéndice el ejemplo de Iberia):

def simple_post(url,params):
    """
    Attempts to send a post http request with the params given,
    which is a dictionary of pairs variable_name : value.
    Returns the request response, otherwise None.
    """
    try:
        with closing(requests.post(url,params)) as resp:
            if is_good_response(resp):
                return resp.content
            else:
                return None

    except RequestException as e:
        log_error('Error during requests to {0} : {1}'.format(url, str(e)))
        return None

def print_req(req):
    print('{}\n{}\n{}\n\n{}'.format(
        '-----------START-----------',
        req.method + ' ' + req.url,
        '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))

def read_params(path):
    params = {}
    with open(path,'r') as f:
        for line in f:
            if(len(line.split())==1):
                key = line.split()[0]
                val = None
            else:
                (key, val) = line.split()
            params[key] = val
    return params

print(simple_get("https://www.iberia.com/web/dispatchSearchFormHOME.do"))

Código 1: Funciones de POST con requests

Por no poder entender el funcionamiento de los POSTS de Iberia, nos vemos incapacitados para:

  • Obtener el .html con la información del vuelo a buscar, en lugar de uno que nos informa del error y con información predeterminada.
  • Saber manipular los parámetros de búsqueda como el que rellena el formulario de la página web.

Por ello, nos vemos obligados a abandonar la investigación con Iberia y cualquier otra aerolínea que requiera de POSTS siempre y cuando la dificultad de sus parámetros de entrada sea de este nivel. Abandonar Iberia sería en el caso de proseguir con requests, claro.

Ejemplo: GET Ryanair

Por otro lado, mostramos en esta subsección la simpleza de realizar un GET con Ryanair.

Si, por ejemplo, queremos buscar vuelos desde Madrid a Palma de Mallorca el 30 de Mayo de 2019, la URL del GET es:

https://www.ryanair.com/es/es/booking/home/MAD/PMI/2019-05-30/1/0/0/0?Discount=0

Es fácil ver que para poder elegir otros vuelos tan solo de cambiar los códigos de aeropuerto de origen (MAD) y destino (PMI), así como la fecha en formato YYYY-MM-DD. Los unos y ceros a la derecha de la fecha indican el número de distintos pasajeros para los que buscamos vuelo.

A continuación mostramos las funciones implementadas para GET de la URL dada:

def simple_get(url):
    """
    Attempts to get the content at `url` by making an HTTP GET request.
    If the content-type of response is some kind of HTML/XML, return the
    text content, otherwise return None.
    """
    try:
        with closing(get(url, stream=True)) as resp:
            if is_good_response(resp):
                return resp.content
            else:
                return None
    except RequestException as e:
        log_error('Error during requests to {0} : {1}'.format(url, str(e)))
        return None

url =
"https://www.ryanair.com/es/es/booking/home/MAD/PMI/2019-05-30/1/0/0/0?Discount=0"
print(simple_get(url))

Código 2: Funciones de GET con requests

No hay función alguna de modificar la URL según los destinos y la fecha (de momento) debido al hecho de que nos encontramos con un segundo y último problema al ejecutar este código.

Segundo Problema: Cabeceras y Scripts

El segundo problema de requests en nuestra investigación es de tal calibre que hace finiquitar este camino de abordaje al problema. Es decir, no se van a sintetizar los procesos de búsqueda con requests.

Dicho problema aparece cuando un host analiza las cabeceras de una petición de forma que rechace paquetes inadecuados. No especificar el navegador, fechas actuales, idioma, etc. son signos para dichos hosts de poder sufrir cualquier tipo de ataque (dado que es más probable que el tráfico sea sintético). Rechazar paquetes inadecuados puede traducirse en enviar .html de errores, y ello, por supuesto, no contiene información alguna sobre nuestras búsquedas.

Resulta que Ryanair parece aplicar este tipo de análisis en el tráfico que intercambia con nosotros y, con ello, nos responde con páginas predeterminadas. Podríamos intentar analizar qué headers son necesarios y cuáles no, pero hacer esto supone:

  1. Alejarnos más de poder intercambiar tráfico canónico para poder manipularlo.
  2. Demasiado tiempo analizando parámetros opacos sin la certeza de que vaya a funcionar.

Aparte, pese a que algunas veces hemos conseguido que el host nos aceptara, se añade otra dificultad: Un GET de scripts embebidos es sinónimo de no recibir los datos que queremos (puesto que dicho script no tiene navegador donde ejecutarse). En el caso de las aerolíneas, scripts que muestran resultados de búsqueda es el recurso más utilizado.

Para completar nuestra araña web basada en requests, solo faltaba parsear los .html obtenidos con una librería como BeautifulSoup. En flight_search/requests se adjuntan dos .html, uno de Expedia donde no se hace uso de estos filtros y, con ello, si parseamos con grep, por ejemplo, encontramos los precios de distintos vuelos. El otro .html es uno de Ryanair, con el cual vemos que no se obtiene ningún dato de precios buscados.

Síntesis

En definitiva, crear una araña web con requests es simple cuanto más simple sea el host de la página web. Para nuestra investigación, es claro que nos daría demasiadas dificultades llevar adelante la implementación con este paquete por estos motivos:

  • Las peticiones POSTS que tengan parámetros difíciles de analizar serán imposibles de sintetizar a nuestro antojo.
  • Cualquier petición en general hacia un host "exigente" con las cabeceras intercambiadas aumenta considerablemente la dificultad en conseguir generar nuestro propio tráfico.

Aparte, señalar que tenemos imposible desde requests poder comunicarnos desde IP "anónima" (o al menos variable para escapar del IP tracking), luego se está despreciando un factor importante para la discriminación perfecta.