Files
millesima-ai-engine/search/search_index.json
2026-03-04 11:53:20 +00:00

1 line
32 KiB
JSON

{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Millesima","text":""},{"location":"scraper/","title":"Scraper","text":"<p>Client HTTP optimis\u00e9 pour le scraping de millesima.fr.</p> <p>G\u00e8re la session persistante, les headers de navigation et un cache double pour optimiser les performances et la discr\u00e9tion.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>class Scraper:\n \"\"\"\n Client HTTP optimis\u00e9 pour le scraping de millesima.fr.\n\n G\u00e8re la session persistante, les headers de navigation et un cache double\n pour optimiser les performances et la discr\u00e9tion.\n \"\"\"\n\n def __init__(self) -&gt; None:\n \"\"\"\n Initialise l'infrastructure de navigation:\n\n - cr\u00e9er une session pour \u00e9viter de faire un handshake pour chaque requ\u00eate\n - ajout d'un header pour \u00e9viter le blocage de l'acc\u00e8s au site\n - ajout d'un syst\u00e8me de cache\n \"\"\"\n self._url: str = \"https://www.millesima.fr/\"\n # Tr\u00e8s utile pour \u00e9viter de renvoyer toujours les m\u00eames handshake\n # TCP et d'avoir toujours une connexion constante avec le server\n self._session: Session = Session()\n # Cr\u00e9e une \"fausse carte d'identit\u00e9\" pour \u00e9viter que le site nous\n # bloque car on serait des robots\n self._session.headers.update(\n {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \\\n AppleWebKit/537.36 (KHTML, like Gecko) \\\n Chrome/122.0.0.0 Safari/537.36\",\n \"Accept-Language\": \"fr-FR,fr;q=0.9,en;q=0.8\",\n }\n )\n # Syst\u00e8me de cache pour \u00e9viter de solliciter le serveur inutilement\n # utilise pour _request\n self._latest_request: tuple[(str, Response)] | None = None\n # utilise pour getsoup\n self._latest_soups: OrderedDict[str, BeautifulSoup] = OrderedDict[\n str, BeautifulSoup\n ]()\n\n def _request(self, subdir: str) -&gt; Response:\n \"\"\"\n Effectue une requ\u00eate GET sur le serveur Millesima.\n\n Args:\n subdir (str): Le sous-r\u00e9pertoire ou chemin de l'URL (ex: \"/vins\").\n\n Returns:\n Response: L'objet r\u00e9ponse de la requ\u00eate.\n\n Raises:\n HTTPError: Si le serveur renvoie un code d'erreur (4xx, 5xx).\n \"\"\"\n target_url: str = self._url + subdir.lstrip(\"/\")\n # envoyer une requ\u00eate GET sur la page si erreur, renvoie un raise\n response: Response = self._session.get(url=target_url, timeout=30)\n response.raise_for_status()\n return response\n\n def getresponse(self, subdir: str = \"\", use_cache: bool = True) -&gt; Response:\n \"\"\"\n R\u00e9cup\u00e8re la r\u00e9ponse d'une page, en utilisant le cache si possible.\n\n Args:\n subdir (str, optional): Le chemin de la page.\n use_cache (bool, optional): Utilise la donn\u00e9e deja sauvegarder ou\n \u00e9crase la donn\u00e9e utilis\u00e9 avec la nouvelle\n\n Returns:\n Response: L'objet r\u00e9ponse (cache ou nouvelle requ\u00eate).\n\n Raises:\n HTTPError: Si le serveur renvoie un code d'erreur (4xx, 5xx).\n \"\"\"\n\n # si dans le cache, latest_request existe\n if use_cache and self._latest_request is not None:\n rq_subdir, rq_response = self._latest_request\n\n # si c'est la meme requete et que use_cache est true,\n # on renvoie celle enregistrer\n if subdir == rq_subdir:\n return rq_response\n\n request: Response = self._request(subdir)\n # on recr\u00e9e la structure pour le systeme de cache si activer\n if use_cache:\n self._latest_request = (subdir, request)\n\n return request\n\n def getsoup(self, subdir: str, use_cache: bool = True) -&gt; BeautifulSoup:\n \"\"\"\n R\u00e9cup\u00e8re le contenu HTML d'une page et le transforme en objet BeautifulSoup.\n\n Args:\n subdir (str, optional): Le chemin de la page.\n\n Returns:\n BeautifulSoup: L'objet pars\u00e9 pour extraction de donn\u00e9es.\n\n Raises:\n HTTPError: Si le serveur renvoie un code d'erreur (4xx, 5xx).\n \"\"\"\n\n if use_cache and subdir in self._latest_soups:\n return self._latest_soups[subdir]\n\n markup: str = self.getresponse(subdir).text\n soup: BeautifulSoup = BeautifulSoup(markup, features=\"html.parser\")\n\n if use_cache:\n self._latest_soups[subdir] = soup\n\n if len(self._latest_soups) &gt; 10:\n _ = self._latest_soups.popitem(last=False)\n\n return soup\n\n def getjsondata(self, subdir: str, id: str = \"__NEXT_DATA__\") -&gt; _ScraperData:\n \"\"\"\n Extrait les donn\u00e9es JSON contenues dans la balise __NEXT_DATA__ du site.\n\n Args:\n subdir (str): Le chemin de la page.\n id (str, optional): L'identifiant de la balise script.\n\n Raises:\n HTTPError: Erreur renvoy\u00e9e par le serveur (4xx, 5xx).\n JSONDecodeError: Si le contenu de la balise n'est pas un JSON valide.\n ValueError: Si les cl\u00e9s 'props' ou 'pageProps' sont absentes.\n\n Returns:\n _ScraperData: Instance contenant les donn\u00e9es extraites.\n \"\"\"\n\n soup: BeautifulSoup = self.getsoup(subdir)\n script: Tag | None = soup.find(\"script\", id=id)\n\n if script is None or not script.string:\n raise ValueError(f\"le script id={id} est introuvable\")\n\n current_data: object = cast(object, loads(script.string))\n\n for key in [\"props\", \"pageProps\"]:\n if isinstance(current_data, dict) and key in current_data:\n current_data = cast(object, current_data[key])\n continue\n raise ValueError(f\"Cl\u00e9 manquante dans le JSON : {key}\")\n\n return _ScraperData(cast(dict[str, object], current_data))\n\n def _geturlproductslist(self, subdir: str) -&gt; list[dict[str, Any]] | None:\n \"\"\"\n R\u00e9cup\u00e8re la liste des produits d'une page de cat\u00e9gorie.\n \"\"\"\n try:\n data: dict[str, object] = self.getjsondata(subdir).getdata()\n\n for element in [\"initialReduxState\", \"categ\", \"content\"]:\n data = cast(dict[str, object], data.get(element))\n\n products: list[dict[str, Any]] = cast(\n list[dict[str, Any]], data.get(\"products\")\n )\n\n if isinstance(products, list):\n return products\n\n except (JSONDecodeError, HTTPError):\n return None\n\n def _writevins(self, cache: set[str], product: dict[str, Any], f: Any) -&gt; None:\n \"\"\"_summary_\n\n Args:\n cache (set[str]): _description_\n product (dict): _description_\n f (Any): _description_\n \"\"\"\n if isinstance(product, dict):\n link: Any | None = product.get(\"seoKeyword\")\n if link and link not in cache:\n try:\n infos = self.getjsondata(link).informations()\n _ = f.write(infos + \"\\n\")\n cache.add(link)\n except (JSONDecodeError, HTTPError) as e:\n print(f\"Erreur sur le produit {link}: {e}\")\n\n def getvins(self, subdir: str, filename: str, reset: bool = False) -&gt; None:\n \"\"\"\n Scrape toutes les pages d'une cat\u00e9gorie et sauvegarde en CSV.\n\n Args:\n subdir (str): La cat\u00e9gorie (ex: '/vins-rouges').\n filename (str): Nom du fichier de sortie (ex: 'vins.csv').\n reset (bool): (Optionnel) pour r\u00e9initialiser le processus.\n \"\"\"\n # mode d'\u00e9criture fichier\n mode: Literal[\"w\", \"a+\"] = \"w\" if reset else \"a+\"\n # titre\n title: str = \"Appellation,Robert,Robinson,Suckling,Prix\\n\"\n # page du d\u00e9but\n page: int = 1\n # le set qui sert de cache\n cache: set[str] = set[str]()\n\n custom_format = \"{l_bar} {bar:20} {r_bar}\"\n\n if not reset:\n # appelle la fonction pour load le cache, si il existe\n # pas, il utilise les variables de base sinon il override\n # toute les variables pour continuer et pas recommencer le\n # processus en entier.\n serializable: tuple[int, set[str]] | None = loadstate()\n if isinstance(serializable, tuple):\n # override la page et le cache\n page, cache = serializable\n try:\n with open(filename, mode) as f:\n # check si le titre est bien pr\u00e9sent au d\u00e9but du buffer\n # sinon il l'ecrit, petit bug potentiel, a+ ecrit tout le\n # temps a la fin du buffer, si on a ecrit des choses avant\n # le titre sera apres ces donn\u00e9es mais on part du principe\n # que personne va toucher le fichier.\n _ = f.seek(0, SEEK_SET)\n if not (f.read(len(title)) == title):\n _ = f.write(title)\n else:\n _ = f.seek(0, SEEK_END)\n\n while True:\n products_list: list[dict[str, Any]] | None = (\n self._geturlproductslist(f\"{subdir}?page={page}\")\n )\n if not products_list:\n break\n\n pbar: tqdm[dict[str, Any]] = tqdm(\n products_list, bar_format=custom_format\n )\n for product in pbar:\n keyword = product.get(\"seoKeyword\", \"Inconnu\")[:40]\n pbar.set_description(\n f\"Page: {page:&lt;3} | Product: {keyword:&lt;40}\"\n )\n self._writevins(cache, product, f)\n page += 1\n except (Exception, HTTPError, KeyboardInterrupt, JSONDecodeError):\n if not reset:\n savestate((page, cache))\n</code></pre>"},{"location":"scraper/#scraper.Scraper.__init__","title":"<code>__init__()</code>","text":"<p>Initialise l'infrastructure de navigation:</p> <ul> <li>cr\u00e9er une session pour \u00e9viter de faire un handshake pour chaque requ\u00eate</li> <li>ajout d'un header pour \u00e9viter le blocage de l'acc\u00e8s au site</li> <li>ajout d'un syst\u00e8me de cache</li> </ul> Source code in <code>site-packages/scraper.py</code> <pre><code>def __init__(self) -&gt; None:\n \"\"\"\n Initialise l'infrastructure de navigation:\n\n - cr\u00e9er une session pour \u00e9viter de faire un handshake pour chaque requ\u00eate\n - ajout d'un header pour \u00e9viter le blocage de l'acc\u00e8s au site\n - ajout d'un syst\u00e8me de cache\n \"\"\"\n self._url: str = \"https://www.millesima.fr/\"\n # Tr\u00e8s utile pour \u00e9viter de renvoyer toujours les m\u00eames handshake\n # TCP et d'avoir toujours une connexion constante avec le server\n self._session: Session = Session()\n # Cr\u00e9e une \"fausse carte d'identit\u00e9\" pour \u00e9viter que le site nous\n # bloque car on serait des robots\n self._session.headers.update(\n {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \\\n AppleWebKit/537.36 (KHTML, like Gecko) \\\n Chrome/122.0.0.0 Safari/537.36\",\n \"Accept-Language\": \"fr-FR,fr;q=0.9,en;q=0.8\",\n }\n )\n # Syst\u00e8me de cache pour \u00e9viter de solliciter le serveur inutilement\n # utilise pour _request\n self._latest_request: tuple[(str, Response)] | None = None\n # utilise pour getsoup\n self._latest_soups: OrderedDict[str, BeautifulSoup] = OrderedDict[\n str, BeautifulSoup\n ]()\n</code></pre>"},{"location":"scraper/#scraper.Scraper.getjsondata","title":"<code>getjsondata(subdir, id='__NEXT_DATA__')</code>","text":"<p>Extrait les donn\u00e9es JSON contenues dans la balise NEXT_DATA du site.</p> <p>Parameters:</p> Name Type Description Default <code>subdir</code> <code>str</code> <p>Le chemin de la page.</p> required <code>id</code> <code>str</code> <p>L'identifiant de la balise script.</p> <code>'__NEXT_DATA__'</code> <p>Raises:</p> Type Description <code>HTTPError</code> <p>Erreur renvoy\u00e9e par le serveur (4xx, 5xx).</p> <code>JSONDecodeError</code> <p>Si le contenu de la balise n'est pas un JSON valide.</p> <code>ValueError</code> <p>Si les cl\u00e9s 'props' ou 'pageProps' sont absentes.</p> <p>Returns:</p> Name Type Description <code>_ScraperData</code> <code>_ScraperData</code> <p>Instance contenant les donn\u00e9es extraites.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def getjsondata(self, subdir: str, id: str = \"__NEXT_DATA__\") -&gt; _ScraperData:\n \"\"\"\n Extrait les donn\u00e9es JSON contenues dans la balise __NEXT_DATA__ du site.\n\n Args:\n subdir (str): Le chemin de la page.\n id (str, optional): L'identifiant de la balise script.\n\n Raises:\n HTTPError: Erreur renvoy\u00e9e par le serveur (4xx, 5xx).\n JSONDecodeError: Si le contenu de la balise n'est pas un JSON valide.\n ValueError: Si les cl\u00e9s 'props' ou 'pageProps' sont absentes.\n\n Returns:\n _ScraperData: Instance contenant les donn\u00e9es extraites.\n \"\"\"\n\n soup: BeautifulSoup = self.getsoup(subdir)\n script: Tag | None = soup.find(\"script\", id=id)\n\n if script is None or not script.string:\n raise ValueError(f\"le script id={id} est introuvable\")\n\n current_data: object = cast(object, loads(script.string))\n\n for key in [\"props\", \"pageProps\"]:\n if isinstance(current_data, dict) and key in current_data:\n current_data = cast(object, current_data[key])\n continue\n raise ValueError(f\"Cl\u00e9 manquante dans le JSON : {key}\")\n\n return _ScraperData(cast(dict[str, object], current_data))\n</code></pre>"},{"location":"scraper/#scraper.Scraper.getresponse","title":"<code>getresponse(subdir='', use_cache=True)</code>","text":"<p>R\u00e9cup\u00e8re la r\u00e9ponse d'une page, en utilisant le cache si possible.</p> <p>Parameters:</p> Name Type Description Default <code>subdir</code> <code>str</code> <p>Le chemin de la page.</p> <code>''</code> <code>use_cache</code> <code>bool</code> <p>Utilise la donn\u00e9e deja sauvegarder ou \u00e9crase la donn\u00e9e utilis\u00e9 avec la nouvelle</p> <code>True</code> <p>Returns:</p> Name Type Description <code>Response</code> <code>Response</code> <p>L'objet r\u00e9ponse (cache ou nouvelle requ\u00eate).</p> <p>Raises:</p> Type Description <code>HTTPError</code> <p>Si le serveur renvoie un code d'erreur (4xx, 5xx).</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def getresponse(self, subdir: str = \"\", use_cache: bool = True) -&gt; Response:\n \"\"\"\n R\u00e9cup\u00e8re la r\u00e9ponse d'une page, en utilisant le cache si possible.\n\n Args:\n subdir (str, optional): Le chemin de la page.\n use_cache (bool, optional): Utilise la donn\u00e9e deja sauvegarder ou\n \u00e9crase la donn\u00e9e utilis\u00e9 avec la nouvelle\n\n Returns:\n Response: L'objet r\u00e9ponse (cache ou nouvelle requ\u00eate).\n\n Raises:\n HTTPError: Si le serveur renvoie un code d'erreur (4xx, 5xx).\n \"\"\"\n\n # si dans le cache, latest_request existe\n if use_cache and self._latest_request is not None:\n rq_subdir, rq_response = self._latest_request\n\n # si c'est la meme requete et que use_cache est true,\n # on renvoie celle enregistrer\n if subdir == rq_subdir:\n return rq_response\n\n request: Response = self._request(subdir)\n # on recr\u00e9e la structure pour le systeme de cache si activer\n if use_cache:\n self._latest_request = (subdir, request)\n\n return request\n</code></pre>"},{"location":"scraper/#scraper.Scraper.getsoup","title":"<code>getsoup(subdir, use_cache=True)</code>","text":"<p>R\u00e9cup\u00e8re le contenu HTML d'une page et le transforme en objet BeautifulSoup.</p> <p>Parameters:</p> Name Type Description Default <code>subdir</code> <code>str</code> <p>Le chemin de la page.</p> required <p>Returns:</p> Name Type Description <code>BeautifulSoup</code> <code>BeautifulSoup</code> <p>L'objet pars\u00e9 pour extraction de donn\u00e9es.</p> <p>Raises:</p> Type Description <code>HTTPError</code> <p>Si le serveur renvoie un code d'erreur (4xx, 5xx).</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def getsoup(self, subdir: str, use_cache: bool = True) -&gt; BeautifulSoup:\n \"\"\"\n R\u00e9cup\u00e8re le contenu HTML d'une page et le transforme en objet BeautifulSoup.\n\n Args:\n subdir (str, optional): Le chemin de la page.\n\n Returns:\n BeautifulSoup: L'objet pars\u00e9 pour extraction de donn\u00e9es.\n\n Raises:\n HTTPError: Si le serveur renvoie un code d'erreur (4xx, 5xx).\n \"\"\"\n\n if use_cache and subdir in self._latest_soups:\n return self._latest_soups[subdir]\n\n markup: str = self.getresponse(subdir).text\n soup: BeautifulSoup = BeautifulSoup(markup, features=\"html.parser\")\n\n if use_cache:\n self._latest_soups[subdir] = soup\n\n if len(self._latest_soups) &gt; 10:\n _ = self._latest_soups.popitem(last=False)\n\n return soup\n</code></pre>"},{"location":"scraper/#scraper.Scraper.getvins","title":"<code>getvins(subdir, filename, reset=False)</code>","text":"<p>Scrape toutes les pages d'une cat\u00e9gorie et sauvegarde en CSV.</p> <p>Parameters:</p> Name Type Description Default <code>subdir</code> <code>str</code> <p>La cat\u00e9gorie (ex: '/vins-rouges').</p> required <code>filename</code> <code>str</code> <p>Nom du fichier de sortie (ex: 'vins.csv').</p> required <code>reset</code> <code>bool</code> <p>(Optionnel) pour r\u00e9initialiser le processus.</p> <code>False</code> Source code in <code>site-packages/scraper.py</code> <pre><code>def getvins(self, subdir: str, filename: str, reset: bool = False) -&gt; None:\n \"\"\"\n Scrape toutes les pages d'une cat\u00e9gorie et sauvegarde en CSV.\n\n Args:\n subdir (str): La cat\u00e9gorie (ex: '/vins-rouges').\n filename (str): Nom du fichier de sortie (ex: 'vins.csv').\n reset (bool): (Optionnel) pour r\u00e9initialiser le processus.\n \"\"\"\n # mode d'\u00e9criture fichier\n mode: Literal[\"w\", \"a+\"] = \"w\" if reset else \"a+\"\n # titre\n title: str = \"Appellation,Robert,Robinson,Suckling,Prix\\n\"\n # page du d\u00e9but\n page: int = 1\n # le set qui sert de cache\n cache: set[str] = set[str]()\n\n custom_format = \"{l_bar} {bar:20} {r_bar}\"\n\n if not reset:\n # appelle la fonction pour load le cache, si il existe\n # pas, il utilise les variables de base sinon il override\n # toute les variables pour continuer et pas recommencer le\n # processus en entier.\n serializable: tuple[int, set[str]] | None = loadstate()\n if isinstance(serializable, tuple):\n # override la page et le cache\n page, cache = serializable\n try:\n with open(filename, mode) as f:\n # check si le titre est bien pr\u00e9sent au d\u00e9but du buffer\n # sinon il l'ecrit, petit bug potentiel, a+ ecrit tout le\n # temps a la fin du buffer, si on a ecrit des choses avant\n # le titre sera apres ces donn\u00e9es mais on part du principe\n # que personne va toucher le fichier.\n _ = f.seek(0, SEEK_SET)\n if not (f.read(len(title)) == title):\n _ = f.write(title)\n else:\n _ = f.seek(0, SEEK_END)\n\n while True:\n products_list: list[dict[str, Any]] | None = (\n self._geturlproductslist(f\"{subdir}?page={page}\")\n )\n if not products_list:\n break\n\n pbar: tqdm[dict[str, Any]] = tqdm(\n products_list, bar_format=custom_format\n )\n for product in pbar:\n keyword = product.get(\"seoKeyword\", \"Inconnu\")[:40]\n pbar.set_description(\n f\"Page: {page:&lt;3} | Product: {keyword:&lt;40}\"\n )\n self._writevins(cache, product, f)\n page += 1\n except (Exception, HTTPError, KeyboardInterrupt, JSONDecodeError):\n if not reset:\n savestate((page, cache))\n</code></pre>"},{"location":"scraperdata/","title":"_ScraperData","text":"<p>Conteneur de donn\u00e9es sp\u00e9cialis\u00e9 pour extraire les informations des dictionnaires JSON.</p> <p>Cette classe agit comme une interface simplifi\u00e9e au-dessus du dictionnaire brut renvoy\u00e9 par la balise NEXT_DATA du site Millesima.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>class _ScraperData:\n \"\"\"\n Conteneur de donn\u00e9es sp\u00e9cialis\u00e9 pour extraire les informations des dictionnaires JSON.\n\n Cette classe agit comme une interface simplifi\u00e9e au-dessus du dictionnaire brut\n renvoy\u00e9 par la balise __NEXT_DATA__ du site Millesima.\n \"\"\"\n\n def __init__(self, data: dict[str, object]) -&gt; None:\n \"\"\"\n Initialise le conteneur avec un dictionnaire de donn\u00e9es.\n\n Args:\n data (dict[str, object]): Le dictionnaire JSON brut extrait de la page.\n \"\"\"\n self._data: dict[str, object] = data\n\n def _getcontent(self) -&gt; dict[str, object] | None:\n \"\"\"\n Navigue dans l'arborescence Redux pour atteindre le contenu du produit.\n\n Returns:\n dict[str, object] | None: Le dictionnaire du produit ou None si la structure diff\u00e8re.\n \"\"\"\n current_data: dict[str, object] = self._data\n for key in [\"initialReduxState\", \"product\", \"content\"]:\n new_data: object | None = current_data.get(key)\n if new_data is None:\n return None\n current_data: dict[str, object] = cast(dict[str, object], new_data)\n\n return current_data\n\n def _getattributes(self) -&gt; dict[str, object] | None:\n \"\"\"\n Extrait les attributs techniques (notes, appellations, etc.) du produit.\n\n Returns:\n dict[str, object] | None: Les attributs du vin ou None.\n \"\"\"\n current_data: object = self._getcontent()\n if current_data is None:\n return None\n return cast(dict[str, object], current_data.get(\"attributes\"))\n\n def prix(self) -&gt; float | None:\n \"\"\"\n Calcule le prix unitaire d'une bouteille (standardis\u00e9e \u00e0 75cl).\n\n Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum).\n Cette m\u00e9thode normalise le prix pour obtenir celui d'une seule unit\u00e9.\n\n Returns:\n float | None: Le prix calcul\u00e9 arrondi \u00e0 2 d\u00e9cimales, ou None.\n \"\"\"\n\n content = self._getcontent()\n if content is None:\n return None\n\n items = content.get(\"items\")\n\n # V\u00e9rifie que items existe et n'est pas vide\n if not isinstance(items, list) or len(items) == 0:\n return None\n\n prix_calcule: float | None = None\n\n for item in items:\n if not isinstance(item, dict):\n continue\n\n p = item.get(\"offerPrice\")\n attrs = item.get(\"attributes\", {})\n\n nbunit = attrs.get(\"nbunit\", {}).get(\"value\")\n equivbtl = attrs.get(\"equivbtl\", {}).get(\"value\")\n\n if not isinstance(p, (int, float)) or not nbunit or not equivbtl:\n continue\n\n nb = float(nbunit)\n eq = float(equivbtl)\n\n if nb &lt;= 0 or eq &lt;= 0:\n continue\n\n if nb == 1 and eq == 1:\n return float(p)\n\n prix_calcule = round(float(p) / (nb * eq), 2)\n\n return prix_calcule\n\n def appellation(self) -&gt; str | None:\n \"\"\"\n Extrait le nom de l'appellation du vin.\n\n Returns:\n str | None: Le nom (ex: 'Pauillac') ou None.\n \"\"\"\n attrs: dict[str, object] | None = self._getattributes()\n if attrs is not None:\n app_dict: object | None = attrs.get(\"appellation\")\n if isinstance(app_dict, dict):\n return cast(str, app_dict.get(\"value\"))\n return None\n\n def _getcritiques(self, name: str) -&gt; str | None:\n \"\"\"\n M\u00e9thode g\u00e9n\u00e9rique pour parser les notes des critiques (Parker, Suckling, etc.).\n\n G\u00e8re les notes simples (\"95\") et les plages de notes (\"95-97\") en faisant la moyenne.\n\n Args:\n name (str): La cl\u00e9 de l'attribut dans le JSON (ex: 'note_rp').\n\n Returns:\n str | None: La note format\u00e9e en cha\u00eene de caract\u00e8res ou None.\n \"\"\"\n\n current_value: dict[str, object] | None = self._getattributes()\n if current_value is not None:\n app_dict: dict[str, object] = cast(\n dict[str, object], current_value.get(name)\n )\n if not app_dict:\n return None\n\n val = cast(str, app_dict.get(\"value\")).rstrip(\"+\").split(\"-\")\n if len(val) &gt; 1 and val[1] != \"\":\n val[0] = str(round((float(val[0]) + float(val[1])) / 2, 1))\n\n return val[0]\n return None\n\n def parker(self) -&gt; str | None:\n \"\"\"Note Robert Parker.\"\"\"\n return self._getcritiques(\"note_rp\")\n\n def robinson(self) -&gt; str | None:\n \"\"\"Note Jancis Robinson.\"\"\"\n return self._getcritiques(\"note_jr\")\n\n def suckling(self) -&gt; str | None:\n \"\"\"Note James Suckling.\"\"\"\n return self._getcritiques(\"note_js\")\n\n def getdata(self) -&gt; dict[str, object]:\n \"\"\"Retourne le dictionnaire de donn\u00e9es complet.\"\"\"\n return self._data\n\n def informations(self) -&gt; str:\n \"\"\"\n Agr\u00e8ge les donn\u00e9es cl\u00e9s pour l'export CSV.\n\n Returns:\n str: Ligne format\u00e9e : \"Appellation,Parker,Robinson,Suckling,Prix\".\n \"\"\"\n\n appellation = self.appellation()\n parker = self.parker()\n robinson = self.robinson()\n suckling = self.suckling()\n prix = self.prix()\n\n return f\"{appellation},{parker},{robinson},{suckling},{prix}\"\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.__init__","title":"<code>__init__(data)</code>","text":"<p>Initialise le conteneur avec un dictionnaire de donn\u00e9es.</p> <p>Parameters:</p> Name Type Description Default <code>data</code> <code>dict[str, object]</code> <p>Le dictionnaire JSON brut extrait de la page.</p> required Source code in <code>site-packages/scraper.py</code> <pre><code>def __init__(self, data: dict[str, object]) -&gt; None:\n \"\"\"\n Initialise le conteneur avec un dictionnaire de donn\u00e9es.\n\n Args:\n data (dict[str, object]): Le dictionnaire JSON brut extrait de la page.\n \"\"\"\n self._data: dict[str, object] = data\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.appellation","title":"<code>appellation()</code>","text":"<p>Extrait le nom de l'appellation du vin.</p> <p>Returns:</p> Type Description <code>str | None</code> <p>str | None: Le nom (ex: 'Pauillac') ou None.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def appellation(self) -&gt; str | None:\n \"\"\"\n Extrait le nom de l'appellation du vin.\n\n Returns:\n str | None: Le nom (ex: 'Pauillac') ou None.\n \"\"\"\n attrs: dict[str, object] | None = self._getattributes()\n if attrs is not None:\n app_dict: object | None = attrs.get(\"appellation\")\n if isinstance(app_dict, dict):\n return cast(str, app_dict.get(\"value\"))\n return None\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.getdata","title":"<code>getdata()</code>","text":"<p>Retourne le dictionnaire de donn\u00e9es complet.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def getdata(self) -&gt; dict[str, object]:\n \"\"\"Retourne le dictionnaire de donn\u00e9es complet.\"\"\"\n return self._data\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.informations","title":"<code>informations()</code>","text":"<p>Agr\u00e8ge les donn\u00e9es cl\u00e9s pour l'export CSV.</p> <p>Returns:</p> Name Type Description <code>str</code> <code>str</code> <p>Ligne format\u00e9e : \"Appellation,Parker,Robinson,Suckling,Prix\".</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def informations(self) -&gt; str:\n \"\"\"\n Agr\u00e8ge les donn\u00e9es cl\u00e9s pour l'export CSV.\n\n Returns:\n str: Ligne format\u00e9e : \"Appellation,Parker,Robinson,Suckling,Prix\".\n \"\"\"\n\n appellation = self.appellation()\n parker = self.parker()\n robinson = self.robinson()\n suckling = self.suckling()\n prix = self.prix()\n\n return f\"{appellation},{parker},{robinson},{suckling},{prix}\"\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.parker","title":"<code>parker()</code>","text":"<p>Note Robert Parker.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def parker(self) -&gt; str | None:\n \"\"\"Note Robert Parker.\"\"\"\n return self._getcritiques(\"note_rp\")\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.prix","title":"<code>prix()</code>","text":"<p>Calcule le prix unitaire d'une bouteille (standardis\u00e9e \u00e0 75cl).</p> <p>Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum). Cette m\u00e9thode normalise le prix pour obtenir celui d'une seule unit\u00e9.</p> <p>Returns:</p> Type Description <code>float | None</code> <p>float | None: Le prix calcul\u00e9 arrondi \u00e0 2 d\u00e9cimales, ou None.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def prix(self) -&gt; float | None:\n \"\"\"\n Calcule le prix unitaire d'une bouteille (standardis\u00e9e \u00e0 75cl).\n\n Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum).\n Cette m\u00e9thode normalise le prix pour obtenir celui d'une seule unit\u00e9.\n\n Returns:\n float | None: Le prix calcul\u00e9 arrondi \u00e0 2 d\u00e9cimales, ou None.\n \"\"\"\n\n content = self._getcontent()\n if content is None:\n return None\n\n items = content.get(\"items\")\n\n # V\u00e9rifie que items existe et n'est pas vide\n if not isinstance(items, list) or len(items) == 0:\n return None\n\n prix_calcule: float | None = None\n\n for item in items:\n if not isinstance(item, dict):\n continue\n\n p = item.get(\"offerPrice\")\n attrs = item.get(\"attributes\", {})\n\n nbunit = attrs.get(\"nbunit\", {}).get(\"value\")\n equivbtl = attrs.get(\"equivbtl\", {}).get(\"value\")\n\n if not isinstance(p, (int, float)) or not nbunit or not equivbtl:\n continue\n\n nb = float(nbunit)\n eq = float(equivbtl)\n\n if nb &lt;= 0 or eq &lt;= 0:\n continue\n\n if nb == 1 and eq == 1:\n return float(p)\n\n prix_calcule = round(float(p) / (nb * eq), 2)\n\n return prix_calcule\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.robinson","title":"<code>robinson()</code>","text":"<p>Note Jancis Robinson.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def robinson(self) -&gt; str | None:\n \"\"\"Note Jancis Robinson.\"\"\"\n return self._getcritiques(\"note_jr\")\n</code></pre>"},{"location":"scraperdata/#scraper._ScraperData.suckling","title":"<code>suckling()</code>","text":"<p>Note James Suckling.</p> Source code in <code>site-packages/scraper.py</code> <pre><code>def suckling(self) -&gt; str | None:\n \"\"\"Note James Suckling.\"\"\"\n return self._getcritiques(\"note_js\")\n</code></pre>"}]}