Skip to content

_ScraperData

Conteneur de données spécialisé pour extraire les informations des dictionnaires JSON.

Cette classe agit comme une interface simplifiée au-dessus du dictionnaire brut renvoyé par la balise NEXT_DATA du site Millesima.

Source code in site-packages/scraper.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
class _ScraperData:
    """
    Conteneur de données spécialisé pour extraire les informations des dictionnaires JSON.

    Cette classe agit comme une interface simplifiée au-dessus du dictionnaire brut
    renvoyé par la balise __NEXT_DATA__ du site Millesima.
    """

    def __init__(self, data: dict[str, object]) -> None:
        """
        Initialise le conteneur avec un dictionnaire de données.

        Args:
            data (dict[str, object]): Le dictionnaire JSON brut extrait de la page.
        """
        self._data: dict[str, object] = data

    def _getcontent(self) -> dict[str, object] | None:
        """
        Navigue dans l'arborescence Redux pour atteindre le contenu du produit.

        Returns:
            dict[str, object] | None: Le dictionnaire du produit ou None si la structure diffère.
        """
        current_data: dict[str, object] = self._data
        for key in ["initialReduxState", "product", "content"]:
            new_data: object | None = current_data.get(key)
            if new_data is None:
                return None
            current_data: dict[str, object] = cast(dict[str, object], new_data)

        return current_data

    def _getattributes(self) -> dict[str, object] | None:
        """
        Extrait les attributs techniques (notes, appellations, etc.) du produit.

        Returns:
            dict[str, object] | None: Les attributs du vin ou None.
        """
        current_data: object = self._getcontent()
        if current_data is None:
            return None
        return cast(dict[str, object], current_data.get("attributes"))

    def prix(self) -> float | None:
        """
        Calcule le prix unitaire d'une bouteille (standardisée à 75cl).

        Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum).
        Cette méthode normalise le prix pour obtenir celui d'une seule unité.

        Returns:
            float | None: Le prix calculé arrondi à 2 décimales, ou None.
        """

        content = self._getcontent()
        if content is None:
            return None

        items = content.get("items")

        # Vérifie que items existe et n'est pas vide
        if not isinstance(items, list) or len(items) == 0:
            return None

        prix_calcule: float | None = None

        for item in items:
            if not isinstance(item, dict):
                continue

            p = item.get("offerPrice")
            attrs = item.get("attributes", {})

            nbunit = attrs.get("nbunit", {}).get("value")
            equivbtl = attrs.get("equivbtl", {}).get("value")

            if not isinstance(p, (int, float)) or not nbunit or not equivbtl:
                continue

            nb = float(nbunit)
            eq = float(equivbtl)

            if nb <= 0 or eq <= 0:
                continue

            if nb == 1 and eq == 1:
                return float(p)

            prix_calcule = round(float(p) / (nb * eq), 2)

        return prix_calcule

    def appellation(self) -> str | None:
        """
        Extrait le nom de l'appellation du vin.

        Returns:
            str | None: Le nom (ex: 'Pauillac') ou None.
        """
        attrs: dict[str, object] | None = self._getattributes()
        if attrs is not None:
            app_dict: object | None = attrs.get("appellation")
            if isinstance(app_dict, dict):
                return cast(str, app_dict.get("value"))
        return None

    def _getcritiques(self, name: str) -> str | None:
        """
        Méthode générique pour parser les notes des critiques (Parker, Suckling, etc.).

        Gère les notes simples ("95") et les plages de notes ("95-97") en faisant la moyenne.

        Args:
            name (str): La clé de l'attribut dans le JSON (ex: 'note_rp').

        Returns:
            str | None: La note formatée en chaîne de caractères ou None.
        """

        current_value: dict[str, object] | None = self._getattributes()
        if current_value is not None:
            app_dict: dict[str, object] = cast(
                dict[str, object], current_value.get(name)
            )
            if not app_dict:
                return None

            val = cast(str, app_dict.get("value")).rstrip("+").split("-")
            if len(val) > 1 and val[1] != "":
                val[0] = str(round((float(val[0]) + float(val[1])) / 2, 1))

            return val[0]
        return None

    def parker(self) -> str | None:
        """Note Robert Parker."""
        return self._getcritiques("note_rp")

    def robinson(self) -> str | None:
        """Note Jancis Robinson."""
        return self._getcritiques("note_jr")

    def suckling(self) -> str | None:
        """Note James Suckling."""
        return self._getcritiques("note_js")

    def getdata(self) -> dict[str, object]:
        """Retourne le dictionnaire de données complet."""
        return self._data

    def informations(self) -> str:
        """
        Agrège les données clés pour l'export CSV.

        Returns:
            str: Ligne formatée : "Appellation,Parker,Robinson,Suckling,Prix".
        """

        appellation = self.appellation()
        parker = self.parker()
        robinson = self.robinson()
        suckling = self.suckling()
        prix = self.prix()

        return f"{appellation},{parker},{robinson},{suckling},{prix}"

__init__(data)

Initialise le conteneur avec un dictionnaire de données.

Parameters:

Name Type Description Default
data dict[str, object]

Le dictionnaire JSON brut extrait de la page.

required
Source code in site-packages/scraper.py
61
62
63
64
65
66
67
68
def __init__(self, data: dict[str, object]) -> None:
    """
    Initialise le conteneur avec un dictionnaire de données.

    Args:
        data (dict[str, object]): Le dictionnaire JSON brut extrait de la page.
    """
    self._data: dict[str, object] = data

appellation()

Extrait le nom de l'appellation du vin.

Returns:

Type Description
str | None

str | None: Le nom (ex: 'Pauillac') ou None.

Source code in site-packages/scraper.py
147
148
149
150
151
152
153
154
155
156
157
158
159
def appellation(self) -> str | None:
    """
    Extrait le nom de l'appellation du vin.

    Returns:
        str | None: Le nom (ex: 'Pauillac') ou None.
    """
    attrs: dict[str, object] | None = self._getattributes()
    if attrs is not None:
        app_dict: object | None = attrs.get("appellation")
        if isinstance(app_dict, dict):
            return cast(str, app_dict.get("value"))
    return None

getdata()

Retourne le dictionnaire de données complet.

Source code in site-packages/scraper.py
201
202
203
def getdata(self) -> dict[str, object]:
    """Retourne le dictionnaire de données complet."""
    return self._data

informations()

Agrège les données clés pour l'export CSV.

Returns:

Name Type Description
str str

Ligne formatée : "Appellation,Parker,Robinson,Suckling,Prix".

Source code in site-packages/scraper.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def informations(self) -> str:
    """
    Agrège les données clés pour l'export CSV.

    Returns:
        str: Ligne formatée : "Appellation,Parker,Robinson,Suckling,Prix".
    """

    appellation = self.appellation()
    parker = self.parker()
    robinson = self.robinson()
    suckling = self.suckling()
    prix = self.prix()

    return f"{appellation},{parker},{robinson},{suckling},{prix}"

parker()

Note Robert Parker.

Source code in site-packages/scraper.py
189
190
191
def parker(self) -> str | None:
    """Note Robert Parker."""
    return self._getcritiques("note_rp")

prix()

Calcule le prix unitaire d'une bouteille (standardisée à 75cl).

Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum). Cette méthode normalise le prix pour obtenir celui d'une seule unité.

Returns:

Type Description
float | None

float | None: Le prix calculé arrondi à 2 décimales, ou None.

Source code in site-packages/scraper.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def prix(self) -> float | None:
    """
    Calcule le prix unitaire d'une bouteille (standardisée à 75cl).

    Le site vend souvent par caisses (6, 12 bouteilles) ou formats (Magnum).
    Cette méthode normalise le prix pour obtenir celui d'une seule unité.

    Returns:
        float | None: Le prix calculé arrondi à 2 décimales, ou None.
    """

    content = self._getcontent()
    if content is None:
        return None

    items = content.get("items")

    # Vérifie que items existe et n'est pas vide
    if not isinstance(items, list) or len(items) == 0:
        return None

    prix_calcule: float | None = None

    for item in items:
        if not isinstance(item, dict):
            continue

        p = item.get("offerPrice")
        attrs = item.get("attributes", {})

        nbunit = attrs.get("nbunit", {}).get("value")
        equivbtl = attrs.get("equivbtl", {}).get("value")

        if not isinstance(p, (int, float)) or not nbunit or not equivbtl:
            continue

        nb = float(nbunit)
        eq = float(equivbtl)

        if nb <= 0 or eq <= 0:
            continue

        if nb == 1 and eq == 1:
            return float(p)

        prix_calcule = round(float(p) / (nb * eq), 2)

    return prix_calcule

robinson()

Note Jancis Robinson.

Source code in site-packages/scraper.py
193
194
195
def robinson(self) -> str | None:
    """Note Jancis Robinson."""
    return self._getcritiques("note_jr")

suckling()

Note James Suckling.

Source code in site-packages/scraper.py
197
198
199
def suckling(self) -> str | None:
    """Note James Suckling."""
    return self._getcritiques("note_js")