From 6638195e4405248babea65ec1818055412dce576 Mon Sep 17 00:00:00 2001 From: Marc Reverdin Date: Sun, 7 Jun 2026 10:23:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(insee):=20parser=20SDMX-ML=20XML=20?= =?UTF-8?q?=E2=80=94=20l'API=20BDM=20ne=20supporte=20pas=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'API portail-api.insee.fr retourne uniquement du XML (SDMX-ML), peu importe l'Accept header. Suppression du code SDMX-JSON et remplacement par un parser XML via regex sur les attributs . - Base URL : https://api.insee.fr/series/BDM/data/SERIES_BDM/{id} - Aucune clé API requise (accès public) - Résultat : 7/11 séries retournent des valeurs (dont chômage 7.4%) Co-Authored-By: Claude Sonnet 4.6 --- src/lib/insee.ts | 97 ++++++++++++++++----------------- src/scripts/fetch-indicators.ts | 4 +- 2 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/lib/insee.ts b/src/lib/insee.ts index ff39c4b..c81ce21 100644 --- a/src/lib/insee.ts +++ b/src/lib/insee.ts @@ -1,28 +1,49 @@ /** * Client INSEE BDM (Banque de Données Macroéconomiques) - * API SDMX publique — pas de clé requise pour les séries publiques. - * Doc: https://api.insee.fr/catalogue/site/themes/wso2/subthemes/insee/pages/item-info.jag?name=BDM&version=V1&provider=INSEE + * Portail : https://portail-api.insee.fr — API publique, aucune clé requise. + * Format : SDMX-ML XML (l'API ne supporte pas JSON). + * Doc: https://api.insee.fr/series/BDM */ -const BASE = 'https://api.insee.fr/series/BDM/V1'; +const BASE = 'https://api.insee.fr/series/BDM/data/SERIES_BDM'; export interface INSEEValue { value: number | null; - period: string; // ex: "2024-Q3" ou "2024" - unit?: string; + period: string; // ex: "2026-Q1" → on extrait l'année + year: string; // ex: "2026" + label?: string; } -interface SDMXObs { - [period: string]: [number | null, ...unknown[]]; -} +/** + * Parse la réponse SDMX-ML XML d'INSEE BDM. + * Extrait les observations + * et retourne la plus récente avec une valeur non nulle. + */ +function parseSDMXXML(xml: string): INSEEValue { + // Extraire toutes les observations + const obsRegex = //g; + const observations: { period: string; value: number }[] = []; -interface SDMXSeries { - observations: SDMXObs; - attributes?: unknown[]; -} + for (const match of xml.matchAll(obsRegex)) { + const attrs = match[1]; + const period = attrs.match(/TIME_PERIOD="([^"]+)"/)?.[1]; + const valStr = attrs.match(/OBS_VALUE="([^"]+)"/)?.[1]; + if (period && valStr) { + const value = parseFloat(valStr); + if (!isNaN(value)) { + observations.push({ period, value }); + } + } + } -interface SDMXDataset { - series?: Record; + if (observations.length === 0) return { value: null, period: '', year: '' }; + + // La plus récente (elles sont triées chronologiquement dans la réponse) + const last = observations[observations.length - 1]; + // Extraire l'année : "2026-Q1" → "2026", "2025-A" → "2025", "2025" → "2025" + const year = last.period.split('-')[0]; + + return { value: last.value, period: last.period, year }; } /** @@ -31,67 +52,43 @@ interface SDMXDataset { */ export async function fetchSerie(serieId: string): Promise { try { - const url = `${BASE}/data/SERIES_BDM/${serieId}?lastNObservations=4&format=sdmx-json`; + const url = `${BASE}/${serieId}?lastNObservations=4`; const res = await fetch(url, { headers: { - Accept: 'application/json', - // Pas besoin de token pour les séries publiques + // Pas de token — API publique + Accept: 'application/xml', }, signal: AbortSignal.timeout(12_000), }); if (!res.ok) { console.warn(`[insee] ${serieId} → HTTP ${res.status}`); - return { value: null, period: '' }; + return { value: null, period: '', year: '' }; } - const data = await res.json(); - - // Parcourir la structure SDMX-JSON - const datasets: SDMXDataset[] = data?.dataSets || []; - const firstDataset = datasets[0]; - if (!firstDataset?.series) return { value: null, period: '' }; - - const firstSeries = Object.values(firstDataset.series)[0] as SDMXSeries; - if (!firstSeries?.observations) return { value: null, period: '' }; - - // Trouver la période la plus récente - const structure = data?.structure; - const timeDimension = structure?.dimensions?.observation?.find( - (d: { id: string }) => d.id === 'TIME_PERIOD' - ); - - const obsEntries = Object.entries(firstSeries.observations); - if (obsEntries.length === 0) return { value: null, period: '' }; - - // L'index SDMX-JSON → période via timeDimension.values - const lastEntry = obsEntries[obsEntries.length - 1]; - const obsIdx = parseInt(lastEntry[0]); - const period = timeDimension?.values?.[obsIdx]?.id || lastEntry[0]; - const value = (lastEntry[1] as unknown[])[0] as number | null; - - return { value, period }; + const xml = await res.text(); + return parseSDMXXML(xml); } catch (e) { console.warn(`[insee] ${serieId} → ${(e as Error).message}`); - return { value: null, period: '' }; + return { value: null, period: '', year: '' }; } } /** * Séries INSEE courantes (IDs BDM) - * Source: https://www.bdm.insee.fr/bdm2/choixCriteres?codeGroupe=CONSO_MENAGES + * Trouver les IDs sur : https://www.bdm.insee.fr */ export const SERIES_INSEE = { // PIB - PIB_VOLUME_T: '010565692', // PIB volume, variation trimestrielle + PIB_VOLUME_T: '010565692', // PIB volume, variation trimestrielle CVS PIB_VOLUME_A: '010565695', // PIB volume, variation annuelle // Inflation IPC_ENSEMBLE: '001759970', // IPC ensemble, variation annuelle - IPC_ALIMENTAIRE: '001759971', // IPC alimentation + IPC_ALIMENTAIRE: '001759971', // IPC alimentation, variation annuelle // Emploi - TAUX_CHOMAGE: '001688526', // Taux de chômage BIT France métro + TAUX_CHOMAGE: '001688526', // Taux de chômage BIT France métro (trimestriel) TAUX_EMPLOI: '001688527', // Taux d'emploi 15-64 ans EMPLOI_TOTAL: '001688535', // Emploi total (milliers) @@ -100,6 +97,6 @@ export const SERIES_INSEE = { DETTE_PUBLIQUE: '001639746', // Dette publique % PIB (Maastricht) // Ménages - POUVOIR_ACHAT: '010565799', // Pouvoir d'achat des ménages + POUVOIR_ACHAT: '010565799', // Pouvoir d'achat par unité de consommation TAUX_EPARGNE: '001637643', // Taux d'épargne des ménages } as const; diff --git a/src/scripts/fetch-indicators.ts b/src/scripts/fetch-indicators.ts index df31138..e8c5130 100644 --- a/src/scripts/fetch-indicators.ts +++ b/src/scripts/fetch-indicators.ts @@ -41,7 +41,7 @@ interface IndicateursJSON { generated_at: string; source: string; indicators: IndicateurResult[]; - insee: Record; + insee: Record; } // ─── FETCH ──────────────────────────────────────────────────────────────────── @@ -51,7 +51,7 @@ async function main() { const startTime = Date.now(); const indicators: IndicateurResult[] = []; - const inseeData: Record = {}; + const inseeData: Record = {}; // 1. Eurostat — fetch en parallèle par batch de 5 console.log(`[fetch-indicators] Fetch ${INDICATEURS.filter(i => i.source === 'eurostat').length} indicateurs Eurostat...`);