Skip to content

5 Implementación: Selenium

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

Implementación: Selenium

Por supuesto, no todo está acabado tras ver que requests no es apto para responder a nuestra pregunta. Queda un último (último por el hecho de ser el que funcionó) recurso.

En flight_search/selenium está el programa flight_search.py (en un alarde de creatividad); que se comentará en este capítulo.

Pseudocódigo y Estructura

flight_search es un programa de búsquedas de vuelos hecho en Python dados una serie de argumentos de ejecución útiles para nuestra investigación (y para buscar vuelos como tal).

El pseudocógido de este programa es simple cuando no se entran en detalles de Selenium.

  Recoger argumentos de entrada (verbosidad, privacidad y formulario de búsqueda)
  Preparar driver de selenium según la privacidad dada
  Según la compañía en la que buscar, entrar en su función de búsqueda
  Recoger resultados de horas y precios
  Si lo piden los argumentos:
    Guardar vuelo más barato con precio y timestamp en DB
  Cerrar driver y finalizar.

Código 3: Pseudocódigo del flujo principal de flight_search

Este pseudocódigo es más bien una enumeración debido a su linealidad, pero considero importante tener claro este flujo principal dado que deja clara una modularización del código.

Con ello, a continuación se listan los módulos (y otros archivos) que dan lugar a flight_search:

  • cookie_data/: Carpeta con las cookies que se cargan y almacenan en cada búsqueda realizada sin privacidad alguna.
  • drivers/: Carpeta con los ejecutables de geckodriver y PhantomJS, necesarios para poder navegar automatizadamente con Selenium en Firefox y Tor, respectivamente.
  • flight_data/: Carpeta que contiene:
    • flight_data_db.sqlite: Base de datos administrada con sqlite3 con datos sobre vuelos buscados.
    • Múltiples .txt con datos sobre un vuelo en específico, con cada línea un par precio, timestamp de cuando se buscó.
  • flight_search.py: Programa principal. Su código es únicamente el del pseudocódigo anterior con algunas funciones extra para guardar en la base de datos y ordenar según el precio.
  • Makefile: Con instrucciones para lanzar los procesos de tandas de búsquedas.
  • log/: Carpeta con logs de cuando se realiza una tanda de búsquedas. Es simplemente a donde paso la salida de ejecución de mi Makefile para después buscar qué errores han habido.
  • searches/: Carpeta con bash scripts de las tandas de búsquedas de cada aerolínea y .txt con los vuelos que hemos fijado.
  • src/: Carpeta con el código fuente del resto de módulos. Contiene:
    • Driver.py: Clase Driver, con funciones de preparación y cierre del Driver de selenium.
    • Iberia.py y Ryanair.py: Módulos que implementan la búsqueda de vuelos y recogida de precios y horas. Más adelante se explicará por qué hemos de distinguir compañías entre módulos.

Argumentos de Ejecución

Para entrar mejor en contexto con lo que le pediremos a flight_search de cara a nuestros experimentos, considero importante saber qué argumentos de ejecución tiene este programa. Los argumentos han sido recogidos mediante la librería argparse de Python.

La salida de flight_search.py -h nos informa bastante bien de dichos argumentos:


  usage: flight_search.py [-h] [-c | -t] [-f] [-v] [-hd] company fromc to date

  A headless flight searcher

  positional arguments:
    company          select company of the flight
    fromc            from (recommended city name or airport code)
    to               to (recommended city name or airport code)
    date             date (MM/DD/YYYY)

  optional arguments:
    -h, --help       show this help message and exit
    -c, --nocookies  disables cookies
    -t, --tor        search using TOR
    -f, --file       store (timestamp,depart hour,lowest price) in a generated
                     archive in 'flight_data/'
    -v, --verbosity  program verbosity
    -hd, --headless  use headless browser

Texto 4: Menú de ayuda de flight_search

Algunos comentarios sobre estos parámetros de entrada:

  • -c y -t son claves para nuestra investigación:
    • Sin ninguno de ellos estamos en el escenario de buscar con cookies e IP tracking
    • Con -c nos situamos en navegación privada sin cookies pero con IP tracking.
    • Con -t nos situamos en el escenario de navegación privada, sin cookies y con IP "anónima" (consultar la sección 6 para entender estas comillas).
  • -f es el encargado de "Si lo piden los argumentos" de nuestro pseudocódigo anterior.
  • -v y -hd han sido utilizados para debugging. Con ellos, el programa iba informando de cada paso por el que iba, así como cuando no se introducía -hd se puede ver qué se va introduciendo en el navegador y cómo se avanza.

Selenium: Web Browser Automation

Antes de entrar en detalles de implementación una vez ya queda presentado flight_search, en esta sección daré a conocer a selenium.

Selenium es una librería implementada tanto para Python como para Javascript. A grosso modo, su funcionamiento consiste en el de abrir un driver y ejecutar en dicho driver comandos como obtener URL's, buscar elementos HTML, rellenar campos en formularios, etc. Por ello, Selenium es una librería bastante útil para el debugging de páginas web: ponemos a prueba la concurrencia del servidor y la robustez en cualesquiera parámetros de entrada para los formularios.

Por otro lado, claro está, esto convierte a Selenium en una herramienta para el lado opuesto de los usuarios de una página web: intentar buscar en ella vulnerabilidades, poner a prueba el servidor y, en el caso de nuestro proyecto, automatizar tareas de las que se debería encargar un humano. Dicha tarea es, por supuesto, buscar un vuelo y apuntar sus precios.

Se listan ahora algunas de las funciones principales para conseguir el funcionamiento de flight_search:

Inicialización del Driver

Para inicializar un Driver hemos de llamar al constructor adecuado en función del navegador que queramos utilizar. En nuestro caso, este podía ser o bien Firefox o bien Tor (más adelante se hablará de Selenium con Tor).

En el caso de Firefox, en una primera instancia el driver se asigna así:

driver = webdriver.Firefox()

Al __init__() de Firefox se le pueden asignar bastantes argumentos de entrada. De ellos destacamos options y firefox_profile. Ambos se inicializan con respectivos constructores y de estos cambiamos atributos del objeto. Por ejemplo, para iniciar el driver de forma headless (no mostrar la ventana):

  options = Options()
  if headless and v:
    print("Setting headless mode...")
   options.headless = headless

Código 4: Estableciendo modo headless

De firefox_profile hablaremos en detalle en la sección Distinción de Contextos.

Cierre del Driver

Cerrar el driver inicializado es tan simple como:

driver.close()

Realmente en algunos contextos de distinción supondrá mayor esfuerzo cerrar el driver, pero para el caso general se procede de esta forma.

Cargar enlaces

Cargar un enlace también es fácil:

driver.get(url)

Y la distinción es mínima en este caso en función del contexto.

Encontrar Elementos e Interactuación

Pasamos a la sección clave para entender y manejar Selenium. Una vez ya tenemos nuestro driver con la configuración adecuada, en la URL objetivo, ya solo es cuestión de navegar "como un humano". Esto, en el lenguaje del paquete selenium, se traduce en buscar elementos del HTML e interactuar. Comentar que esto supone analizar una página con las herramientas de "Inspeccionar Elemento" y con ello saber cómo referenciárselos a nuestro driver.

Para buscar elementos tenemos varias opciones. Destacamos tres:

  • driver.find_elements_by_xpath(str): El método más recomendado. Xpath es sintaxis que define partes de un XML. Esto nos permite realizar queries bastante versátiles. Ejemplo:

    FromElement = driver.find_elements_by_xpath("//input[@placeholder='Departure airport']")

  • driver.find_elements_by_id(str): Este método y el siguiente pasan a estar incluidos dentro del primer método. Los he incluido dado que si está claro el campo que distingue nuestro elemento HTML de otros, podemos ahorrar sintaxis Xpath con estos otros métodos. El id de un elemento HTML es único por elemento, pero no todos los desarrolladores web se lo asignan a su página. Ejemplo:

    FromElement = driver.find_elements_by_xpath("//input[@placeholder='Departure airport']")

  • driver.find_elements_by_class(str): Su contexto de uso es el mismo que para find_elements_by_id. Ejemplo:

    driver.find_elements_by_class_name('hour')

Un último aspecto importante a destacar a la hora de buscar elementos es que existen estos métodos exactamente iguales pero para encontrar uno único. Serían find_element_by_xpath(str) y análogos. Considero más correcto utilizarlos en plural por la comprobación de errores en la búsqueda y por la posibilidad de iterar con los resultados.

Pasamos ahora a las funciones de interactuación, que son métodos de la clase WebElement:

  • elm.clear(): Usado en formularios. Vacía el contenido que haya en el campo de entrada.
  • elm.send_keys(str): Introduce str dentro del campo del formulario de elm.
  • elm.click(): Usado con botones. Clica en el elemento web.

Los ejemplos de estos métodos se entenderán mejor en conjunto en la siguiente sección.

Ejemplo: Búsquedas con Iberia

A continuación mostramos un ejemplo simplificado para entender la idea de cómo hemos buscado vuelos en los compañías dadas. En una primera instancia Iberia dio menos problemas y era más simple, luego aquí está el ejemplo:

  #Declaracion del driver
  driver = webdriver.Firefox()
  
  #Cargamos iberia.com
  url='https://www.iberia.com/'
  driver.get(url)

  #Obtenemos el campo "desde" y "hacia"
  FromElement = driver.find_elements_by_id('text-from-visible')
  ToElement = driver.find_elements_by_id('text-to-visible')
  if v:
      print('Inserting search parameters...')
  if(FromElement and ToElement):
  #Limpiamos los predeterminados (ubicación, busquedas anteriores)
  #e introducimos los parámetros que introdujo el usuario
      FromElement[0].clear()
      ToElement[0].clear()
      #'\r\n' evitan que aparezcan pop-ups de destinos y autocomplete
      FromElement[0].send_keys(depart+'\r\n')
      ToElement[0].send_keys(arrive+'\r\n')
  else:
      print('Fallo en la búsqueda de elementos')

  #Las comprobaciones de encontrar el elemento están en cada búsqueda
  #De ahora en adelante se omitirán por simplificar.

  #Solo ida
  NoReturnButton = driver.find_elements_by_id('ida')
  NoReturnButton[0].click()
  
  #Obtenemos el campo de Fecha y la introducimos
  DateElement = driver.find_elements_by_id('diaSalida')
  #Más adelante hablaremos de este sleep
  sleep(1)
  DateElement[0].send_keys(date+'\r\n\r\n')
  
  #Y más adelante hablaremos de randomClick
  randomClick = driver.find_elements_by_xpath('//button[@type="close"]')
  randomClick[0].click()

  #Obtenemos el boton de "Buscar"
  SearchButton = driver.find_elements_by_id('toAvailSubmit')
  #Y clicamos
  SearchButton[0].click()
  if v:
      print('Flight search queried, now waiting for results...')
  sleep(Iberia.SEARCH_WAIT)

Código 5: Ejemplo de Iberia. Rellenando el formulario de búsqueda

Nota: Este código ya no es el de Iberia.py. Tuvo que actualizarse por motivos que se comentarán en el siguiente capítulo. Dado que el objetivo de este apartado es entender cómo interactuar con los nodos HTML y este ejemplo sigue siendo el más simple, no se actualiza.

De momento está la búsqueda realizada. Tras esperar un tiempo predefinido para que carguen los resultados, se muestran los métodos usados para obtener los precios y horas de vuelo como parte de este ejemplo:

    @staticmethod
    def get_prices(driver,v=False):
        ret = []
        i = 1
        while True:
        #Es cuestión de inspeccionar la página de resultados de Iberia
        #para comprobar que se puede ir iterando según el nombre del id.
            PriceLabel = 
              driver.find_elements_by_id('precio_ida-tarifa_1-vuelo_'+str(i))
            if not PriceLabel:
                break
            ret.append(PriceLabel[0].text)
            i+=1
        return ret

    @staticmethod
    def get_hours(driver,v=False):
        ret_list = []
        ret_tuples = []
        #Análogo a guardar precios pero iterando de cuatro en cuatro
        #para las horas.
        for element in driver.find_elements_by_class_name('hour'):
            ret_list.append(element.text.split('\n')[0])
        for i in range(0,len(ret_list),4):
            ret_tuples.append((ret_list[i],ret_list[i+1]))
        return ret_tuples

Código 6: Ejemplo de Iberia. Obteniendo precios y horas de vuelo.

Por último, flight_search mostrará los resultados encontrados, liberará todo recurso pedido y, si la flag -f estaba especificada, ordenará la lista de resultados por precio y se guardarán en un .txt y, más importante, en nuestra base de datos de búsquedas de vuelos.

No considero importante mostrar en el ejemplo detalles sobre el uso de la librería sqlite3 de Python, cómo ordenar los precios o cómo escribir en un fichero; puesto que se salen del objetivo de analizar y comprender el funcionamiento de Selenium.

Con el esqueleto de este ejemplo, ya tenemos un autómata de búsquedas de vuelos, pero esto es solo importante si no existieran (o no nos fiáramos) de los buscadores oficiales que ya existen en páginas web. A continuación se mostrará el core de este trabajo: Las diferencias en código que consiguen distinguir unas privacidades de otras.

Distinción de Contextos

Por último, pero no menos importante, presentaré los detalles de implementación que dan lugar a poder estar en un entorno de pruebas en el cual se puede decir que estamos diferenciando nuestras tres casuísticas principales.

Nuestras tres ramas de ejecución son similares en lo detallado en la sección anterior: Recoger parámetros de entrada, cargar la URL, rellenar el formulario y obtener los precios y horarios de vuelos. Las diferencias residen principalmente en el proceso de preparación del driver y detalles menores de interactuación con la web.

Con Cookies

Podríamos pensar en una primera instancia que la navegación con cookies mediante Selenium es trivial, pero lo cierto es que Selenium inicia un nuevo perfil de navegador con cada instacia nueva del driver. En cierto modo, esto equivale a como si instaláramos y desinstaláramos Firefox tras cada ejecución de flight_search.

Para solventar este contratiempo y simular un usuario normal sin privacidad, la solución está en guardar y cargar la configuración necesaria del navegador tras cada ejecución sin -c ni -t. Como lo que diferenciamos aquí es en el conservar IP y utilizar Cookies, lo anterior se traduce en hacer un pickle de nuestras cookies.

Antes de mostrar en código cómo cargar y guardar cookies, destacar algo más: es necesario estar en la URL del dominio antes de cargar su cookie. Esto puede suponer un problema, ¿y si desde el primer GET ya se nos está analizando lo que hacemos? Pese a que dicho primer GET no tenga tanta importancia como el de realizar búsquedas, nos curamos en salud de la siguiente manera: accediendo a una URL sin importancia del mismo dominio.

Una URL que no tiene importancia (a ojos de clientes usuales) común en todo dominio (para mayor generalidad del código) es robots.txt, un recurso ubicado al inicio de la mayoría de hosts web del que hablaremos en el capítulo 6.

Con ello, estas son las funciones de carga y descarga de cookies:

    @staticmethod
    def save_cookies(company,driver):
        pickle.dump(driver.get_cookies(),
                    open("cookie_data/"+company+"_cookies.pkl","wb"))

    @staticmethod
    def load_cookies(company,driver,url):
        # este es el filename predeterminado segun la compañia
        file_name = "cookie_data/"+company+"_cookies.pkl"
        if os.path.exists(file_name):
            # vamos a robots.txt para ir al dominio
            def_url = url+'/robots.txt'
            driver.get(def_url)
            # cargamos el pickle
            cookies = pickle.load(open(file_name, "rb"))
            for cookie in cookies:
                try:
                    # añadimos cada cookie de la lista guardada
                    driver.add_cookie(cookie)
                except selenium.common.exceptions.InvalidCookieDomainException:
                    try:
                        # para las cookies secundarias de otros dominios
                        driver.get(cookie['domain'])
                        driver.add_cookie(cookie)
                        driver.get(def_url)
                    except:
                        print('Error: Invalid cookie:' + str(cookie))
                        cookies.remove(cookie)
        return driver

Código 7: Funciones de carga y descarga de cookies

Y, por ejemplo, las partes clave de carga y descarga de cookies en Ryanair:

    #Al inicio de search

    url='https://www.ryanair.com/'
    if not disable_cookies and not tor:
        if v:
            print("Loading cookies...")
        driver = Driver.load_cookies('ryanair',driver,url)
        driver.get(url)

    # [. . .]

    #Tras clicar en botón de búsqueda

    SearchButton[0].click()
    if not disable_cookies:
        if v:
            print("Saving cookies...")
        Driver.save_cookies('ryanair',driver)

    # [. . .]

Código 8: Ejemplo de carga y descarga de cookies

Sin Cookies, con IP Tracking

Después de lo explicado sobre Selenium y sus sesiones en el contexto de cookies e IP, se concluye fácilmente que no hemos de hacer nada para situarnos en este contexto cuando inicializamos driver. Pero, otra vez, para curarnos en salud, cambiamos la configuración de nuestro driver antes de acceder al correspondiente dominio.

Para ello, configuramos nuestro driver con un firefox_profile que navega en modo de incógnito (aunque sea redundante) y con las cookies desactivadas.

    #En search de Driver.py
    if disable_cookies:
      firefox_profile = webdriver.FirefoxProfile()
      firefox_profile.set_preference("browser.privatebrowsing.autostart", True)
      firefox_profile.set_preference("network.cookie.cookieBehavior",2)
      driver = webdriver.Firefox(options=options,firefox_profile=firefox_profile)

Código 9: Inicialización de un webdriver en incógnito con cookies deshabilitadas

Destacar que este último rasgo del driver genera diversos problemas en distintas webs:

  • En Iberia aparece un pop-up informando sobre el uso de cookies y pidiendo que se activen (aunque realmente podemos navegar sin activarlas) que imposibilitaba interactuar con el resto de la web. Puede parecer de solución fácil, pero recordemos que hemos de interactuar con dicho pop-up, y precisamente el nodo HTML de este mensaje estaba programado para dificultar su cierre desde Selenium. Tras varios intentos, este es el código que ignora el pop-up, que hace uso de ActionChains.
    if disable_cookies:
        # buscamos el boton de cierre del pop-up
        CookiesButtonElement = driver.find_elements_by_class_name('close')
        # creamos la cadena de acciones
        action = webdriver.common.action_chains.ActionChains(driver)
        # "movemos el puntero" (no altera el ratón) al botón de cerrar
        action.move_to_element_with_offset(CookiesButtonElement[0], 0, 0)
        # clicamos en el boton
        action.click()
        # pedimos que se performen las acciones especificadas
        action.perform()

Código 10: Cierre del pop-up sobre cookies

  • En Ryanair directamente nos dejan en espera si detectan que tenemos las cookies desactivadas. Aparece una pantalla de carga (que nunca cargará). Pese a que parezca imposible realizar pruebas con este contexto, resulta que la página de Ryanair que contiene los resultados de vuelo no realiza este check. ¿Cómo acceder a dichos resultados si no podemos rellenar el formulario de búsqueda? La respuesta está en lo que llamo "Atajos URL". Es básicamente hacer uso de lo aprendido en el capítulo anterior y llamar a driver.get(url), con url una que varía en función de los parámetros de búsqueda. A continuación se muestra shorcut del paquete Ryanair.py:
    @staticmethod
    def shortcut(driver,display,depart,arrive,date,tor,v,headless):
        MM,DD,YYYY = date.split('/')
        uri_date = YYYY+'-'+MM+'-'+DD
        def_url = 'https://www.ryanair.com/'
        shortcut_url = 
            'booking/home/'+depart+'/'+arrive+'/'+uri_date+'//1/0/0/0'
        if tor:
            driver.load_url(def_url)
            sleep(Ryanair.SEARCH_WAIT)
            url = driver.current_url + shortcut_url
            if v:
                print('Searching in '+ url + ' ...')
            driver.load_url(url)
            sleep(Ryanair.TOR_WAIT)
        else:
            url = def_url+'es/es/'+shortcut_url
            if v:
                print('Searching in '+ url + ' ...')
            driver.get(url)
            sleep(Ryanair.SEARCH_WAIT)
        if v:
            print('Getting results...')
        prices = Ryanair.get_prices(driver,v)
        if not prices:
            return None
        hours = Ryanair.get_hours(driver,v)

        return dict(zip(hours,prices))

Código 11: Implementación del "Atajo URL" de Ryanair

IP Anónima

Llegamos al contexto más difícil de este trabajo: IP anónima. Antes de continuar, comentar que es imposible (o al menos bastante difícil) ser IP anónima, y no es el caso de este contexto. Sin embargo, veremos que es posible escapar del IP tracking con el código que tenemos. La clave está en el uso de Tor, un navegador (más bien una rama de Firefox) con ciertas características especiales a la hora de navegar que lo hacen el mejor en términos de privacidad (excesiva para el objetivo de evitar IP tracking).

El primer rasgo característico de Tor es el que da origen a su icono (una cebolla): su infraestructura de red. El esquema que esta infraestructura sigue se puede ver como un grafo:

  • Cada nodo del grafo es un servidor o usuario, que ofrece su equipo para ser utilizado en el proceso de conexión de Tor. A estos nodos se les suele llamar relay.
  • De este grafo, con nuestro nodo como el inicial (que no quiere decir que nosotros ofrezcamos conexión a otros) trazamos un camino "aleatorio" de entre tres y cuatro nodos (por lo general). Este camino se conoce como Tor bridge.
  • El nodo inicial del camino somos nosotros realizando peticiones HTTP al navegador, mientras que el nodo final es quien realmente se comunicará con el host web.
  • Esta es la parte clave para comprender la seguridad de Tor: cada salto de nodo en nodo supone encriptar en esquema híbrido todo el mensaje que vamos a enviar. Si el camino es de tres nodos, el procedimiento es así:
  1. Establezco las clave entre Yo y $R_1$.
  2. Le pido a $R_1$ que medie entre Yo y $R_2$, con quien establezco otra clave.
  3. Ahora $R_1$ no sabe qué comunico con $R_2$, a quien le pido mediar con $R_3$.
  4. Mis mensajes $M$ serán $K_1(K_2(K_3(M)))$, con $K_i$ la clave de sesión entre Yo y $R_i$ (que ojo, iba encriptada en el intercambio de claves con la clave pública de $R_i$).
  • Este esquema tiene las siguientes tres premisas importantes por el punto anterior:
    • $R_i$ solo conoce a $R_{i-1}$ y $R_{i+1}$ ($R_{0}$ soy Yo y $R_{N+1}$ es el host web).
    • $R_1$ es el único que conoce mi IP, pero no puede saber qué tráfico envío al seguir encriptado por las $K_i$ con $i > 1$.
    • $R_N$ es el único relay que descubre qué tráfico envío ($M$), pero no sabe quién ha sido al llevar el paquete la IP de $R_{N-1}$.

La realidad es que para evitar el IP tracking solo nos hace falta el hecho de que $R_N$ variará al actualizar el Tor bridge, pero ya hemos visto que curarse en salud es un hábito en esta diferenciación de contextos.

Terminada la parte de teoría, pasamos al código, donde "dejamos de lado" selenium para pasar a tbselenium. Destacamos únicamente utilizar stem para inicializar el driver y el uso de Xvfb para ocultar el navegador en flight_search -t --headless:

  #En prepare_driver de Driver.py

  else:
    if v:
        print("Configuring tor browser...")
    tbb_dir = tor_path # este path se configurará en flight_search.conf
    if headless:
        xvfb_display = start_xvfb()
    try:
        tor_process = launch_tbb_tor_with_stem(tbb_path=tbb_dir)
    except OSError as e:
        if 'timeout' in str(e):
            print('Error: Tor connection timeout.' +
                     'Check URL or Internet connection')
            return None, None, None
        else:
            raise e
    driver = TorBrowserDriver(tbb_dir, tor_cfg=cm.USE_STEM)
    if headless:
        return driver, xvfb_display, tor_process
    else:
        return driver, None, tor_process

Código 12: Inicialización de un webdriver con Tor

La buena noticia es que el resto del código es exactamente igual que el contexto de "Sin cookies", la mala es que en el siguiente capítulo tenemos bastantes comentarios sobre problemas que nos han dado los hosts web con Tor.

Síntesis

El final de este trabajo (y de este documento, al fin) se acerca. Parecemos tener un entorno de pruebas adecuado para realizar mediciones. Solo queda programar sencillos bash scripts que realizan lo que he llamado "lotes de búsquedas", que recogerán los resultados en una base de datos SQL (para extraer más fácilmente conclusiones, dado que serán bastantes los vuelos buscados).

Parecemos tener bastante evidencia y base cuantitativa a la conclusión que saquemos en el último capítulo pero, por supuesto, también es importante la calidad de dicha base. Todo el código del programa está a disposición del lector en caso de que este pueda encontrar una fuga importante (pese a mis múltiples vueltas a dicho código). Aparte, quizás puedan presentarse dualidades en opinión a ciertos criterios y consideraciones que he tomado en esta implementación.

Para dichas consideraciones, problemas que se antepusieron al proyecto en mitad de la fase de recogida de datos e información adicional a tener en cuenta, está el penúltimo capítulo: "Previa a Conclusiones"