Files
veille-reglementaire/server/articleFetcher.ts

95 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* articleFetcher.ts
*
* Récupère le contenu textuel des 500 premiers mots d'un article web
* pour enrichir la classification IA quand le résumé RSS est trop court.
*
* Stratégie :
* - Fetch HTTP avec timeout 5 secondes
* - Extraction des balises <p>, <h1><h3> du HTML brut (sans dépendance lourde)
* - Limite à 500 mots pour rester dans le contexte LLM
* - Retourne null en cas d'erreur (timeout, 403, paywall, JS-only…) — silencieux
*/
const FETCH_TIMEOUT_MS = 5_000;
const MAX_WORDS = 500;
/**
* Extrait le texte brut des balises de contenu d'un HTML.
* Approche légère sans parser DOM complet.
*/
function extractTextFromHtml(html: string): string {
// Supprimer les balises script, style, nav, header, footer, aside
let cleaned = html
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<nav[\s\S]*?<\/nav>/gi, " ")
.replace(/<header[\s\S]*?<\/header>/gi, " ")
.replace(/<footer[\s\S]*?<\/footer>/gi, " ")
.replace(/<aside[\s\S]*?<\/aside>/gi, " ")
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ");
// Extraire le contenu des balises de texte pertinentes
const contentTags = cleaned.match(/<(p|h[1-3]|li|blockquote)[^>]*>([\s\S]*?)<\/\1>/gi) ?? [];
const texts = contentTags.map((tag) =>
tag
.replace(/<[^>]+>/g, " ") // supprimer les balises HTML restantes
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&nbsp;/g, " ")
.replace(/&quot;/g, '"')
.replace(/&#\d+;/g, " ")
.replace(/\s+/g, " ")
.trim()
);
return texts.filter((t) => t.length > 20).join(" ");
}
/**
* Récupère les premiers ~500 mots du contenu d'un article web.
* Retourne null si l'article est inaccessible ou si le contenu est trop pauvre.
*
* @param url URL de l'article
* @returns Texte extrait (500 mots max) ou null
*/
export async function fetchArticleFirstParagraph(url: string): Promise<string | null> {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
const response = await fetch(url, {
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (compatible; VeilleBot/1.0; +https://itinova.fr)",
Accept: "text/html,application/xhtml+xml",
"Accept-Language": "fr-FR,fr;q=0.9",
},
});
clearTimeout(timer);
if (!response.ok) return null;
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("text/html")) return null;
const html = await response.text();
const text = extractTextFromHtml(html);
if (!text || text.length < 50) return null;
// Limiter à MAX_WORDS mots
const words = text.split(/\s+/);
const truncated = words.slice(0, MAX_WORDS).join(" ");
return truncated.length > 50 ? truncated : null;
} catch {
// Timeout, réseau, CORS, paywall, etc. — silencieux
return null;
}
}