modif(main.py): restructuration de la classe

This commit is contained in:
2026-02-06 18:55:37 +01:00
parent 76475d4a2a
commit f02a23f032
2 changed files with 85 additions and 80 deletions

158
main.py
View File

@@ -1,90 +1,94 @@
import requests
from typing import Any, Dict
from bs4 import BeautifulSoup
import json
from sys import stderr
from typing import cast
from requests import Response, Session
from bs4 import BeautifulSoup, Tag
from json import JSONDecodeError, loads
class Scraper:
"""
Scraper est une classe qui permet de gerer
de façon dynamique des requetes uniquement
sur le serveur https de Millesina
"""
def __init__(self, subdir: str = "") -> None:
self._url: str = "https://www.millemisa.fr/"
self._session: Session = Session()
def __init__(self, subdir: str = None):
"""
Initialise la session de scraping et récupère la page d'accueil.
"""
# Très utile pour éviter de renvoyer toujours les mêmes handshake
# TCP et d'avoir toujours une connexion constante avec le server
self._session: requests.Session = requests.Session()
self._url: str = "https://www.millesima.fr/"
self._soup = self.getsoup(subdir)
self._latest_request: tuple[(str, Response | None)] = ("", None)
def _request(
self, subdir: str, use_cache: bool = True
) -> requests.Response | requests.HTTPError:
"""
Effectue une requête GET sur le serveur Millesima.
:param subdir: Le sous-répertoire ou chemin de l'URL (ex: "/vins").
:param use_cache: Si True, retourne la réponse précédente si l'URL est
identique.
:return: requests.Response: L'objet réponse de la requête.
:rtype: requests.HTTPError: Si le serveur renvoie un code d'erreur
(4xx, 5xx).
"""
def _request(self, subdir: str) -> Response:
target_url: str = self._url + subdir.lstrip("/")
response: Response = self._session.get(url=target_url, timeout=10)
response.raise_for_status()
return response
target_url: str = f"{self._url}{subdir.lstrip('/')}" if subdir is \
not None else self._url
# Éviter un max possible de faire des requetes au servers même
# en ayant un tunnel tcp avec le paramètre `use_cache` que si
# activer, va comparer l'url avec l'url précédant
if use_cache and hasattr(self, "_response") \
and self._response is not None:
if self._response.url == target_url:
return self._response
def getresponse(self, subdir: str = "") -> Response:
rq_subdir, rq_response = self._latest_request
self._response: requests.Response = self._session.get(
target_url, timeout=10)
self._response.raise_for_status()
if rq_response is None or subdir != rq_subdir:
request: Response = self._request(subdir)
self._latest_request = (subdir, request)
return request
return self._response
return rq_response
def getsoup(self, subdir: str = None
) -> BeautifulSoup | requests.HTTPError:
"""
Récupère le contenu HTML d'une page et le transforme en objet
BeautifulSoup.
def getsoup(self, subdir: str = "") -> BeautifulSoup:
markup: str = self.getresponse(subdir).text
return BeautifulSoup(markup, features="html.parser")
:param subdir: Le chemin de la page. Si None, retourne la soupe
actuelle.
:return: BeautifulSoup: L'objet parsé pour extraction de données.
:rtype: BeautifulSoup
"""
if not hasattr(self, "_soup") or subdir is not None:
self._request(subdir)
self._soup = BeautifulSoup(self._response.text, "html.parser")
return self._soup
# def getjsondata(self, subdir: str = "", id: str = "__NEXT_DATA__") -> dict[str, object]:
# soup: BeautifulSoup = self.getsoup(subdir)
# # On s'assure que c'est bien un Tag pour avoir accès à .string
# script = soup.find("script", id=id)
def get_json_data(self) -> Dict[str, Any]:
"""
Extrait les données JSON contenues dans la balise __NEXT_DATA__ du
site.
Beaucoup de sites modernes (Next.js) stockent leur état initial dans
une balise <script> pour l'hydratation côté client.
# if isinstance(script, Tag) and script.string:
# try:
# # On commence avec le dictionnaire complet
# current_data: Any = loads(script.string)
:return Dict[str, Any]: Un dictionnaire contenant les props de la page,
ou un dictionnaire vide en cas d'erreur ou
d'absence.
"""
script = self._soup.find("script", id="__NEXT_DATA__")
if script and script.string:
# # Parcours de la structure imbriquée
# keys = ['props', 'pageProps', 'initialReduxState', 'product', 'content']
# for key in keys:
# if isinstance(current_data, dict) and key in current_data:
# current_data = current_data[key]
# else:
# # Si une clé manque, on lève une erreur explicite
# raise ValueError(f"Clé manquante dans le JSON : {key}")
# # On garantit à Pyright que le résultat final est un dictionnaire
# if isinstance(current_data, dict):
# return cast(dict[str, object], current_data)
# except (decoder.JSONDecodeError, ValueError) as e:
# print(f"Erreur lors de l'extraction JSON : {e}", file=stderr)
# return {}
def getjsondata(
self, subdir: str = "", id: str = "__NEXT_DATA__"
) -> dict[str, object]:
soup: BeautifulSoup = self.getsoup(subdir)
script: Tag | None = soup.find("script", id=id)
if isinstance(script, Tag) and script.string:
try:
data: dict[str, Any] = json.loads(script.string)
for element in ['props', 'pageProps', 'initialReduxState',
'product', 'content']:
data = data[element]
return data
except json.decoder.JSONDecodeError:
pass
return {}
current_data: object = loads(script.string)
# tout le chemin à parcourir pour arriver au données
# (plein d'information inutile)
keys: list[str] = [
"props",
"pageProps",
"initialReduxState",
"product",
"content",
]
for key in keys:
# si current_data est bien un dictionnaire et que la clé
# est bien dedans
if isinstance(current_data, dict) and key in current_data:
current_data = current_data[key]
else:
raise ValueError(f"Clé manquante dans le JSON : {key}")
if isinstance(current_data, dict):
return cast(dict[str, object], current_data)
except (JSONDecodeError, ValueError) as e:
print(f"Erreur lors de l'extraction JSON : {e}", file=stderr)
return {}