fix(insee): parser SDMX-ML XML — l'API BDM ne supporte pas JSON
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 <Obs>. - 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 <noreply@anthropic.com>
This commit is contained in:
parent
b687720cce
commit
6638195e44
2 changed files with 49 additions and 52 deletions
|
|
@ -1,28 +1,49 @@
|
||||||
/**
|
/**
|
||||||
* Client INSEE BDM (Banque de Données Macroéconomiques)
|
* Client INSEE BDM (Banque de Données Macroéconomiques)
|
||||||
* API SDMX publique — pas de clé requise pour les séries publiques.
|
* Portail : https://portail-api.insee.fr — API publique, aucune clé requise.
|
||||||
* Doc: https://api.insee.fr/catalogue/site/themes/wso2/subthemes/insee/pages/item-info.jag?name=BDM&version=V1&provider=INSEE
|
* 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 {
|
export interface INSEEValue {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
period: string; // ex: "2024-Q3" ou "2024"
|
period: string; // ex: "2026-Q1" → on extrait l'année
|
||||||
unit?: string;
|
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 <Obs TIME_PERIOD="..." OBS_VALUE="..."/>
|
||||||
|
* et retourne la plus récente avec une valeur non nulle.
|
||||||
|
*/
|
||||||
|
function parseSDMXXML(xml: string): INSEEValue {
|
||||||
|
// Extraire toutes les observations
|
||||||
|
const obsRegex = /<Obs\s+([^/]+)\/>/g;
|
||||||
|
const observations: { period: string; value: number }[] = [];
|
||||||
|
|
||||||
interface SDMXSeries {
|
for (const match of xml.matchAll(obsRegex)) {
|
||||||
observations: SDMXObs;
|
const attrs = match[1];
|
||||||
attributes?: unknown[];
|
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 {
|
if (observations.length === 0) return { value: null, period: '', year: '' };
|
||||||
series?: Record<string, SDMXSeries>;
|
|
||||||
|
// 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<INSEEValue> {
|
export async function fetchSerie(serieId: string): Promise<INSEEValue> {
|
||||||
try {
|
try {
|
||||||
const url = `${BASE}/data/SERIES_BDM/${serieId}?lastNObservations=4&format=sdmx-json`;
|
const url = `${BASE}/${serieId}?lastNObservations=4`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
// Pas de token — API publique
|
||||||
// Pas besoin de token pour les séries publiques
|
Accept: 'application/xml',
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(12_000),
|
signal: AbortSignal.timeout(12_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.warn(`[insee] ${serieId} → HTTP ${res.status}`);
|
console.warn(`[insee] ${serieId} → HTTP ${res.status}`);
|
||||||
return { value: null, period: '' };
|
return { value: null, period: '', year: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const xml = await res.text();
|
||||||
|
return parseSDMXXML(xml);
|
||||||
// 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 };
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[insee] ${serieId} → ${(e as Error).message}`);
|
console.warn(`[insee] ${serieId} → ${(e as Error).message}`);
|
||||||
return { value: null, period: '' };
|
return { value: null, period: '', year: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Séries INSEE courantes (IDs BDM)
|
* 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 = {
|
export const SERIES_INSEE = {
|
||||||
// PIB
|
// 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
|
PIB_VOLUME_A: '010565695', // PIB volume, variation annuelle
|
||||||
|
|
||||||
// Inflation
|
// Inflation
|
||||||
IPC_ENSEMBLE: '001759970', // IPC ensemble, variation annuelle
|
IPC_ENSEMBLE: '001759970', // IPC ensemble, variation annuelle
|
||||||
IPC_ALIMENTAIRE: '001759971', // IPC alimentation
|
IPC_ALIMENTAIRE: '001759971', // IPC alimentation, variation annuelle
|
||||||
|
|
||||||
// Emploi
|
// 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
|
TAUX_EMPLOI: '001688527', // Taux d'emploi 15-64 ans
|
||||||
EMPLOI_TOTAL: '001688535', // Emploi total (milliers)
|
EMPLOI_TOTAL: '001688535', // Emploi total (milliers)
|
||||||
|
|
||||||
|
|
@ -100,6 +97,6 @@ export const SERIES_INSEE = {
|
||||||
DETTE_PUBLIQUE: '001639746', // Dette publique % PIB (Maastricht)
|
DETTE_PUBLIQUE: '001639746', // Dette publique % PIB (Maastricht)
|
||||||
|
|
||||||
// Ménages
|
// 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
|
TAUX_EPARGNE: '001637643', // Taux d'épargne des ménages
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ interface IndicateursJSON {
|
||||||
generated_at: string;
|
generated_at: string;
|
||||||
source: string;
|
source: string;
|
||||||
indicators: IndicateurResult[];
|
indicators: IndicateurResult[];
|
||||||
insee: Record<string, { value: number | null; period: string }>;
|
insee: Record<string, { value: number | null; period: string; year: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── FETCH ────────────────────────────────────────────────────────────────────
|
// ─── FETCH ────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -51,7 +51,7 @@ async function main() {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
const indicators: IndicateurResult[] = [];
|
const indicators: IndicateurResult[] = [];
|
||||||
const inseeData: Record<string, { value: number | null; period: string }> = {};
|
const inseeData: Record<string, { value: number | null; period: string; year: string }> = {};
|
||||||
|
|
||||||
// 1. Eurostat — fetch en parallèle par batch de 5
|
// 1. Eurostat — fetch en parallèle par batch de 5
|
||||||
console.log(`[fetch-indicators] Fetch ${INDICATEURS.filter(i => i.source === 'eurostat').length} indicateurs Eurostat...`);
|
console.log(`[fetch-indicators] Fetch ${INDICATEURS.filter(i => i.source === 'eurostat').length} indicateurs Eurostat...`);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue