From e7f2ccdd870f998b9199b85bf2c486f8d1d0cceb Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 25 Aug 2018 22:54:33 +0200 Subject: management: création d’un sous-module parser Signed-off-by: Alban Gruin --- management/parsers/ups2017.py | 164 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 management/parsers/ups2017.py (limited to 'management/parsers/ups2017.py') diff --git a/management/parsers/ups2017.py b/management/parsers/ups2017.py new file mode 100644 index 0000000..94c1918 --- /dev/null +++ b/management/parsers/ups2017.py @@ -0,0 +1,164 @@ +# Copyright (C) 2017-2018 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +import datetime +import re + +from bs4 import BeautifulSoup +from django.utils import timezone + +from ...models import Course, Group, Room +from ...utils import get_week + +import requests +import edt + + +def add_time(date, time): + ptime = datetime.datetime.strptime(time, "%H:%M") + delta = datetime.timedelta(hours=ptime.hour, minutes=ptime.minute) + return date + delta + + +def delete_courses_in_week(source, year, week, today): + start, end = get_week(year, week) + Course.objects.filter(begin__gte=max(start, today), begin__lt=end, + source=source).delete() + + +def get_event(source, event, event_week, today): + """Renvoie une classe Course à partir d’un événement récupéré par BS4""" + # On récupère la date de l’évènement à partir de la semaine + # et de la semaine référencée, puis l’heure de début et de fin + date = event_week + datetime.timedelta(int(event.day.text)) + begin = add_time(date, event.starttime.text) + end = add_time(date, event.endtime.text) + + # On ne traite pas le cours si il commence après le moment du traitement + if today is not None and begin < today: + return + + # Création de l’objet cours + course = Course.objects.create(source=source, begin=begin, end=end) + + # On récupère les groupes concernés par les cours + groups = [Group.objects.get_or_create(source=source, + celcat_name=item.text)[0] + for item in event.resources.group.find_all("item")] + course.groups.add(*groups) + + # On récupère le champ « remarque » + if event.notes is not None: + course.notes = "\n".join(event.notes.find_all(text=True)) + + # On récupère le champ « nom » + if event.resources.module is not None: + course.name = event.resources.module.item.text + elif event.category is not None: + # Il est possible qu’un cours n’ait pas de nom. Oui oui. + # Qui sont les concepteurs de ce système ? Quels sont leurs + # réseaux ? + # Bref, dans ce cas, si le cours a un type, il devient son nom. + course.type = event.category.text + # Si il n’a pas de type (mais je ne pense pas que ça soit possible…), + # il obtiendra une valeur par défaut définie à l’avance. + + # Récupération du type de cours + if event.category is not None: + course.type = event.category.text + + # Si un cours a une salle attribuée (oui, il est possible qu’il n’y + # en ait pas… qui sont ils, leurs réseaux, tout ça…), on les insère + # dans la base de données, et on les ajoute dans l’objet cours + if event.resources.room is not None: + rooms = [Room.objects.get_or_create(name=item.text)[0] + for item in event.resources.room.find_all("item")] + course.rooms.add(*rooms) + + return course + + +def get_events(source, soup, weeks_in_soup, today, year=None, week=None): + """Récupère tous les cours disponibles dans l’emploi du temps Celcat. + Le traîtement se limitera à la semaine indiquée si il y en a une.""" + for event in soup.find_all("event"): + event_week = weeks_in_soup[event.rawweeks.text] + event_week_num = event_week.isocalendar()[1] # Numéro de semaine + + # On passe le traitement si la semaine de l’événement ne correspond pas + # à la semaine passée, ou qu’il ne contient pas de groupe ou n’a pas de + # date de début ou de fin. + if (event_week_num == week and event_week.year == year or + year is None or week is None) and \ + event.resources.group is not None and \ + event.starttime is not None and event.endtime is not None: + course = get_event(source, event, event_week, today) + + # On renvoie le cours si il n’est pas nul + if course is not None: + yield course + + +def get_update_date(soup): + # Explication de la regex + # + # (\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+) + # (\d+) au moins un nombre + # / un slash + # (\d+) au moins un nombre + # / un slash + # (\d+) au moins un nombre + # \s+ au moins un espace + # (\d+) au moins un nombre + # : un deux-points + # (\d+) au moins un nombre + # : un deux-points + # (\d+) au moins un nombre + datetime_regex = re.compile(r"(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+)") + search = datetime_regex.search(soup.footer.text) + if search is None: + return None + + day, month, year, hour, minute, second = [int(v) for v in search.groups()] + date = datetime.datetime(year, month, day, hour, minute, second) + return timezone.make_aware(date) + + +def get_weeks(soup): + # Les semaines sont référencées de manière assez… exotique + # En gros, il y a une liste d’éléments span qui contiennent une sorte d’ID + # de la semaine, formaté de la manière suivante : + # NNNNNNNNNNNNNNNNNNNYNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN + # Tous sont de la même longueur, contiennent 51 N et un seul Y. + # Allez savoir pourquoi. Il se trouve dans la balise « alleventweeks ». + # Un paramètre du span (« date ») représente la date de début. + # Un cours contient donc un ID de semaine, puis le nombre de jours après le + # début de cette semaine. + weeks = {} + for span in soup.find_all("span"): # Liste de toutes les semaines définies + # On parse la date et on la fait correspondre à l’ID + weeks[span.alleventweeks.text] = timezone.make_aware( + datetime.datetime.strptime(span["date"], "%d/%m/%Y")) + + return weeks + + +def get_xml(url): + user_agent = "celcatsanitizer/" + edt.VERSION + req = requests.get(url, headers={"User-Agent": user_agent}) + req.encoding = "utf8" + + soup = BeautifulSoup(req.content, "html.parser") + return soup -- cgit v1.2.1 From 77b6ade4a7d465ca0fbc6b82950f3b54689d60e3 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 25 Aug 2018 23:01:31 +0200 Subject: parsers: déplacement de delete_courses_in_week() vers timetable.py Signed-off-by: Alban Gruin --- management/parsers/ups2017.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'management/parsers/ups2017.py') diff --git a/management/parsers/ups2017.py b/management/parsers/ups2017.py index 94c1918..8522793 100644 --- a/management/parsers/ups2017.py +++ b/management/parsers/ups2017.py @@ -20,7 +20,6 @@ from bs4 import BeautifulSoup from django.utils import timezone from ...models import Course, Group, Room -from ...utils import get_week import requests import edt @@ -32,12 +31,6 @@ def add_time(date, time): return date + delta -def delete_courses_in_week(source, year, week, today): - start, end = get_week(year, week) - Course.objects.filter(begin__gte=max(start, today), begin__lt=end, - source=source).delete() - - def get_event(source, event, event_week, today): """Renvoie une classe Course à partir d’un événement récupéré par BS4""" # On récupère la date de l’évènement à partir de la semaine @@ -155,7 +148,7 @@ def get_weeks(soup): return weeks -def get_xml(url): +def get_source(url): user_agent = "celcatsanitizer/" + edt.VERSION req = requests.get(url, headers={"User-Agent": user_agent}) req.encoding = "utf8" -- cgit v1.2.1 From d02046f9255a07c4eb2bda9eb73d229cdb4f4a53 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 27 Aug 2018 17:43:16 +0200 Subject: parsers: parseur orienté objet avec une classe abstraite Signed-off-by: Alban Gruin --- management/parsers/ups2017.py | 259 +++++++++++++++++++++--------------------- 1 file changed, 132 insertions(+), 127 deletions(-) (limited to 'management/parsers/ups2017.py') diff --git a/management/parsers/ups2017.py b/management/parsers/ups2017.py index 8522793..99ce34d 100644 --- a/management/parsers/ups2017.py +++ b/management/parsers/ups2017.py @@ -20,9 +20,7 @@ from bs4 import BeautifulSoup from django.utils import timezone from ...models import Course, Group, Room - -import requests -import edt +from .abstractparser import AbstractParser def add_time(date, time): @@ -31,127 +29,134 @@ def add_time(date, time): return date + delta -def get_event(source, event, event_week, today): - """Renvoie une classe Course à partir d’un événement récupéré par BS4""" - # On récupère la date de l’évènement à partir de la semaine - # et de la semaine référencée, puis l’heure de début et de fin - date = event_week + datetime.timedelta(int(event.day.text)) - begin = add_time(date, event.starttime.text) - end = add_time(date, event.endtime.text) - - # On ne traite pas le cours si il commence après le moment du traitement - if today is not None and begin < today: - return - - # Création de l’objet cours - course = Course.objects.create(source=source, begin=begin, end=end) - - # On récupère les groupes concernés par les cours - groups = [Group.objects.get_or_create(source=source, - celcat_name=item.text)[0] - for item in event.resources.group.find_all("item")] - course.groups.add(*groups) - - # On récupère le champ « remarque » - if event.notes is not None: - course.notes = "\n".join(event.notes.find_all(text=True)) - - # On récupère le champ « nom » - if event.resources.module is not None: - course.name = event.resources.module.item.text - elif event.category is not None: - # Il est possible qu’un cours n’ait pas de nom. Oui oui. - # Qui sont les concepteurs de ce système ? Quels sont leurs - # réseaux ? - # Bref, dans ce cas, si le cours a un type, il devient son nom. - course.type = event.category.text - # Si il n’a pas de type (mais je ne pense pas que ça soit possible…), - # il obtiendra une valeur par défaut définie à l’avance. - - # Récupération du type de cours - if event.category is not None: - course.type = event.category.text - - # Si un cours a une salle attribuée (oui, il est possible qu’il n’y - # en ait pas… qui sont ils, leurs réseaux, tout ça…), on les insère - # dans la base de données, et on les ajoute dans l’objet cours - if event.resources.room is not None: - rooms = [Room.objects.get_or_create(name=item.text)[0] - for item in event.resources.room.find_all("item")] - course.rooms.add(*rooms) - - return course - - -def get_events(source, soup, weeks_in_soup, today, year=None, week=None): - """Récupère tous les cours disponibles dans l’emploi du temps Celcat. - Le traîtement se limitera à la semaine indiquée si il y en a une.""" - for event in soup.find_all("event"): - event_week = weeks_in_soup[event.rawweeks.text] - event_week_num = event_week.isocalendar()[1] # Numéro de semaine - - # On passe le traitement si la semaine de l’événement ne correspond pas - # à la semaine passée, ou qu’il ne contient pas de groupe ou n’a pas de - # date de début ou de fin. - if (event_week_num == week and event_week.year == year or - year is None or week is None) and \ - event.resources.group is not None and \ - event.starttime is not None and event.endtime is not None: - course = get_event(source, event, event_week, today) - - # On renvoie le cours si il n’est pas nul - if course is not None: - yield course - - -def get_update_date(soup): - # Explication de la regex - # - # (\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+) - # (\d+) au moins un nombre - # / un slash - # (\d+) au moins un nombre - # / un slash - # (\d+) au moins un nombre - # \s+ au moins un espace - # (\d+) au moins un nombre - # : un deux-points - # (\d+) au moins un nombre - # : un deux-points - # (\d+) au moins un nombre - datetime_regex = re.compile(r"(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+)") - search = datetime_regex.search(soup.footer.text) - if search is None: - return None - - day, month, year, hour, minute, second = [int(v) for v in search.groups()] - date = datetime.datetime(year, month, day, hour, minute, second) - return timezone.make_aware(date) - - -def get_weeks(soup): - # Les semaines sont référencées de manière assez… exotique - # En gros, il y a une liste d’éléments span qui contiennent une sorte d’ID - # de la semaine, formaté de la manière suivante : - # NNNNNNNNNNNNNNNNNNNYNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN - # Tous sont de la même longueur, contiennent 51 N et un seul Y. - # Allez savoir pourquoi. Il se trouve dans la balise « alleventweeks ». - # Un paramètre du span (« date ») représente la date de début. - # Un cours contient donc un ID de semaine, puis le nombre de jours après le - # début de cette semaine. - weeks = {} - for span in soup.find_all("span"): # Liste de toutes les semaines définies - # On parse la date et on la fait correspondre à l’ID - weeks[span.alleventweeks.text] = timezone.make_aware( - datetime.datetime.strptime(span["date"], "%d/%m/%Y")) - - return weeks - - -def get_source(url): - user_agent = "celcatsanitizer/" + edt.VERSION - req = requests.get(url, headers={"User-Agent": user_agent}) - req.encoding = "utf8" - - soup = BeautifulSoup(req.content, "html.parser") - return soup +class Parser(AbstractParser): + def __get_event(self, event, event_week, today): + """Renvoie une classe Course à partir d’un événement lu par BS4""" + # On récupère la date de l’évènement à partir de la semaine + # et de la semaine référencée, puis l’heure de début et de fin + date = event_week + datetime.timedelta(int(event.day.text)) + begin = add_time(date, event.starttime.text) + end = add_time(date, event.endtime.text) + + # On ne traite pas le cours si il commence après le moment du + # traitement + if today is not None and begin < today: + return + + # Création de l’objet cours + course = Course.objects.create(source=self.source, begin=begin, + end=end) + + # On récupère les groupes concernés par les cours + groups = [ + Group.objects.get_or_create( + source=self.source, celcat_name=item.text + )[0] + for item in event.resources.group.find_all("item") + ] + course.groups.add(*groups) + + # On récupère le champ « remarque » + if event.notes is not None: + course.notes = "\n".join(event.notes.find_all(text=True)) + + # On récupère le champ « nom » + if event.resources.module is not None: + course.name = event.resources.module.item.text + elif event.category is not None: + # Il est possible qu’un cours n’ait pas de nom. Dans ce + # cas, si le cours a un type, il devient son nom. + course.type = event.category.text + # Si il n’a pas de type, il obtiendra une valeur par + # défaut définie à l’avance. + + # Récupération du type de cours + if event.category is not None: + course.type = event.category.text + + # Si un cours a une salle attribuée, on les insère dans la + # base de données, et on les ajoute dans l’objet cours + if event.resources.room is not None: + rooms = [ + Room.objects.get_or_create(name=item.text)[0] + for item in event.resources.room.find_all("item") + ] + course.rooms.add(*rooms) + + return course + + def get_events(self, today, year=None, week=None): + """Récupère tous les cours disponibles dans l’emploi du temps Celcat. + Le traîtement se limitera à la semaine indiquée si il y en a une.""" + for event in self.soup.find_all("event"): + event_week = self.weeks[event.rawweeks.text] + event_week_num = event_week.isocalendar()[1] # Numéro de semaine + + # On passe le traitement si la semaine de l’événement ne + # correspond pas à la semaine passée, ou qu’il ne contient + # pas de groupe ou n’a pas de date de début ou de fin. + if ( + ( + event_week_num == week + and event_week.year == year + or year is None + or week is None + ) + and event.resources.group is not None + and event.starttime is not None + and event.endtime is not None + ): + course = self.__get_event(event, event_week, today) + + # On renvoie le cours si il n’est pas nul + if course is not None: + yield course + + def get_update_date(self): + # Explication de la regex + # + # (\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+) + # (\d+) au moins un nombre + # / un slash + # (\d+) au moins un nombre + # / un slash + # (\d+) au moins un nombre + # \s+ au moins un espace + # (\d+) au moins un nombre + # : un deux-points + # (\d+) au moins un nombre + # : un deux-points + # (\d+) au moins un nombre + datetime_regex = re.compile(r"(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+)") + search = datetime_regex.search(self.soup.footer.text) + if search is None: + return None + + day, month, year, hour, minute, second = [ + int(v) for v in search.groups() + ] + date = datetime.datetime(year, month, day, hour, minute, second) + return timezone.make_aware(date) + + def get_weeks(self): + # Les semaines présentes dans l’emploi du temps sont toutes + # stockées dans un élément span. Il contient une chaîne de + # caractère qui correspond à une forme d’ID, et un champ date, + # qui correspond au lundi de cette semaine. Un cours contient + # un ID correspondant à une semaine, puis le nombre de jours + # après le début de cette semaine. + self.weeks = {} + + # Liste de toutes les semaines définies + for span in self.soup.find_all("span"): + # On parse la date et on la fait correspondre à l’ID + self.weeks[span.alleventweeks.text] = timezone.make_aware( + datetime.datetime.strptime(span["date"], "%d/%m/%Y") + ) + + return self.weeks + + def get_source(self): + req = super(Parser, self).get_source() + self.soup = BeautifulSoup(req.content, "html.parser") + return self.soup -- cgit v1.2.1