Skip to content
Extraits de code Groupes Projets
Valider 35523063 rédigé par Bastien Le Querrec's avatar Bastien Le Querrec
Parcourir les fichiers

Attrap: améliore la documentation du code.

Closes #7
Closes #8
parent 5db482a4
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
......@@ -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))
......
......@@ -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 ;
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter