Tabla de contenidos
- The Bunyols Library
- Introducción
- Historia
- Manual
- Metodología
- Descripción técnica
- Diseño
- Implementacion
- Pruebas
- Comparación Temporal
- Conclusiones
- Dificultades
Bunyols Library nace con la idea de presentar la información básica que cualquier libro debe de tener. Únicamente información representativa sobre el libro en sí de una forma muy fácil para el usuario.
En este proyecto usaremos MongoDB como base de datos a través del servicio Mongo Atlas. Para la lógica de la aplicación utilizaremos el lenguaje de programación Python (versión 3.9).
Para la conversión y creación del sitio web estático utilizaremos el SSG Hugo. Y por último, emplearemos el servidor web Nginx para servir nuestro sitio web.
Nuestro tan llamativo nombre surgió de un día ir caminando por Palma de Mallorca y una mujer en un puesto de estos aceitosos y sabrosas frutas fritas regalarme una bolsa de 1kg totalmente gratis. No era capaz de comerme tal semejante cantidad de fruta de sartén y así fue como Samuele comió bunyols de camino al tren, dentro del tren y de desayuno al siguiente día.
- Con cariño, Eze
Git
Python3
pip3
fake-headers
bs4
lxml
pymongo
matplotlib
Se recomienda utilizar en 'virtualenv' para instalar todas las dependencias utilizadas por el programa. En Windows lo puedes instalar siguiendo su guía. En Linux ejecuta la siguiente instrucción.
$ sudo apt-get install python3-venv
Crea un directorio y sitúate dentro de él. Aquí dentro residirá todo el código de la aplicación.
$ mkdir ./bunyols-library
$ cd bunyols-library
Ya dentro del directorio clona el repositorio.
$ git clone git@github.com:stonarini/Bunyols.git
Se preparó un archivo 'setup' que activara el entorno virtual y descargara las dependencias necesarias por ti, ejecútalo.
$ ./setup.sh
Cuando se quiera entrar en el entorno virtual sin pasar por './setup.sh', ejecuta:
$ source venv/bin/activate
1. Formatea los elementos de la lista de la siguiente manera.
("URL", ("Familia", ["Temática"]))
- 'URL' referencia a la página del libro en Amazon.
- 'Familia' referencia al conjunto de libros que pertenece el actual.
- 'Temática' referencia al tema del libro.
Un ejemplo
("https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882", ("Robert C Martin Series", ["IT"])
2. Ejecuta 'bunyols.py' encima del archivo donde estén todos tus libros a scrapear definiendo la carpeta de 'Hugo' donde se creara el contenido.
python3 bunyols.py [CARPETA]/
- 'CARPETA' hace referencia al directorio de 'Hugo'.
Para la parte lógica de la aplicación se ha usado una metodología incremental utilizando la filosofía de construir funcionalidades del programa, analizándolo y construyendo incrementos donde uno unido al otro tiene el fin de construir un uso funcional de la aplicación.
Para la parte de diseño web se ha empleado una metodología prototipada donde hemos diseñado diferentes prototipos de la página en diferentes formatos (papel y software gráfico). Una vez elegido el diseño, se recreó en HTML y CSS pasándolo por último a hugo.
Por último, hemos usado Scrum como marco de trabajo durante todo el proyecto.
Como parte interesada solo tenemos a lo que vienen siendo los empleados de la empresa que necesitan visualizar y gestionar los elementos de la base de datos con nuestra aplicación. Podemos denominar esta parte interesada como Empleado.
Requisitos Funcionales
-
RF_Empleado_01: Visualizar Elementos (R)
-
RF_Empleado_02: Crear Elementos (C)
-
RF_Empleado_03: Modificar Elementos (U)
-
RF_Empleado_04: Borrar Elementos (D)
Requisitos No Funcionales
- RnF_01: Recolección Automática de Datos
- Al crear un nuevo elemento se usará un web scraper para recolectar automáticamente datos sobre ese elemento.
- RnF_02: Catalogación de Elementos
- A cada elemento se asignaran categorías que servirán para agruparlos
- RnF_03: Creación de Gráficos
- Se generaran automáticamente gráficos al renderizar la página
-
Presentation layer
- nginx/hugo el primero es un software de servidor web open source utilizado para servir archivos HTML que construirá la segunda tecnología.
-
Service layer
- bunyols.py programa principal.
-
Business layer
- get_page_source encargado de hacer peticiones y general el contenido parseado de la página web solicitada.
- book_scraper encargado de obtener los datos estáticos y dinámicos de la página web en la que se solicite la acción.
- markdownify encargado de crear el frontmatter empleado por Hugo así de generar la estructura de los items devueltos al hacer peticiones a la base de datos.
- generate_graphs encargado usando los datos dinámicos precios y reviews solicitados a la base de datos de realizar gráficos.
-
Data layer
- database todo lo relacionado a acciones hechas sobre y con la base de datos MongoDB con instrucciones de la librería 'pymongo.'
web-scraper | testing | generación de gráficos |
---|---|---|
BS4 y requests | pytest | ggplot |
Selenium | Robot | matplotlib |
- Web-Scraping
X | Facil de Usar | Facil de Configurar | Extensible | Personalizable | Mandatorio |
---|---|---|---|---|---|
BS4 y requests | ✔️ | ✔️ | ✔️ | ||
Selenium | ✔️ | ✔️ |
- Testing
X | Facil de Usar | Facil de Configurar | Extensible | Personalizable | Mandatorio |
---|---|---|---|---|---|
pytest | ✔️ | ✔️ | ✔️ | ✔️ | |
Robot | ✔️ | ✔️ |
- Generacion de Graficos
X | Facil de Usar | Facil de Configurar | Extensible | Personalizable | Mandatorio |
---|---|---|---|---|---|
ggplot | ✔️ | ✔️ | |||
Matplotlib | ✔️ | ✔️ | ✔️ |
-
utilities
package que alberga herramientas que devuelven una información utilizada por otros módulos o realizan una tarea en específico. -
book_scraper
package que alberga todo lo relacionado con el web scraper en sí, dentro de este existen 2 packages más 'get_book_dynamic_data' y 'get_book_static_data' que scrapean la información establecida dentro de sus módulos. -
get_page_source
package que alberga aquellos métodos usados para ejecutar peticiones a una página y devolver el contenido que después será scrapeado por 'book_scraper'. -
markdownify
package que alberga todo lo empleado para generar estructuras markdown así como lo necesario para 'Hugo'. -
generate_graphs
package que alberga aquellos métodos empleados para generar gráficos mediante los datos 'precios' y 'reyes' contenidos en la base de datos. -
database
package donde se sitúa todo lo relacionado con el manejo de la base de datos, en resumen, sus métodos CRUD.
{
"title": {
"type": "string",
"description": "Book's Title"
},
"author": {
"type": "array",
"description": "List of authors of the book"
},
"publisher": {
"type": "string",
"description": "Entity that published the book"
},
"ISBN_13": {
"type": "string",
"description": "Unique book's identifier"
},
"publish_date": {
"type": "string",
"description": "Day when the book was published"
},
"price": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "Current price of the book"
},
"date": {
"type": "string",
"description": "The date of when the price was retrived"
}
}
},
"description": "Array of object contaning the book price and the day the price was retrived"
},
"reviews": {
"type": "object",
"properties": {
"total_reviews": {
"type": "number",
"description": "Total number of reviews of the book"
},
"5": {
"type": "number",
"description": "Total number of 5 star reviews"
},
"4": {
"type": "number",
"description": "Total number of 4 star reviews"
},
"3": {
"type": "number",
"description": "Total number of 3 star reviews"
},
"2": {
"type": "number",
"description": "Total number of 2 star reviews"
},
"1": {
"type": "number",
"description": "Total number of 1 star reviews"
}
}
},
"categories": {
"type": "array",
"items": {
"type": "string",
"description": "Category name"
},
"description": "Array of categories of the book"
},
"family": {
"type": "string",
"description": "Saga/Collection of which the book is part of"
}
}
{
"title": "Code Complete",
"author": ["Steve McConnell"],
"publisher": "Microsoft Press",
"ISBN_13": "9780735619678",
"publish_date": "June 2004",
"price": [
{ "date": "2021-12-06 18:13:33", "value": "$16.43" },
{ "date": "2021-12-10 00:24:26", "value": "$29.99" }
],
"reviews": {
"total_reviews": 896,
"5": 699,
"4": 117,
"3": 45,
"2": 17,
"1": 18
},
"categories": ["IT"]
}
- Python
- Beautiful Soup 4 es una biblioteca de Python para extraer datos de archivos HTML y XML dejando a elegir el parser para proporcional formas de navegación, búsqueda y modificación. Referencia
- requests es una librería HTTP para Python usada para efectuar peticiones HTTP y hacerlas más amigables. Referencia
- fake-headers utilizada para generar una cabecera aleatoria para enmascarar diferentes peticiones y que no vengan bloqueadas. Referencia
- matplotlib es una librería para crear animaciones estáticas y visualizaciones interactivas en Python. Referencia
- pytest es un framework utilizado para escribir casos test y poner aprueba aplicaciones y librerías. Referencia
- coverage junto a pytest es una herramienta para medir el código cubierto de un programa. Monitoriza el programa informando de que cantidad de código ha sido ejecutada. Referencia
- MongoDB
- pymongo es una librería que contiene herramientas para trabajar con MongoDB. Referencia
- Hugo
- Framework empleado para la creación de sitios webs estáticos.
- HTML5 y CSS
- Estilización de la parte web junto a hugo.
- Git
- Realización de cambios y subida de versiones modificadas independientes, no sobrescribiendo en el archivo original.
- Markdown
- Lenguaje de marcado ligero utilizado en gohugo
La primera función del backend es añadir/actualizar la información de los libros en la base de datos:
for item in item_list:
URL, categories = item
content = get_page_source(URL)
ISBN = utilities.get_book_isbn(content)
if database.find_one({"ISBN_13": ISBN}):
utilities.update_bunyol(ISBN, content)
else:
utilities.create_bunyol(ISBN, content, categories)
En este código conseguimos el ISBN del libro y luego comprobamos si está en la base de datos; Si esta lo actualizamos y si no está lo añadimos.
# Añadir
def create_bunyol(ISBN, content, categories):
book_data = book_scraper.create_book_data(content, ISBN)
if book_data:
categories = get_categories(*categories)
book_data.update(**categories)
database.create_one(book_data)
Aquí podemos ver como añadimos un libro nuevo consiguiendo la información desde nuestras fuentes.
# Actualizar
def update_bunyol(ISBN, content):
new_book_data = book_scraper.update_book_data(content)
database.update_one(
{"ISBN_13": ISBN},
{
"$push": {"price": new_book_data["price"][0]},
"$set": {
"reviews": new_book_data["reviews"],
},
},
)
Aquí estamos actualizando un determinado libro de la base de datos. Más en concreto los precios y las reviews.
La otra función que tiene es la de generar los files en formato Markdown y los gráficos desde los ítems en la base de datos:
books = database.find_all()
for book in books:
del book["_id"]
ISBN = book["ISBN_13"]
price, reviews = book.pop("price"), book.pop("reviews")
markdown = markdownify(book)
utilities.write_to_file(path + "content/bunyols/", f"{ISBN}.md", markdown)
generate_graphs(path + "static/images/" + ISBN, reviews, price)
Se puede ver como conseguimos todos los libros y formateamos determinados campos para generar el Markdown y gráficos y luego esa información la escribimos en una ruta determinada. En este caso la ruta de un path que es especificado como la ruta de nuestro proyecto de Hugo.
Dentro del repositorio existe una rama llamada hugo-template donde reside todo lo relacionado a la parte visual aportada al usuario.
Describiendo nuestro diseño, hemos ido a por una visualización sencilla y organizada de la información.
Organizamos nuestras pruebas de manera que cada unidad hubiese mínimo 2 test:
- Un test que simulase una ejecución correcta.
- Un test que simulase el peor caso posible.
Con estos test ya se podía iniciar a codificar. Después, si había más casos a tener en cuenta se iba añadiendo durante el desarrollo,
Para más concretización sobre los test, véase el file pytest.ini o la carpeta de test en la rama develop.
Lo que si merece la pena mencionar son los test del módulo database, que están diseñados de manera que un elemento se cree, actualice y luego se borre, sin dejar elementos no queridos en la base de datos.
Podemos observar como nuestros test tienen una cobertura del 91% de las lineas de código. Con unos pocos test deberíamos llegar al 100% sin problemas.
En pytest, cuando ejecutas los test, un . verde significa que el test ha pasado y una F roja
significa que ha fallado. Todo el output del programa generado por un test vendrá antes del .
o F que represente ese test.
i.e:
test/test_prueba.py ..
Este test ha fallado
F Este test NO ha fallado
.
En este ejemplo podemos ver que hay 4 test.
Los dos primeros han pasado sin ningún output, mientras que el tercero ha fallado y ha tenido
output.
El cuarto no ha fallado y ha tenido también un output descriptivo.
Como podemos observar en los test de create, seguimos nuestro diseño de un test que pase y otro que no, así que en estos test intentamos meter un documento que no sigue nuestro esquema y otro que si:
En los test de find test intentamos leer dos elementos, uno que no existe y otro que si. Los dos test pasan sin problema:
En el caso de los test de update tenemos tres test, uno que intenta añadir un parámetro test que no está presente en el esquema, otro que intenta añadir el parámetro opcional family que sí que está presente en el esquema, pero no es obligatorio, y por último este test simula una actualización de las reviews y un cambio de precio. Como podemos observar el primer test da error y nos dice si estamos seguros de que nuestro documento sigue el esquema, mientras que los otros dos test pasan sin problemas.
Como último, están los tests de delete. El primer test borra el elemento creado en los tests de create, mientras que el segundo test intenta borrar un elemento que no existe. Aquí podemos observar como el primer test pasa sin problema, mientras que el segundo nos avisa de que el elemento no existe:
Para tener bajo control el tiempo utilizado y para poder comparar nuestras estimaciones hemos usado la herramienta Clockify con las siguientes etiquetas:
- Scraper: Para los módulos 'book_scraper' y 'get_page_source'.
- Database: Para el módulo 'database' y la creación del esquema y validación de la base de datos
- Web Design: Para el prototipado de la página web (tanto diseño como implementación HTML y CSS)
- Hugo: Para la configuración de 'Hugo' y para la implementación del prototipo realizado en 'Web Design'
- Markdown: Para los módulos 'markdownify' y 'generate_graphs'
- Docs: Para el tiempo usado para escribir esta documentación
- Configuración: Para configuración de git y para la configuración de herramientas y scripts (git-hooks, tox, pytest, etc...)
Nuestra predicción temporal fue 34 horas, aunque no contamos con cosas como la configuración, documentación y refactorización:
Predicción de Tiempo: 34h
Tiempo Real: 56h y 40min
Teniendo el gráfico presente la tarea en la que más hemos invertido tiempo ha sido en la creación de nuestro propio scraper, teníamos la opción de usar el scraper ya hecho por nuestros tutores de segundo, pero prescindimos de este al no ser posible adaptarlo del todo a los requisitos que queríamos.
Invertimos así el tiempo teniendo en mente que la lógica detrás de 'markdownify' no nos iba a ocupar el tiempo que sabíamos la creación del scraper desde cero sí.
En el desarrollo de este trabajo se ha aprendido mucho de las buenas prácticas mientras se codifica aprendidas así como el pensamiento previo a codificar que se debe de tener en mente a la hora de empezar a trabajar.
Nunca dudamos de que aun al empezar tarde el desarrollo del proyecto en sí de que llegaríamos a tiempo a su entrega con unos resultados bastantes satisfactorios para nosotros, además de darnos una cucharada de todo el tiempo que se invierte en desarrollar un programa con un funcionamiento correcto, y sobre todo que salga como tú quieras.
- Estamos trabajando en la implementación de Typer para poder realizar una inserción nueva de libros desde la linea de comandos.
- Estamos trabajando en la implementación de Typer para poder manejar funciones relacionadas al modulo de base de datos desde la linea de comandos.
No hemos experimentado dificultades notorias mas haya que desarrollar el modulo book-scraper
desde cero, pues el proyecto no consistía directamente en esta funcionalidad pero se necesitaba si queríamos crear el proyecto a nuestros gustos personales.