@tidic/nautiljon-scraper
Version:
Nautiljon's anime and manga website scraping tool
207 lines (178 loc) • 7.02 kB
JavaScript
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
let browser = null;
let isRequestInProgress = false;
let requestQueue = [];
// Configuration retry
const RETRY_CONFIG = {
maxRetries: 3,
baseDelay: 1000, // 1 seconde
maxDelay: 10000, // 10 secondes max
backoffFactor: 2
};
// Mutex pour éviter les appels concurrents
async function waitForTurn() {
return new Promise((resolve) => {
if (!isRequestInProgress) {
isRequestInProgress = true;
resolve();
} else {
requestQueue.push(resolve);
}
});
}
function releaseQueue() {
isRequestInProgress = false;
if (requestQueue.length > 0) {
const nextResolve = requestQueue.shift();
isRequestInProgress = true;
nextResolve();
}
}
// Fonction de délai avec backoff exponentiel
function calculateDelay(attempt) {
const delay = Math.min(
RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffFactor, attempt),
RETRY_CONFIG.maxDelay
);
return delay + Math.random() * 1000; // Ajouter jitter
}
async function getBrowser() {
if (!browser) {
console.log('Initialisation du navigateur...');
try {
browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
'--window-size=1920x1080',
'--disable-blink-features=AutomationControlled',
'--disable-features=VizDisplayCompositor'
],
ignoreDefaultArgs: ['--enable-automation'],
defaultViewport: null
});
} catch (error) {
console.error('Erreur lors de l\'initialisation du navigateur:', error.message);
throw new Error('Impossible d\'initialiser Puppeteer');
}
}
return browser;
}
async function req(url, attempt = 0) {
// Attendre son tour dans la queue
await waitForTurn();
let page = null;
try {
console.log(`Chargement de la page (tentative ${attempt + 1}): ${url}`);
const browserInstance = await getBrowser();
page = await browserInstance.newPage();
// Headers pour simuler un navigateur réel
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Cache-Control': 'max-age=0'
});
// Timeout adaptatif selon la tentative
const timeout = 30000 + (attempt * 10000); // +10s par tentative
// Naviguer vers la page
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: timeout
});
// Attendre adaptatif selon la taille de page estimée
const waitTime = url.includes('/animes/') || url.includes('/mangas/') ? 3000 : 1500;
await new Promise(resolve => setTimeout(resolve, waitTime));
// Récupérer le contenu HTML
const html = await page.content();
// Vérifier que la page n'est pas une erreur Cloudflare
if (html.includes('Checking your browser') || html.includes('Please wait')) {
throw new Error('Page Cloudflare détectée');
}
console.log('Récupération réussie');
return cheerio.load(html);
} catch (error) {
console.error(`Erreur lors de la requête vers ${url} (tentative ${attempt + 1}):`, error.message);
// Retry logic
if (attempt < RETRY_CONFIG.maxRetries) {
const delay = calculateDelay(attempt);
console.log(`Nouvelle tentative dans ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
// Libérer la queue pour permettre le retry
releaseQueue();
return req(url, attempt + 1);
}
console.log('Récupération échouée définitivement');
throw new Error(`Échec après ${RETRY_CONFIG.maxRetries + 1} tentatives: ${error.message}`);
} finally {
// Toujours fermer la page
if (page) {
try {
await page.close();
} catch (e) {
console.warn('Erreur lors de la fermeture de page:', e.message);
}
}
// Libérer la queue pour le prochain appel (seulement si pas de retry)
if (attempt === 0 || attempt >= RETRY_CONFIG.maxRetries) {
releaseQueue();
}
}
}
// Auto-cleanup : fermer le navigateur après inactivité
let browserCleanupTimeout = null;
function scheduleBrowserCleanup() {
if (browserCleanupTimeout) {
clearTimeout(browserCleanupTimeout);
}
browserCleanupTimeout = setTimeout(async () => {
if (browser && !isRequestInProgress && requestQueue.length === 0) {
console.log('Fermeture automatique du navigateur (inactivité)...');
await closeBrowser();
}
}, 30000); // 30 secondes d'inactivité
}
async function closeBrowser() {
if (browserCleanupTimeout) {
clearTimeout(browserCleanupTimeout);
browserCleanupTimeout = null;
}
// Attendre que toutes les requêtes en cours se terminent
let waitCount = 0;
while ((isRequestInProgress || requestQueue.length > 0) && waitCount < 100) {
await new Promise(resolve => setTimeout(resolve, 100));
waitCount++;
}
if (browser) {
console.log('Fermeture du navigateur...');
try {
await browser.close();
} catch (error) {
console.warn('Erreur lors de la fermeture du navigateur:', error.message);
}
browser = null;
}
}
// Programmer le nettoyage automatique après chaque requête
const originalReq = req;
req = async function(url, attempt = 0) {
const result = await originalReq(url, attempt);
scheduleBrowserCleanup();
return result;
};
module.exports = {
req,
closeBrowser
};