En aquesta guia veurem com obtenir dades obertes d'un servei web i guardar-les, per tal d'utilitzar-les a la nostra aplicació. Aquesta tècnica es coneix com a web scrapping.
Per aconseguir-ho, en primer lloc provarem Pandas en un programa de Python local i/o amb Jupyter Notebook.
Posteriorment, incorporarem el codi d'exemple de Pandas en una aplicació web feta amb Django. Una bona opció és utiltizar la funcionalitat del seeder que ens ofereix Django per inserir automàticament els equips.
Només s'hauria d'usar aquesta guia per a obtenir dades obertes i públiques; sinó ens podriem trobar amb problemes ètics i legals.
Amb venv:
$ python3 -m venv env $ source env/bin/activate (env) $ pip install pandas
Amb Anaconda:
$ conda activate env (env) $ conda install -n env -c anaconda pandas
Exemple: Volem obtenir el llistat dels equips de la lliga de futbol femení 2022-2023, que hem trobat a:
https://www.sport.es/deportes/futbol/liga-femenina-futbol/clasificacion-liga.html
En aquest cas potser acabem abans copiant i enganxant en un full de càlcul els resultats; però a la hora de la veritat no sempre funciona.
A més a més, ens interessa automatitzar les tasques.
Exemple codi local, arrencat en un fitxer IPYNB
El codi que mostro a continuació el podeu veure arrencat en directe a:
https://colab.research.google.com/drive/1vKHXmy5e9KFGv3EnRHwJpzeHTP5zp6I5#scrollTo=ROmuDJnMryJL
JuPyTeR (Julia, Python i R) Notebook és un servei que permet executar codi i llibreries Python al navegador o en qualsevol IDE, fins i tot llibreries per generar gràfics. Els seus fitxers tenen la extensió .IPYNB
Pas 1. Seleccionar la font de dades.
En el nostre cas, dins de la pàgina indicada la taula que ens interessa té un atribut HTML «summary» amb el valor «Clasificación». El mètode read_html ha de conèixer la pàgina i atributs html per identificar la taula que volem extreure.
Pas 2. Obtenir les dades amb Pandas
import pandas as pd from io import StringIO url: str = None # Web scrapping: obtenció de dades externes al nostre sistema. if url: print("Obtaining data") url: str = "https://www.sport.es/deportes/futbol/liga-femenina-futbol/clasificacion-liga.html" classif: pd.DataFrame = pd.read_html(url, attrs={"summary": "Clasificación"}) # Per algún motiu que desconec la informació que m'interessa me la posa a # la primera posició d'una llista. classificacio_df = classif[0] else: print("Data already obtained")
Pas 3. Filtrar dades i obtenir resultats.
# Seleccionem la llista dels noms dels equips. classificacio_df['EQUIPO'] = classificacio_df['EQUIPO'].astype("string") list_aux = classificacio_df['EQUIPO'].values.tolist() llista_equips = sum(list_aux, []) def print_data(): #print(classificacio_df) print(classificacio_df.dtypes) print(classificacio_df['EQUIPO']) print(llista_equips) print_data()
Pas 4. Inserim el codi que hem comprovat a la aplicació django, dins de l'script seeder.
En primer lloc, posem el codi que hem provat abans:
def handle(self, *args, **options): print("Obtenim dades equips, des d'un portal Open Data") # https://www.sport.es/deportes/futbol/liga-femenina-futbol/clasificacion-liga.html # Web scrapping: obtenció de dades externes al nostre sistema. url: str = "https://www.sport.es/deportes/futbol/liga-femenina-futbol/clasificacion-liga.html" classif: pd.DataFrame = pd.read_html(url, attrs={"summary": "Clasificación"}) # Per algún motiu que desconec la informació que m'interessa me la posa a # la primera posició d'una llista. classificacio_df = classif[0] # Seleccionem la llista dels noms dels equips. classificacio_df['EQUIPO'] = classificacio_df['EQUIPO'].astype("string") list_aux = classificacio_df['EQUIPO'].values.tolist() llista_equips = sum(list_aux, []) print(llista_equips) print("Dades equips obtingudes correctament.") #print(jugador=Jugadora.objects.get(dorsal=5)) titol_lliga = options['titol_lliga'][0] ...
Després del codi que crea la lliga si no existeix, comento el codi de creació d'equips del Faker i poso aquest, que crea i guarda els equips pel seu nom:
for nom_equip in llista_equips: equip = Equip(nom=nom_equip) #print(equip) equip.save() lliga.equips.add(equip) # Seleccionarem algunes jugadores. print("Creem jugadores de l'equip "+nom_equip) ...
from django.db import models # Observació: Django genera per defecte camps id autonumèrics # com a clau primària. Hi ha formes de canviar aquest # comportament però per ara ens sembla OK. # Hauria de posar una relació ManyToMany per tal que un mateix equip # pugui compatir en diverses lligues en un any (Champions) # equips = models.ManyToManyField(Equip) class Equip(models.Model): nom = models.CharField(max_length=100) ciutat = models.CharField(max_length=100,null=True) estadi = models.CharField(max_length=100,null=True) #lliga = models.ForeignKey(Lliga, on_delete=models.CASCADE) def __str__(self): return self.nom class Lliga(models.Model): nom_temporada = models.CharField(max_length=100) equips = models.ManyToManyField(Equip) def __str__(self): return self.nom_temporada class Meta: verbose_name_plural = 'Lligues' class Jugadora(models.Model): nom = models.CharField(max_length=100) cognom = models.CharField(max_length=100) dorsal = models.IntegerField() equip = models.ForeignKey(Equip, on_delete=models.CASCADE) def __str__(self): return f'{self.dorsal} - {self.nom}' class Meta: verbose_name_plural = 'Jugadores' class Partit(models.Model): class Meta: unique_together = ["local","visitant","lliga"] local = models.ForeignKey(Equip,on_delete=models.CASCADE, related_name="partits_local",null=True,blank=True) visitant = models.ForeignKey(Equip,on_delete=models.CASCADE, related_name="partits_visitant",null=True,blank=True) lliga = models.ForeignKey(Lliga,on_delete=models.CASCADE) detalls = models.TextField(null=True,blank=True) inici = models.DateTimeField(null=True,blank=True) def __str__(self): return "{} - {}".format(self.local,self.visitant) def gols_local(self): return self.event_set.filter( tipus=Event.EventType.GOL,equip=self.local).count() def gols_visitant(self): return self.event_set.filter( tipus=Event.EventType.GOL,equip=self.visitant).count() class Event(models.Model): # Solució subòptima i provisional. # tipus_event = models.CharField(max_length=100) # el tipus d'event l'implementem amb algo tipus "enum" class EventType(models.TextChoices): GOL = "GOL" AUTOGOL = "AUTOGOL" FALTA = "FALTA" PENALTY = "PENALTY" MANS = "MANS" CESSIO = "CESSIO" FORA_DE_JOC = "FORA_DE_JOC" ASSISTENCIA = "ASSISTENCIA" TARGETA_GROGA = "TARGETA_GROGA" TARGETA_VERMELLA = "TARGETA_VERMELLA" partit = models.ForeignKey(Partit,on_delete=models.CASCADE) temps = models.TimeField() tipus = models.CharField(max_length=30,choices=EventType.choices) jugador = models.ForeignKey(Jugadora,null=True, on_delete=models.SET_NULL, related_name="events_fets") equip = models.ForeignKey(Equip,null=True, on_delete=models.SET_NULL) # per les faltes jugador2 = models.ForeignKey(Jugadora,null=True,blank=True, on_delete=models.SET_NULL, related_name="events_rebuts") detalls = models.TextField(null=True,blank=True) def __str__(self): return f'{self.EventType} - {self.jugador}'
from django.db import models # Observació: Django genera per defecte camps id autonumèrics # com a clau primària. Hi ha formes de canviar aquest # comportament però per ara ens sembla OK. # Hauria de posar una relació ManyToMany per tal que un mateix equip # pugui compatir en diverses lligues en un any (Champions) # equips = models.ManyToManyField(Equip) class Equip(models.Model): nom = models.CharField(max_length=100) ciutat = models.CharField(max_length=100,null=True) estadi = models.CharField(max_length=100,null=True) #lliga = models.ForeignKey(Lliga, on_delete=models.CASCADE) def __str__(self): return self.nom class Lliga(models.Model): nom_temporada = models.CharField(max_length=100) equips = models.ManyToManyField(Equip) def __str__(self): return self.nom_temporada class Meta: verbose_name_plural = 'Lligues' class Jugadora(models.Model): nom = models.CharField(max_length=100) cognom = models.CharField(max_length=100) dorsal = models.IntegerField() equip = models.ForeignKey(Equip, on_delete=models.CASCADE) def __str__(self): return f'{self.dorsal} - {self.nom}' class Meta: verbose_name_plural = 'Jugadores' class Partit(models.Model): class Meta: unique_together = ["local","visitant","lliga"] local = models.ForeignKey(Equip,on_delete=models.CASCADE, related_name="partits_local",null=True,blank=True) visitant = models.ForeignKey(Equip,on_delete=models.CASCADE, related_name="partits_visitant",null=True,blank=True) lliga = models.ForeignKey(Lliga,on_delete=models.CASCADE) detalls = models.TextField(null=True,blank=True) inici = models.DateTimeField(null=True,blank=True) def __str__(self): return "{} - {}".format(self.local,self.visitant) def gols_local(self): return self.event_set.filter( tipus=Event.EventType.GOL,equip=self.local).count() def gols_visitant(self): return self.event_set.filter( tipus=Event.EventType.GOL,equip=self.visitant).count() class Event(models.Model): # Solució subòptima i provisional. # tipus_event = models.CharField(max_length=100) # el tipus d'event l'implementem amb algo tipus "enum" class EventType(models.TextChoices): GOL = "GOL" AUTOGOL = "AUTOGOL" FALTA = "FALTA" PENALTY = "PENALTY" MANS = "MANS" CESSIO = "CESSIO" FORA_DE_JOC = "FORA_DE_JOC" ASSISTENCIA = "ASSISTENCIA" TARGETA_GROGA = "TARGETA_GROGA" TARGETA_VERMELLA = "TARGETA_VERMELLA" partit = models.ForeignKey(Partit,on_delete=models.CASCADE) temps = models.TimeField() tipus = models.CharField(max_length=30,choices=EventType.choices) jugador = models.ForeignKey(Jugadora,null=True, on_delete=models.SET_NULL, related_name="events_fets") equip = models.ForeignKey(Equip,null=True, on_delete=models.SET_NULL) # per les faltes jugador2 = models.ForeignKey(Jugadora,null=True,blank=True, on_delete=models.SET_NULL, related_name="events_rebuts") detalls = models.TextField(null=True,blank=True) def __str__(self): return f'{self.EventType} - {self.jugador}'
Resultat final, quan arrenquem la aplicació:
Si heu canviat el model, primer executeu:
(env) nomusuari@linux:~/lligafemprj$ python manage.py makemigrations lliga (env) nomusuari@linux:~/lligafemprj$ python manage.py migrate
Per omplir les dades de l'script seeder:
(env) nomusuari@linux:~/lligafemprj$ ./manage.py crea_lliga <nom_lliga> (env) nomusuari@linux:~/lligafemprj$ ./manage.py runserver (env) nomusuari@linux:~/lligafemprj$ firefox http://127.0.0.1:8080/admin
Si anem a veure la taula de partits podem comprovar que s'han generat correctament, juntament amb els gols dels locals i visitants.
Publicar i fer ús de dades públiques d'interès general a nivell de cultura, salut, economia, … és un encert si en fem un bon ús i estem ben formats amb llenguatges com Python que ens faciliten aquesta tasca.