From 355230632d068a7dfd287f920bf0b4bff09fa2a2 Mon Sep 17 00:00:00 2001 From: Bastien Le Querrec <blq@laquadrature.net> Date: Sun, 9 Jun 2024 14:58:45 +0200 Subject: [PATCH] =?UTF-8?q?Attrap:=20am=C3=A9liore=20la=20documentation=20?= =?UTF-8?q?du=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #7 Closes #8 --- Attrap.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 6 ++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/Attrap.py b/Attrap.py index e02670f..6527cac 100644 --- a/Attrap.py +++ b/Attrap.py @@ -41,6 +41,8 @@ logger = logging.getLogger(__name__) class Attrap: class RAA: + """La classe représentant un Recueil des actes administratifs. La plupart du temps, il s'agit d'un PDF avec plusieurs arrêtés.""" + url = "" date = datetime.datetime(1970, 1, 1) date_str = "" @@ -59,11 +61,13 @@ class Attrap: self.name = name def get_sha256(self): + """Calcule et met en cache le hash sha256 de l'URL du PDF, pour servir d'identifiant unique.""" if (self.sha256 == ""): self.sha256 = hashlib.sha256(self.url.encode('utf-8')).hexdigest() return self.sha256 def get_pdf_dates(self, data_dir): + """Extrait les dates des PDF pour les ajouter à l'objet du RAA.""" raa_data_dir = f'{data_dir}/raa/' reader = PdfReader(f'{raa_data_dir}{self.get_sha256()}.pdf') @@ -77,6 +81,7 @@ class Attrap: self.pdf_modification_date = pdf_metadata.modification_date def extract_content(self, data_dir): + """Extrait le contenu du PDF OCRisé pour l'écrire dans le fichier qui servira à faire la recherche de mots-clés. Supprime tous les PDF à la fin.""" raa_data_dir = f'{data_dir}/raa/' text = "" @@ -100,6 +105,7 @@ class Attrap: os.remove(f'{raa_data_dir}{self.get_sha256()}.flat.pdf') def write_properties(self, data_dir): + """Écris les propriétés du RAA dans un fichier JSON.""" raa_data_dir = f'{data_dir}/raa/' pdf_creation_date_json = None @@ -123,6 +129,7 @@ class Attrap: f.close() def parse_metadata(self, data_dir): + """Lance l'extraction des dates du PDF puis l'écriture de ses propriétés dans un fichier JSON.""" self.get_pdf_dates(data_dir) self.write_properties(data_dir) @@ -155,6 +162,7 @@ class Attrap: self.print_output(str(self.__class__.__name__)) def configure_mastodon(self, access_token, instance, mastodon_prefix, mastodon_suffix): + """Configuration de Mastodon afin de publier un toot à chaque RAA détecté.""" if access_token and access_token != "" and instance and instance != "": self.mastodon = Mastodon( access_token=access_token, @@ -164,6 +172,7 @@ class Attrap: self.mastodon_suffix = mastodon_suffix def mastodon_toot(self, content): + """Publie le toot en ajoutant le header et footer configurés.""" if self.mastodon: toot = content if not self.mastodon_prefix == '': @@ -173,6 +182,7 @@ class Attrap: self.mastodon.toot(toot) def enable_tor(self, max_requests=0): + """Active l'utilisation de Tor pour effectuer les requêtes.""" proxies = { "http": f"socks5h://127.0.0.1:9050", "https": f"socks5h://127.0.0.1:9050", @@ -184,6 +194,7 @@ class Attrap: self.tor_get_new_id() def disable_tor(self): + """Désactive l'utilisation de Tor.""" proxies = {} self.tor_enabled = False self.tor_max_requests = 0 @@ -191,6 +202,7 @@ class Attrap: self.session.proxies.update(proxies) def tor_get_new_id(self): + """Change d'identité Tor. Cela permet de changer de noeud de sortie donc d'IP.""" if self.tor_enabled: logger.info('Changement d\'identité Tor') try: @@ -204,6 +216,14 @@ class Attrap: logger.debug(f'Impossible de changer d\'identité Tor: {exc}') def get_sub_pages(self, page_content, element, host, recursive_until_pdf): + """ + Récupère, à partir d'un chemin CSS, les sous-pages d'une page. + + page_content -- Un contenu HTML à analyser + element -- Le chemin CSS vers l'objet renvoyant vers la sous-page recherchée + host -- Le nom d'hôte du site + recursive_until_pdf -- Un booléen pour savoir s'il faut rechercher un fichier PDF dans le chemin CSS. Le cas échéant, relance la recherche sur la sous-page si le lien n'est pas un PDF. + """ soup = BeautifulSoup(page_content, 'html.parser') sub_pages = [] for a in soup.select(element): @@ -237,6 +257,15 @@ class Attrap: return sub_pages def get_sub_pages_with_pager(self, page, sub_page_element, pager_element, details_element, host): + """ + Récupère, à partir d'un chemin CSS, les sous-pages d'une page contenant un pager. + + page -- L'URL de la page à analyser + sub_page_element -- Le chemin CSS vers l'objet renvoyant vers la sous-page recherchée + pager_element -- Le chemin CSS vers le lien de page suivante du pager + details_element -- Le chemin CSS vers l'objet contenant les détails de la sous-page recherchée + host -- Le nom d'hôte du site + """ pages = [] page_content = self.get_page(page, 'get').content @@ -276,6 +305,13 @@ class Attrap: return pages def get_raa_with_pager(self, pages_list, pager_element, host): + """ + Récupère et analyse les RAA d'une page contenant un pager. + + pages_list -- Un tableau contenant la liste des pages + pager_element -- Le chemin CSS vers le lien de page suivante du pager + host -- Le nom d'hôte du site + """ elements = [] # On parse chaque page passée en paramètre for page in pages_list: @@ -303,9 +339,15 @@ class Attrap: return elements def set_sleep_time(self, sleep_time): + """Configure le temps de temporisation""" self.sleep_time = sleep_time def has_pdf(self, page_content): + """ + Renvoie un booléen Vrai si la page contient un lien vers un PDF + + page_content -- Un contenu HTML à analyser + """ elements = [] soup = BeautifulSoup(page_content, 'html.parser') for a in soup.find_all('a', href=True): @@ -315,6 +357,13 @@ class Attrap: # On démarre le navigateur def get_session(self, url, wait_element, remaining_retries=0): + """ + Lance un navigateur avec Selenium. + + url -- URL à interroger + wait_element -- Élement (désigné par son identifiant CSS) qui indique que la page est chargée + remaining_retries -- Nombre d'échecs autorisé avant de soulever une erreur + """ webdriver_options = webdriver.ChromeOptions() webdriver_options.add_argument("--no-sandbox") webdriver_options.add_argument("--disable-extensions") @@ -369,6 +418,7 @@ class Attrap: return page_content def print_output(self, data): + """Affiche dans le terminal et dans le fichier de log un texte""" print(data) data = data.replace('\033[92m', '') data = data.replace('\033[0m', '') @@ -378,6 +428,13 @@ class Attrap: f.close() def get_page(self, url, method, data={}): + """ + Récupère le contenu HTML d'une page web + + url -- L'URL de la page demandée + method -- 'post' ou 'get', selon le type de requête + data -- Un dictionnaire contenant les données à envoyer au site + """ try: logger.debug(f'Chargement de la page {url}') if self.sleep_time > 0: @@ -412,10 +469,12 @@ class Attrap: return self.get_page(url, method, data) def update_user_agent(self, user_agent): + """Change la valeur du user-agent""" self.user_agent = user_agent self.session.headers.update({'User-Agent': self.user_agent}) def download_file(self, raa): + """Télécharge un RAA""" try: os.makedirs( os.path.dirname(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf'), @@ -433,6 +492,7 @@ class Attrap: logger.warning(f'ATTENTION: Impossible de télécharger le fichier {raa.url}: {exc}') def ocr(self, raa, retry_on_failure=True): + """OCRise un RAA""" cmd = [ 'ocrmypdf', '-l', 'eng+fra', @@ -460,7 +520,7 @@ class Attrap: shutil.copy(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf', f'{self.data_dir}/raa/{raa.get_sha256()}.ocr.pdf') def flatten_pdf(self, raa): - # OCRmyPDF ne sait pas gérer les formulaires, donc on les enlève avant OCRisation + """Supprime les formulaires d'un PDF pour pouvoir les OCRiser après dans OCRmyPDF.""" reader = PdfReader(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf') writer = PdfWriter() @@ -475,6 +535,7 @@ class Attrap: writer.write(f'{self.data_dir}/raa/{raa.get_sha256()}.flat.pdf') def search_keywords(self, raa, keywords): + """Recherche des mots-clés dans le texte extrait du PDF""" if keywords and not keywords == '': text = open(f'{self.data_dir}/raa/{raa.get_sha256()}.txt').read() @@ -503,6 +564,12 @@ class Attrap: ) def parse_raa(self, elements, keywords): + """ + Démarre l'analyse des RAA. + + elements -- Un tableau contenant les RAA à analyser + keywords -- Les mots-clés à rechercher dans chaque RAA + """ self.print_output(f'Termes recherchés: {keywords}') self.print_output('') @@ -525,6 +592,19 @@ class Attrap: def configure_mailer(self, smtp_host, smtp_username, smtp_password, smtp_port, smtp_starttls, smtp_ssl, email_from, email_to, email_object): + """ + Configure les courriers électroniques. + + smtp_host -- Nom d'hôte du serveur SMTP + smtp_username -- Nom d'utilisateur pour se connecter au serveur SMTP + smtp_password -- Mot de passe pour se connecter au serveur SMTP + smtp_port -- Port de connexion au serveur SMTP + smtp_starttls -- Booléen. Si vrai, se connecte avec STARTTLS + smtp_ssl -- Booléen. Si vrai, se connecte avec SSL/TLS + email_from -- Adresse électronique de l'expéditeur des notifications + email_to -- Tableau contenant les adresses électroniques à notifier + email_object -- Objet du courrier électronique de notification + """ self.smtp_host = smtp_host self.smtp_username = smtp_username self.smtp_password = smtp_password @@ -585,9 +665,11 @@ class Attrap: except Exception as exc: logger.warning(f'Impossible d\'envoyer le courrier électronique : {exc}') - # Fonction qui essaie de deviner la date d'un RAA à partir de son nom. - # Utile pour limiter les requêtes lors de l'obtention des RAA à scanner. def guess_date(string, regex): + """ + Essaie de deviner la date d'un RAA à partir de son nom. + Utile pour limiter les requêtes lors de l'obtention des RAA à scanner. + """ try: search = re.search(regex, string, re.IGNORECASE) guessed_date = dateparser.parse(search.group(1)) diff --git a/README.md b/README.md index 61bbb4d..17ed5d6 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ Les contributions à ce projet sont les bienvenues ! Chaque administration est gérée par un fichier dont le nom correspond à son identifiant (`Attrap_XXX.py`). Commencez par copier un de ces fichiers puis adaptez son code à l'administration que vous voulez ajouter. Il est impératif de lancer le moins de requêtes possibles vers le site de l'administration : lorsqu'une administration a une page par année ou par mois, ne lancez une requête que vers les pages qui correspondent à la plage temporelle demandée dans la valeur de configuration `NOT_BEFORE`. +Vous pouvez lancer la commande suivante pour connaître fonctions disponibles pour récupérer les RAA sur le site d'une administration : + +``` +bin/python -m pydoc Attrap +``` + Avant d'ouvrir une merge request, assurez-vous que : - l'administration est activée dans `cli.py` et dans `Makefile` ; - il existe un job dans la CI (`.gitlab-ci.yml`) pour l'administration ; -- GitLab