Skip to content
Extraits de code Groupes Projets

Comparer les révisions

Les modifications sont affichées comme si la révision source était fusionnée avec la révision cible. En savoir plus sur la comparaison des révisions.

Source

Sélectionner le projet cible
No results found

Cible

Sélectionner le projet cible
  • la-quadrature-du-net/Attrap
  • foggyfrog/Attrap
  • skhwiz/Attrap
  • precambrien/Attrap
  • ketsapiwiq/Attrap
  • Joseki/Attrap
  • kr1p/attrap-pref-12
  • kr1p/attrap-pref-46
  • kr1p/attrap-pi
  • Guinness/Attrap
  • astroidgritty/attrap-pref-84
  • davinov/Attrap
  • maettellite/attrap-pref-01
  • m242/Attrap
  • multi/Attrap
  • mverdeil/Attrap
  • olpo/Attrap
17 résultats
Afficher les modifications
Validations sur la source (211)
__pycache__/ __pycache__/
bin/ bin/
lib/ lib/
lib64
data/ data/
pyvenv.cfg pyvenv.cfg
output_*.log output_*.log
*.patch
CACHEDIR.TAG
...@@ -33,7 +33,7 @@ docker: ...@@ -33,7 +33,7 @@ docker:
install: install:
stage: install stage: install
image: registry.git.laquadrature.net/la-quadrature-du-net/raaspotter/base:latest image: registry.git.laquadrature.net/la-quadrature-du-net/attrap/base:latest
tags: tags:
- unprivileged - unprivileged
script: script:
...@@ -51,7 +51,7 @@ install: ...@@ -51,7 +51,7 @@ install:
pep8: pep8:
stage: lint stage: lint
image: registry.git.laquadrature.net/la-quadrature-du-net/raaspotter/base:latest image: registry.git.laquadrature.net/la-quadrature-du-net/attrap/base:latest
needs: [install] needs: [install]
tags: tags:
- unprivileged - unprivileged
...@@ -62,7 +62,6 @@ pep8: ...@@ -62,7 +62,6 @@ pep8:
- lib/ - lib/
- pyvenv.cfg - pyvenv.cfg
script: script:
- source bin/activate
- make lint - make lint
allow_failure: true allow_failure: true
rules: rules:
...@@ -70,28 +69,26 @@ pep8: ...@@ -70,28 +69,26 @@ pep8:
.default_pref: .default_pref:
stage: test stage: test
image: registry.git.laquadrature.net/la-quadrature-du-net/raaspotter/base:latest image: registry.git.laquadrature.net/la-quadrature-du-net/attrap/base:latest
tags: tags:
- unprivileged - unprivileged
needs: [install] needs: [install]
script: script:
- misc/download-from-s3.sh "${PREF}" "${S3_KEY}" "${S3_SECRET}" "${S3_HOST}" "${S3_BUCKET}" data/ || true
- source bin/activate - source bin/activate
- pip install --upgrade -r requirements.txt
- /etc/init.d/tor start - /etc/init.d/tor start
- python ./cli.py --pref "${PREF}" - make "${PREF}"
retry: 2 - misc/upload-to-s3.sh "${PREF}" "${S3_KEY}" "${S3_SECRET}" "${S3_HOST}" "${S3_BUCKET}" data/ || true
cache: cache:
key: $CI_COMMIT_REF_SLUG-$CI_JOB_NAME_SLUG key: $CI_COMMIT_REF_SLUG
fallback_keys:
- $CI_COMMIT_REF_SLUG
paths: paths:
- bin/ - bin/
- lib/ - lib/
- pyvenv.cfg - pyvenv.cfg
- data/${PREF}/*.txt
artifacts: artifacts:
paths: paths:
- data/${PREF}/*.txt - data/${PREF}/raa/*.txt
- data/${PREF}/raa/*.json
- output_${PREF}.log - output_${PREF}.log
expire_in: 2 days expire_in: 2 days
rules: rules:
...@@ -102,6 +99,21 @@ test_ppparis: ...@@ -102,6 +99,21 @@ test_ppparis:
PREF: "ppparis" PREF: "ppparis"
extends: .default_pref extends: .default_pref
test_pref01:
variables:
PREF: "pref01"
extends: .default_pref
test_pref02:
variables:
PREF: "pref02"
extends: .default_pref
test_pref03:
variables:
PREF: "pref03"
extends: .default_pref
test_pref04: test_pref04:
variables: variables:
PREF: "pref04" PREF: "pref04"
...@@ -122,16 +134,56 @@ test_pref09: ...@@ -122,16 +134,56 @@ test_pref09:
PREF: "pref09" PREF: "pref09"
extends: .default_pref extends: .default_pref
test_pref10:
variables:
PREF: "pref10"
extends: .default_pref
test_pref11:
variables:
PREF: "pref11"
extends: .default_pref
test_pref13: test_pref13:
variables: variables:
PREF: "pref13" PREF: "pref13"
extends: .default_pref extends: .default_pref
test_pref2a:
variables:
PREF: "pref2a"
extends: .default_pref
test_pref2b:
variables:
PREF: "pref2b"
extends: .default_pref
test_pref25:
variables:
PREF: "pref25"
extends: .default_pref
test_pref29:
variables:
PREF: "pref29"
extends: .default_pref
test_pref30:
variables:
PREF: "pref30"
extends: .default_pref
test_pref31: test_pref31:
variables: variables:
PREF: "pref31" PREF: "pref31"
extends: .default_pref extends: .default_pref
test_pref33:
variables:
PREF: "pref33"
extends: .default_pref
test_pref34: test_pref34:
variables: variables:
PREF: "pref34" PREF: "pref34"
...@@ -147,21 +199,66 @@ test_pref38: ...@@ -147,21 +199,66 @@ test_pref38:
PREF: "pref38" PREF: "pref38"
extends: .default_pref extends: .default_pref
test_pref39:
variables:
PREF: "pref39"
extends: .default_pref
test_pref42: test_pref42:
variables: variables:
PREF: "pref42" PREF: "pref42"
extends: .default_pref extends: .default_pref
test_pref44:
variables:
PREF: "pref44"
extends: .default_pref
test_pref49:
variables:
PREF: "pref49"
extends: .default_pref
test_pref50:
variables:
PREF: "pref50"
extends: .default_pref
test_pref52:
variables:
PREF: "pref52"
extends: .default_pref
test_pref54:
variables:
PREF: "pref54"
extends: .default_pref
test_pref55:
variables:
PREF: "pref55"
extends: .default_pref
test_pref59: test_pref59:
variables: variables:
PREF: "pref59" PREF: "pref59"
extends: .default_pref extends: .default_pref
test_pref61:
variables:
PREF: "pref61"
extends: .default_pref
test_pref62: test_pref62:
variables: variables:
PREF: "pref62" PREF: "pref62"
extends: .default_pref extends: .default_pref
test_pref63:
variables:
PREF: "pref63"
extends: .default_pref
test_pref64: test_pref64:
variables: variables:
PREF: "pref64" PREF: "pref64"
...@@ -182,6 +279,26 @@ test_pref69: ...@@ -182,6 +279,26 @@ test_pref69:
PREF: "pref69" PREF: "pref69"
extends: .default_pref extends: .default_pref
test_pref73:
variables:
PREF: "pref73"
extends: .default_pref
test_pref75:
variables:
PREF: "pref75"
extends: .default_pref
test_pref76:
variables:
PREF: "pref76"
extends: .default_pref
test_pref77:
variables:
PREF: "pref77"
extends: .default_pref
test_pref80: test_pref80:
variables: variables:
PREF: "pref80" PREF: "pref80"
...@@ -197,7 +314,47 @@ test_pref83: ...@@ -197,7 +314,47 @@ test_pref83:
PREF: "pref83" PREF: "pref83"
extends: .default_pref extends: .default_pref
test_pref87:
variables:
PREF: "pref87"
extends: .default_pref
test_pref91:
variables:
PREF: "pref91"
extends: .default_pref
test_pref92:
variables:
PREF: "pref92"
extends: .default_pref
test_pref93:
variables:
PREF: "pref93"
extends: .default_pref
test_pref94:
variables:
PREF: "pref94"
extends: .default_pref
test_pref976: test_pref976:
variables: variables:
PREF: "pref976" PREF: "pref976"
extends: .default_pref extends: .default_pref
test_prefbretagne:
variables:
PREF: "prefbretagne"
extends: .default_pref
test_prefidf:
variables:
PREF: "prefidf"
extends: .default_pref
test_prefpaca:
variables:
PREF: "prefpaca"
extends: .default_pref
import os import os
import sys
import re import re
import random
import ssl import ssl
import sys
import subprocess import subprocess
import shutil
import string
import logging import logging
import requests import requests
import time import time
from types import SimpleNamespace
import datetime import datetime
import json
from urllib.parse import quote from urllib.parse import quote
from selenium import webdriver from selenium import webdriver
...@@ -15,14 +20,18 @@ from selenium.webdriver.common.by import By ...@@ -15,14 +20,18 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions from selenium.webdriver.support import expected_conditions
import pytz
import dateparser import dateparser
import urllib3
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pyvirtualdisplay import Display from pyvirtualdisplay import Display
from pdfminer.high_level import extract_text
from stem import Signal from pypdf import PdfReader
from stem.control import Controller from pypdf import PdfWriter
from pypdf.generic import NameObject, NumberObject
from pypdf.errors import PdfStreamError
from pypdf.errors import EmptyFileError
import hashlib import hashlib
import smtplib import smtplib
...@@ -30,50 +39,138 @@ import email ...@@ -30,50 +39,138 @@ import email
from mastodon import Mastodon from mastodon import Mastodon
import ftfy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RAAspotter: class Attrap:
class RAA: 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 = "" url = ""
date = datetime.datetime(1970, 1, 1) date = None
date_str = ""
name = "" name = ""
filename = ""
sha256 = "" sha256 = ""
pdf_creation_date = None
pdf_modification_date = None
def __init__(self, url, date, name, filename): def __init__(self, url, date, name, timezone='Europe/Paris'):
self.timezone = timezone
if not url == "": if not url == "":
self.url = url self.url = url
if not date == "": if date is not None:
self.date = date self.date = Attrap.get_aware_datetime(date, timezone=timezone)
self.date_str = date.strftime("%d/%m/%Y")
if not name == "": if not name == "":
self.name = name self.name = name
if not filename == "":
self.filename = filename
def get_sha256(self): 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 == ""): if (self.sha256 == ""):
self.sha256 = hashlib.sha256(self.filename.encode('utf-8')).hexdigest() self.sha256 = hashlib.sha256(self.url.encode('utf-8')).hexdigest()
return self.sha256 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')
pdf_metadata = reader.metadata
if pdf_metadata:
if pdf_metadata.creation_date:
self.pdf_creation_date = Attrap.get_aware_datetime(pdf_metadata.creation_date, timezone=self.timezone)
if self.date is None:
self.date = Attrap.get_aware_datetime(pdf_metadata.creation_date, timezone=self.timezone)
if pdf_metadata.modification_date:
self.pdf_modification_date = Attrap.get_aware_datetime(pdf_metadata.modification_date, timezone=self.timezone)
if self.date is None:
self.date = Attrap.get_aware_datetime(pdf_metadata.modification_date, timezone=self.timezone)
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 = ""
reader = PdfReader(f'{raa_data_dir}{self.get_sha256()}.ocr.pdf')
ftfy_config = ftfy.TextFixerConfig(unescape_html=False, explain=False)
for page in reader.pages:
try:
text = text + "\n" + ftfy.fix_text(page.extract_text(), config=ftfy_config)
except Exception as exc:
logger.warning(f'ATTENTION: Impossible d\'extraire le texte du fichier {self.get_sha256()}.pdf : {exc}')
# Écrit le texte du PDF dans un fichier texte pour une analyse future
f = open(f'{raa_data_dir}{self.get_sha256()}.txt', 'w')
f.write(text)
f.close()
# Supprime le PDF d'origine et la version OCRisée
os.remove(f'{raa_data_dir}{self.get_sha256()}.pdf')
os.remove(f'{raa_data_dir}{self.get_sha256()}.ocr.pdf')
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
pdf_modification_date_json = None
if self.pdf_creation_date:
pdf_creation_date_json = self.pdf_creation_date.astimezone(pytz.utc).isoformat(timespec="seconds")
if self.pdf_modification_date:
pdf_modification_date_json = self.pdf_modification_date.astimezone(pytz.utc).isoformat(timespec="seconds")
properties = {
'version': 2,
'name': self.name,
'date': self.date.strftime("%Y-%m-%d"),
'url': quote(self.url, safe='/:'),
'first_seen_on': datetime.datetime.now(pytz.utc).isoformat(timespec="seconds"),
'pdf_creation_date': pdf_creation_date_json,
'pdf_modification_date': pdf_modification_date_json,
'timezone': self.timezone
}
f = open(f'{raa_data_dir}{self.get_sha256()}.json', 'w')
f.write(json.dumps(properties))
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)
def __init__(self, data_dir, user_agent=''): def __init__(self, data_dir, user_agent=''):
logger.debug('Initialisation de RAAspotter') """
Initialise Attrap et le dossier de données.
data_dir -- le dossier où sont situées les données
user_agent -- le user_agent utilisé pour les requêtes
"""
logger.debug('Initialisation de Attrap')
# On crée le dossier de téléchargement
os.makedirs(data_dir, exist_ok=True)
self.session = requests.Session() self.session = requests.Session()
self.data_dir = data_dir self.data_dir = data_dir
self.found = False self.found = False
self.output_file_path = os.path.dirname(os.path.abspath(__file__)) + f'/output_{self.short_code}.log' self.output_file_path = os.path.dirname(os.path.abspath(__file__)) + f'/output_{self.short_code}.log'
self.sleep_time = 0 self.sleep_time = 0
self.last_http_request = 0
self.tor_enabled = False self.tor_enabled = False
self.tor_max_requests = 0 self.tor_max_requests = 0
self.tor_requests = 0 self.tor_requests = 0
self.tor_socks5_key = None
self.not_before = datetime.datetime(2024, 1, 1) self.not_before = datetime.datetime(2024, 1, 1)
self.smtp_configured = False self.smtp_configured = False
self.mastodon = None self.mastodon = None
self.mastodon_prefix = '' self.mastodon_prefix = ''
self.mastodon_suffix = '' self.mastodon_suffix = ''
self.safe_mode = False
self.timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname()
self.update_user_agent(user_agent) self.update_user_agent(user_agent)
...@@ -81,7 +178,16 @@ class RAAspotter: ...@@ -81,7 +178,16 @@ class RAAspotter:
f.write('') f.write('')
f.close() f.close()
self.print_output(str(self.__class__.__name__))
# Si le safe mode est activé, on configure un long délai entre chaque requête
if os.getenv('SAFE_MODE'):
self.safe_mode = True
logger.warning('ATTENTION: le safe mode est activé, configuration d\'un délai entre chaque requête')
self.sleep_time = 30
def configure_mastodon(self, access_token, instance, mastodon_prefix, mastodon_suffix): 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 != "": if access_token and access_token != "" and instance and instance != "":
self.mastodon = Mastodon( self.mastodon = Mastodon(
access_token=access_token, access_token=access_token,
...@@ -91,6 +197,7 @@ class RAAspotter: ...@@ -91,6 +197,7 @@ class RAAspotter:
self.mastodon_suffix = mastodon_suffix self.mastodon_suffix = mastodon_suffix
def mastodon_toot(self, content): def mastodon_toot(self, content):
"""Publie le toot en ajoutant le header et footer configurés."""
if self.mastodon: if self.mastodon:
toot = content toot = content
if not self.mastodon_prefix == '': if not self.mastodon_prefix == '':
...@@ -100,17 +207,16 @@ class RAAspotter: ...@@ -100,17 +207,16 @@ class RAAspotter:
self.mastodon.toot(toot) self.mastodon.toot(toot)
def enable_tor(self, max_requests=0): def enable_tor(self, max_requests=0):
proxies = { """Active l'utilisation de Tor pour effectuer les requêtes."""
"http": f"socks5h://127.0.0.1:9050", if not self.safe_mode:
"https": f"socks5h://127.0.0.1:9050", self.tor_enabled = True
} self.tor_max_requests = max_requests
self.tor_enabled = True self.tor_get_new_id()
self.tor_max_requests = max_requests else:
self.tor_requests = 0 logger.warning('ATTENTION: le safe mode est activé, Tor n\'a pas été activé')
self.session.proxies.update(proxies)
self.tor_get_new_id()
def disable_tor(self): def disable_tor(self):
"""Désactive l'utilisation de Tor."""
proxies = {} proxies = {}
self.tor_enabled = False self.tor_enabled = False
self.tor_max_requests = 0 self.tor_max_requests = 0
...@@ -118,18 +224,26 @@ class RAAspotter: ...@@ -118,18 +224,26 @@ class RAAspotter:
self.session.proxies.update(proxies) self.session.proxies.update(proxies)
def tor_get_new_id(self): def tor_get_new_id(self):
logger.info('Changement d\'identité Tor') """Change de circuit Tor. Cela permet de changer de noeud de sortie donc d'IP."""
try: if self.tor_enabled:
self.session.close() self.tor_socks5_key = 'attrap_' + ''.join(random.choices(string.ascii_lowercase, k=20))
controller = Controller.from_port(port=9051) proxies = {
controller.authenticate() "http": f"socks5h://attrap:{self.tor_socks5_key}@127.0.0.1:9050",
controller.signal(Signal.NEWNYM) "https": f"socks5h://attrap:{self.tor_socks5_key}@127.0.0.1:9050",
time.sleep(5) }
self.session.proxies.update(proxies)
self.tor_requests = 0 self.tor_requests = 0
except Exception as exc:
logger.debug(f'Impossible de changer d\'identité Tor: {exc}')
def get_sub_pages(self, page_content, element, host, recursive_until_pdf): def get_sub_pages(self, page_content, element, host, recursive_until_pdf, selenium=False):
"""
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.
selenium -- lance un navigateur avec Selenium pour contourner les protections anti-robots
"""
soup = BeautifulSoup(page_content, 'html.parser') soup = BeautifulSoup(page_content, 'html.parser')
sub_pages = [] sub_pages = []
for a in soup.select(element): for a in soup.select(element):
...@@ -145,7 +259,8 @@ class RAAspotter: ...@@ -145,7 +259,8 @@ class RAAspotter:
sub_page_content, sub_page_content,
element, element,
host, host,
recursive_until_pdf recursive_until_pdf,
selenium=selenium
): ):
sub_pages.append(sub_sub_page) sub_pages.append(sub_sub_page)
else: else:
...@@ -162,9 +277,24 @@ class RAAspotter: ...@@ -162,9 +277,24 @@ class RAAspotter:
sub_pages.append(sub_page) sub_pages.append(sub_page)
return sub_pages return sub_pages
def get_sub_pages_with_pager(self, page, sub_page_element, pager_element, details_element, host): def get_sub_pages_with_pager(self, page, sub_page_element, pager_element, details_element, host, selenium=False):
"""
Récupère, à partir d'un chemin CSS, les sous-pages d'une page contenant un pager.
page -- L'URL ou le contenu HTML 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
selenium -- lance un navigateur avec Selenium pour contourner les protections anti-robots
"""
pages = [] pages = []
page_content = self.get_page(page, 'get').content if isinstance(page, bytes):
page = page.decode('utf-8')
if page.startswith('https://') or page.startswith('http://'):
page_content = self.get_page(page, 'get', selenium=selenium).content
else:
page_content = page
# On initialise le parser # On initialise le parser
soup = BeautifulSoup(page_content, 'html.parser') soup = BeautifulSoup(page_content, 'html.parser')
...@@ -195,43 +325,64 @@ class RAAspotter: ...@@ -195,43 +325,64 @@ class RAAspotter:
sub_page_element, sub_page_element,
pager_element, pager_element,
details_element, details_element,
host host,
selenium=selenium
): ):
pages.append(sub_page) pages.append(sub_page)
return pages return pages
def get_raa_with_pager(self, pages_list, pager_element, host): def get_raa_with_pager(self, pages_list, pager_element, host, filter_from_last_element_date=False, selenium=False):
"""
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
filter_from_last_element_date -- (Optionnel) Si la date du dernier élément de la dernière page parsée
n'est pas dans la plage temporelle voulue, ne charge pas les pages suivantes. Par défaut à False. Ne doit
être activé que si l'ordre des éléments est chronologique.
selenium -- lance un navigateur avec Selenium pour contourner les protections anti-robots
"""
elements = [] elements = []
# On parse chaque page passée en paramètre # On parse chaque page passée en paramètre
for page in pages_list: for page in pages_list:
page_content = self.get_page(page, 'get').content page_content = self.get_page(page, 'get', selenium=selenium).content
# Pour chaque page, on récupère les PDF # Pour chaque page, on récupère les PDF
for raa in self.get_raa_elements(page_content): for raa in self.get_raa_elements(page_content):
elements.append(raa) elements.append(raa)
# On regarde également s'il n'y aurait pas un pager # Si la date du dernier RAA est dans la plage temporelle voulue,
sub_pages = [] # on regarde également s'il n'y aurait pas un pager
for sub_page in self.get_sub_pages( if not filter_from_last_element_date or (filter_from_last_element_date and (elements[-1].date >= Attrap.get_aware_datetime(self.not_before, timezone=self.timezone))):
page_content, sub_pages = []
pager_element, for sub_page in self.get_sub_pages(
host, page_content,
True pager_element,
): host,
sub_pages.append(sub_page['url']) True
for sub_raa in self.get_raa_with_pager( ):
sub_pages, sub_pages.append(sub_page['url'])
pager_element, for sub_raa in self.get_raa_with_pager(
host sub_pages,
): pager_element,
elements.append(sub_raa) host,
filter_from_last_element_date=filter_from_last_element_date
):
elements.append(sub_raa)
return elements return elements
def set_sleep_time(self, sleep_time): def set_sleep_time(self, sleep_time):
"""Configure le temps de temporisation"""
self.sleep_time = sleep_time self.sleep_time = sleep_time
def has_pdf(self, page_content): 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 = [] elements = []
soup = BeautifulSoup(page_content, 'html.parser') soup = BeautifulSoup(page_content, 'html.parser')
for a in soup.find_all('a', href=True): for a in soup.find_all('a', href=True):
...@@ -241,6 +392,13 @@ class RAAspotter: ...@@ -241,6 +392,13 @@ class RAAspotter:
# On démarre le navigateur # On démarre le navigateur
def get_session(self, url, wait_element, remaining_retries=0): 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 = webdriver.ChromeOptions()
webdriver_options.add_argument("--no-sandbox") webdriver_options.add_argument("--no-sandbox")
webdriver_options.add_argument("--disable-extensions") webdriver_options.add_argument("--disable-extensions")
...@@ -248,12 +406,17 @@ class RAAspotter: ...@@ -248,12 +406,17 @@ class RAAspotter:
webdriver_options.add_argument("--disable-dev-shm-usage") webdriver_options.add_argument("--disable-dev-shm-usage")
webdriver_options.add_argument("--use_subprocess") webdriver_options.add_argument("--use_subprocess")
webdriver_options.add_argument("--disable-blink-features=AutomationControlled") webdriver_options.add_argument("--disable-blink-features=AutomationControlled")
webdriver_options.add_experimental_option("excludeSwitches", ["enable-automation"])
webdriver_options.add_experimental_option('useAutomationExtension', False)
if not self.user_agent == "": if not self.user_agent == "":
webdriver_options.add_argument(f"--user-agent={self.user_agent}") webdriver_options.add_argument(f"--user-agent={self.user_agent}")
webdriver_options.add_argument("--headless") if self.tor_enabled:
webdriver_options.add_argument("--window-size=1024,768") webdriver_options.add_argument(f'--proxy-server=socks5://127.0.0.1:9050')
webdriver_options.add_argument("--headless=new")
webdriver_options.add_argument("--start-maximized")
display = Display(visible=False, size=(1024, 768)) display = Display(visible=False, size=(1024, 768))
display.start() display.start()
...@@ -277,6 +440,9 @@ class RAAspotter: ...@@ -277,6 +440,9 @@ class RAAspotter:
except TimeoutException as exc: except TimeoutException as exc:
logger.warning(f'TimeoutException: {exc}') logger.warning(f'TimeoutException: {exc}')
if remaining_retries > 0: if remaining_retries > 0:
time.sleep(5)
if self.tor_enabled:
self.tor_get_new_id()
return self.get_session(url, wait_element, (remaining_retries - 1)) return self.get_session(url, wait_element, (remaining_retries - 1))
else: else:
raise TimeoutException(exc) raise TimeoutException(exc)
...@@ -294,6 +460,7 @@ class RAAspotter: ...@@ -294,6 +460,7 @@ class RAAspotter:
return page_content return page_content
def print_output(self, data): def print_output(self, data):
"""Affiche dans le terminal et dans le fichier de log un texte"""
print(data) print(data)
data = data.replace('\033[92m', '') data = data.replace('\033[92m', '')
data = data.replace('\033[0m', '') data = data.replace('\033[0m', '')
...@@ -302,22 +469,42 @@ class RAAspotter: ...@@ -302,22 +469,42 @@ class RAAspotter:
f.write(data + "\n") f.write(data + "\n")
f.close() f.close()
def get_page(self, url, method, data={}): def get_page(self, url, method, data={}, selenium=False):
"""
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
selenium -- lance un navigateur avec Selenium pour contourner les protections anti-robots
"""
try: try:
logger.debug(f'Chargement de la page {url}') logger.debug(f'Chargement de la page {url}')
# Si un délai a été configuré, on vérifie qu'il n'est pas trop tôt pour lancer la requête
if self.sleep_time > 0: if self.sleep_time > 0:
time.sleep(self.sleep_time) current_time = int(time.mktime(datetime.datetime.today().timetuple()))
remaining_sleep_time = self.last_http_request + self.sleep_time - current_time
if remaining_sleep_time > 0:
time.sleep(remaining_sleep_time)
self.last_http_request = int(time.mktime(datetime.datetime.today().timetuple()))
page = None page = None
if method == 'get': if selenium and method == 'get':
page = self.session.get(url) page_content = self.get_session(url, None, 6)
if method == 'post': page = {'content': page_content, 'status_code': 200}
page = self.session.post(url, data=data) page = SimpleNamespace(**page)
else:
if method == 'get':
page = self.session.get(url, timeout=(10, 120))
if method == 'post':
page = self.session.post(url, data=data, timeout=(10, 120))
if page.status_code == 429: if page.status_code == 429:
logger.info('Erreur 429 Too Many Requests reçue, temporisation...') logger.warning('Erreur 429 Too Many Requests reçue, temporisation...')
self.tor_get_new_id() self.tor_get_new_id()
time.sleep(55) time.sleep(1)
return self.get_page(url, method, data) return self.get_page(url, method, data)
if self.tor_enabled: if self.tor_enabled:
...@@ -327,24 +514,32 @@ class RAAspotter: ...@@ -327,24 +514,32 @@ class RAAspotter:
self.tor_get_new_id() self.tor_get_new_id()
return page return page
except requests.exceptions.ConnectionError as exc: except requests.exceptions.ConnectionError:
logger.info(f'Erreur de connexion, temporisation...') logger.warning(f'Erreur de connexion, temporisation...')
self.tor_get_new_id() self.tor_get_new_id()
time.sleep(55) time.sleep(30)
return self.get_page(url, method, data)
except requests.exceptions.Timeout:
logger.warning(f'Timeout, on relance la requête...')
return self.get_page(url, method, data)
except urllib3.exceptions.ProtocolError:
logger.warning(f'Erreur de connexion, on relance la requête...')
return self.get_page(url, method, data) return self.get_page(url, method, data)
def update_user_agent(self, user_agent): def update_user_agent(self, user_agent):
"""Change la valeur du user-agent"""
self.user_agent = user_agent self.user_agent = user_agent
self.session.headers.update({'User-Agent': self.user_agent}) self.session.headers.update({'User-Agent': self.user_agent})
def download_file(self, raa): def download_file(self, raa):
"""Télécharge un RAA"""
try: try:
os.makedirs( os.makedirs(
os.path.dirname(f'{self.data_dir}{raa.get_sha256()}.pdf'), os.path.dirname(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf'),
exist_ok=True exist_ok=True
) )
file = self.get_page(raa.url, 'get') file = self.get_page(raa.url, 'get')
f = open(f'{self.data_dir}{raa.get_sha256()}.pdf', 'wb') f = open(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf', 'wb')
f.write(file.content) f.write(file.content)
f.close() f.close()
except (requests.exceptions.ConnectionError, except (requests.exceptions.ConnectionError,
...@@ -354,37 +549,69 @@ class RAAspotter: ...@@ -354,37 +549,69 @@ class RAAspotter:
except Exception as exc: except Exception as exc:
logger.warning(f'ATTENTION: Impossible de télécharger le fichier {raa.url}: {exc}') logger.warning(f'ATTENTION: Impossible de télécharger le fichier {raa.url}: {exc}')
def parse_pdf(self, raa, keywords): def ocr(self, raa, retry_on_failure=True):
if not os.path.isfile(f'{self.data_dir}{raa.get_sha256()}.pdf'): """OCRise un RAA"""
logger.warning(f'ATTENTION: le fichier {raa.get_sha256()}.pdf n\'existe pas') cmd = [
else: 'python3',
text = "" 'bin/ocrmypdf',
try: '-l', 'eng+fra',
# pdfminer.six est un peu trop verbeux en mode debug, donc on relève son niveau de log '--output-type', 'pdf',
logging.getLogger("pdfminer").setLevel(logging.WARNING) '--redo-ocr',
text = extract_text(f'{self.data_dir}{raa.get_sha256()}.pdf') '--skip-big', '250',
except Exception as exc: '--max-image-mpixels', '250',
logger.warning(f'ATTENTION: Impossible d\'extraire le texte du fichier {raa.get_sha256()}.pdf : {exc}') '--invalidate-digital-signatures',
'--optimize', '0',
f'{self.data_dir}/raa/{raa.get_sha256()}.flat.pdf',
f'{self.data_dir}/raa/{raa.get_sha256()}.ocr.pdf'
]
logger.debug(f'Lancement de ocrmypdf: {cmd}')
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
if exc.returncode == 2 and retry_on_failure:
logger.warning('ATTENTION : Le fichier n\'est pas un PDF correct, nouvelle tentative de le télécharger')
if self.tor_enabled:
self.tor_get_new_id()
self.download_file(raa)
self.ocr(raa, False)
elif (not exc.returncode == 6) and (not exc.returncode == 10) and (not exc.returncode == 4):
logger.warning('ATTENTION : Impossible d\'OCRiser le document', exc.returncode, exc.output)
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):
"""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()
for page in reader.pages:
if page.get('/Annots'):
for annot in page.get('/Annots'):
writer_annot = annot.get_object()
writer_annot.update({
NameObject("/Ff"): NumberObject(1)
})
writer.add_page(page)
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()
date_str = raa.date.strftime("%d/%m/%Y")
found = False found = False
found_keywords = [] found_keywords = []
for keyword in keywords: for keyword in keywords.split(','):
if re.search(keyword, text, re.IGNORECASE | re.MULTILINE): if re.search(keyword, text, re.IGNORECASE | re.MULTILINE):
if not found: if not found:
url = quote(raa.url, safe='/:') url = quote(raa.url, safe='/:')
self.print_output(f'\033[92m{raa.name}\033[0m ({raa.date_str})') self.print_output(f'\033[92m{raa.name}\033[0m ({date_str})')
self.print_output(f'URL : {url}') self.print_output(f'URL : {url}')
found = True found = True
self.found = True self.found = True
self.print_output(f' Le terme \033[1m{keyword}\033[0m a été trouvé.') self.print_output(f' Le terme \033[1m{keyword}\033[0m a été trouvé.')
found_keywords.append(keyword) found_keywords.append(keyword)
# Écrit le texte du PDF dans un fichier texte pour une analyse
# future, puis supprime le PDF
f = open(f'{self.data_dir}{raa.get_sha256()}.txt', 'w')
f.write(text)
f.close()
os.remove(f'{self.data_dir}{raa.get_sha256()}.pdf')
if found: if found:
self.print_output('') self.print_output('')
url = quote(raa.url, safe='/:') url = quote(raa.url, safe='/:')
...@@ -392,45 +619,50 @@ class RAAspotter: ...@@ -392,45 +619,50 @@ class RAAspotter:
[str(x) for x in found_keywords] [str(x) for x in found_keywords]
) )
self.mastodon_toot( self.mastodon_toot(
f'{raa.name} ({raa.date_str})\n\nLes termes suivants ont ' f'{raa.name} ({date_str})\n\nLes termes suivants ont '
f'été trouvés : {found_keywords_str}.\n\nURL : {url}' f'été trouvés : {found_keywords_str}.\n\nURL : {url}'
) )
def ocr(self, raa, retry_on_failure=True):
cmd = [
'ocrmypdf',
'-l', 'eng+fra',
'--output-type', 'pdf',
'--redo-ocr',
'--skip-big', '500',
'--invalidate-digital-signatures',
f'{self.data_dir}{raa.get_sha256()}.pdf',
f'{self.data_dir}{raa.get_sha256()}.pdf'
]
logger.debug(f'Lancement de ocrmypdf: {cmd}')
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
if exc.returncode == 2 and retry_on_failure:
logger.warning('ATTENTION : Le fichier n\'est pas un PDF correct, nouvelle tentative de le télécharger')
if self.tor_enabled:
self.tor_get_new_id()
self.download_file(raa)
self.ocr(raa, False)
elif (not exc.returncode == 6) and (not exc.returncode == 10) and (not exc.returncode == 4):
logger.warning('ATTENTION : Impossible d\'OCRiser le document', exc.returncode, exc.output)
def parse_raa(self, elements, keywords): 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('')
for raa in elements: for raa in elements:
# Si le fichier n'a pas déjà été parsé et qu'il est postérieur à la # Si le fichier n'a pas déjà été parsé et qu'il est postérieur à la
# date maximale d'analyse, on le télécharge et on le parse # date maximale d'analyse, on le télécharge et on le parse
if raa.date >= self.not_before and \ if not os.path.isfile(f'{self.data_dir}/raa/{raa.get_sha256()}.txt') and (not raa.date or (raa.date >= Attrap.get_aware_datetime(self.not_before, timezone=self.timezone))):
not os.path.isfile(f'{self.data_dir}{raa.get_sha256()}.txt'):
url = quote(raa.url, safe='/:') url = quote(raa.url, safe='/:')
logger.info(f'Nouveau fichier : {raa.name} ({raa.date_str}). URL : {url}')
self.download_file(raa) self.download_file(raa)
self.ocr(raa, True) try:
self.parse_pdf(raa, keywords) raa.parse_metadata(self.data_dir)
# Lorsque la date du RAA n'est pas connue, on a dû télécharger le PDF pour récupérer la date de ses métadonnées.
# Donc on vérifie à nouveau ici si la date correspond à ce qu'on veut analyser
if not raa.date:
os.remove(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf')
os.remove(f'{self.data_dir}/raa/{raa.get_sha256()}.json')
logger.error(f'ERREUR: le RAA {raa.name} n\'a pas de date !')
sys.exit(1)
if raa.date >= Attrap.get_aware_datetime(self.not_before, timezone=self.timezone):
date_str = raa.date.strftime("%d/%m/%Y")
logger.info(f'Nouveau fichier : {raa.name} ({date_str}). URL : {url}')
self.flatten_pdf(raa)
self.ocr(raa, True)
raa.extract_content(self.data_dir)
self.search_keywords(raa, keywords)
else:
# On supprime le fichier de metadonnées puisqu'on ne le parsera pas
os.remove(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf')
os.remove(f'{self.data_dir}/raa/{raa.get_sha256()}.json')
except PdfStreamError as exc:
logger.warning(f'ATTENTION: le RAA à l\'adresse {raa.url} n\'est pas valide ! On l\'ignore...')
except EmptyFileError as exc:
logger.warning(f'ATTENTION: le RAA à l\'adresse {raa.url} est vide ! On l\'ignore...')
def get_raa(self, page_content): def get_raa(self, page_content):
logger.error('Cette fonction doit être surchargée') logger.error('Cette fonction doit être surchargée')
...@@ -438,6 +670,19 @@ class RAAspotter: ...@@ -438,6 +670,19 @@ class RAAspotter:
def configure_mailer(self, smtp_host, smtp_username, smtp_password, def configure_mailer(self, smtp_host, smtp_username, smtp_password,
smtp_port, smtp_starttls, smtp_ssl, email_from, smtp_port, smtp_starttls, smtp_ssl, email_from,
email_to, email_object): 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_host = smtp_host
self.smtp_username = smtp_username self.smtp_username = smtp_username
self.smtp_password = smtp_password self.smtp_password = smtp_password
...@@ -498,12 +743,14 @@ class RAAspotter: ...@@ -498,12 +743,14 @@ class RAAspotter:
except Exception as exc: except Exception as exc:
logger.warning(f'Impossible d\'envoyer le courrier électronique : {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): 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: try:
search = re.search(regex, string, re.IGNORECASE) search = re.search(regex, string, re.IGNORECASE)
guessed_date = dateparser.parse(search.group(1)) guessed_date = dateparser.parse(search.group(1), languages=['fr'], settings={'PREFER_DAY_OF_MONTH': 'last', 'PREFER_MONTH_OF_YEAR': 'last'})
if guessed_date is None: if guessed_date is None:
raise Exception('La date est un objet None') raise Exception('La date est un objet None')
else: else:
...@@ -511,3 +758,15 @@ class RAAspotter: ...@@ -511,3 +758,15 @@ class RAAspotter:
except Exception as exc: except Exception as exc:
logger.warning(f'Impossible de deviner la date du terme {string} : {exc}') logger.warning(f'Impossible de deviner la date du terme {string} : {exc}')
return datetime.datetime(9999, 1, 1) return datetime.datetime(9999, 1, 1)
def get_aware_datetime(unknown_datetime, timezone='Europe/Paris'):
"""
Retourne un objet datetime avisé.
datetime - L'objet datetime à aviser. Utilise le fuseau indiqué si datetime est naïf.
"""
if unknown_datetime.tzinfo is not None and unknown_datetime.tzinfo.utcoffset(unknown_datetime) is not None:
return unknown_datetime
else:
return pytz.timezone(timezone).localize(unknown_datetime)
...@@ -3,30 +3,27 @@ import datetime ...@@ -3,30 +3,27 @@ import datetime
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import unquote from urllib.parse import unquote
from RAAspotter import RAAspotter from Attrap import Attrap
class RAAspotter_ppparis(RAAspotter): class Attrap_ppparis(Attrap):
# Config # Config
__HOST = 'https://www.prefecturedepolice.interieur.gouv.fr' hostname = 'https://www.prefecturedepolice.interieur.gouv.fr'
__RAA_PAGE = f'{__HOST}/actualites-et-presse/arretes/accueil-arretes' raa_page = f'{hostname}/actualites-et-presse/arretes/accueil-arretes'
__WAIT_ELEMENT = 'block-decree-list-block' __WAIT_ELEMENT = 'block-decree-list-block'
__USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
full_name = 'Préfecture de police de Paris' full_name = 'Préfecture de police de Paris'
short_code = 'ppparis' short_code = 'ppparis'
timezone = 'Europe/Paris'
def __init__(self, data_dir): def __init__(self, data_dir):
super().__init__(data_dir, self.__USER_AGENT) super().__init__(data_dir, self.user_agent)
def get_raa(self, keywords): def get_raa(self, keywords):
self.print_output('RAAspotter_ppparis') page_content = self.get_session(self.raa_page, self.__WAIT_ELEMENT, 6)
self.print_output(f'Termes recherchés: {keywords}')
self.print_output('')
page_content = self.get_session(self.__RAA_PAGE, self.__WAIT_ELEMENT, 3)
raa_elements = self.get_raa_elements(page_content) raa_elements = self.get_raa_elements(page_content)
self.parse_raa(raa_elements, keywords.split(',')) self.parse_raa(raa_elements, keywords)
self.mailer() self.mailer()
def get_raa_elements(self, page_content): def get_raa_elements(self, page_content):
...@@ -46,8 +43,7 @@ class RAAspotter_ppparis(RAAspotter): ...@@ -46,8 +43,7 @@ class RAAspotter_ppparis(RAAspotter):
url = unquote(url) url = unquote(url)
name = a.find('span').get_text() name = a.find('span').get_text()
date = datetime.datetime.strptime(a.find('div', class_="field--type-datetime").get_text().strip(), '%d/%m/%Y') date = datetime.datetime.strptime(a.find('div', class_="field--type-datetime").get_text().strip(), '%d/%m/%Y')
filename = url.split('/')[-1]
raa = RAAspotter.RAA(url, date, name, filename) raa = Attrap.RAA(url, date, name, timezone=self.timezone)
elements.append(raa) elements.append(raa)
return elements return elements
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref01(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.ain.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs-RAA'
full_name = 'Préfecture de l\'Ain'
short_code = 'pref01'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '(?:Recueil|Recueils) (?:des actes administratifs)(?:[ -])*([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref02(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.aisne.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-Actes-Administratifs'
full_name = 'Préfecture de l\'Aisne'
short_code = 'pref02'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'RAA [Aa]nnée ([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref03(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.allier.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs-arretes'
full_name = 'Préfecture de l\'Allier'
short_code = 'pref03'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref04(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.alpes-de-haute-provence.gouv.fr'
raa_page = f'{hostname}/Publications/Publications-administratives-et-legales/Recueil-des-Actes-Administratifs'
full_name = 'Préfecture des Alpes-de-Haute-Provence'
short_code = 'pref04'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref05(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.hautes-alpes.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs'
full_name = 'Préfecture des Hautes-Alpes'
short_code = 'pref05'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'Année *([0-9]{4})'
Attrap_prefdpt.grey_card['regex']['month'] = '([A-Za-zéû]* *[0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref06(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.alpes-maritimes.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs-RAA'
full_name = 'Préfecture des Alpes-Maritimes'
short_code = 'pref06'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'Année *([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref09(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.ariege.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs/Recueils-des-Actes-Administratifs-de-l-Ariege-a-partir-du-28-avril-2015'
full_name = 'Préfecture de l\'Ariège'
short_code = 'pref09'
timezone = 'Europe/Paris'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref10(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.aube.gouv.fr'
raa_page = [
f'{hostname}/Publications/RAA-Recueil-des-Actes-Administratifs',
f'{hostname}/Publications/RAA-Recueil-des-Actes-Administratifs/RAA-Archives'
]
full_name = 'Préfecture de l\'Aube'
short_code = 'pref10'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'RAA *([0-9]{4})'
# On ajoute un widget custom représentant les liens sur la page d'accueil
Attrap_prefdpt.widgets.append(
Attrap_prefdpt.DptWidget(
'homepage_links',
regex={'year': 'Année *([0-9]{4})'},
css_path={'title': 'div.fr-text--lead p a.fr-link'}
)
)
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref11(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.aude.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-Actes-Administratifs-RAA'
full_name = 'Préfecture de l\'Aude'
short_code = 'pref11'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'Année *([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref13(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.bouches-du-rhone.gouv.fr'
raa_page = [
f'{hostname}/Publications/RAA-et-Archives',
f'{hostname}/Publications/RAA-et-Archives/Archives-RAA-des-Bouches-du-Rhone'
]
full_name = 'Préfecture des Bouches-du-Rhône'
short_code = 'pref13'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'RAA[- ]*([0-9]{4})'
Attrap_prefdpt.grey_card['follow_link_on_unrecognised_date'] = False
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref25(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.doubs.gouv.fr'
raa_page = f'{hostname}/Publications/Publications-Legales/Recueil-des-Actes-Administratifs-RAA'
full_name = 'Préfecture du Doubs'
short_code = 'pref25'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '([0-9]{4})'
Attrap_prefdpt.grey_card['follow_link_on_unrecognised_date'] = False
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref29(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.finistere.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs'
full_name = 'Préfecture du Finistère'
short_code = 'pref29'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '(?:Recueils publiés en ).*([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref2a(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.corse-du-sud.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-actes-administratifs/Recueil-des-actes-administratifs-de-la-prefecture-de-la-Corse-du-Sud'
full_name = 'Préfecture de la Corse-du-Sud'
short_code = 'pref2a'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.white_card['regex']['year'] = '([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref2b(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.haute-corse.gouv.fr'
raa_page = f'{hostname}/Publications/Publications-administratives-et-legales/Recueils-des-actes-administratifs'
full_name = 'Préfecture de Haute-Corse'
short_code = 'pref2b'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = 'Recueils des actes administratifs ([0-9]{4})'
Attrap_prefdpt.white_card['regex']['month'] = '([A-Za-zéû]* [0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref30(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.gard.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-Actes-Administratifs'
full_name = 'Préfecture du Gard'
short_code = 'pref30'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['year'] = '([0-9]{4})'
from Attrap_prefdpt import Attrap_prefdpt
class Attrap_pref31(Attrap_prefdpt):
# Configuration de la préfecture
hostname = 'https://www.haute-garonne.gouv.fr'
raa_page = f'{hostname}/Publications/Recueil-des-Actes-Administratifs/Recueil-des-Actes-Administratifs-Haute-Garonne'
full_name = 'Préfecture de la Haute-Garonne'
short_code = 'pref31'
timezone = 'Europe/Paris'
# Configuration des widgets à analyser
Attrap_prefdpt.grey_card['regex']['month'] = '([A-Za-zéû]* [0-9]{4})'