1// Apify SDK - toolkit for building Apify Actors
2import { parse } from 'json2csv';
3import { Actor } from 'apify';
4// Web scraping and browser automation library
5import { PuppeteerCrawler, log } from 'crawlee';
6
7// Configuration
8const CONFIG = {
9 MAX_PAGES_PER_KEYWORD: 10,
10 MAX_URLS_TO_ANALYZE: 1000,
11 DEFAULT_COUNTRY: 'fr',
12 EMAIL_TOOLS: {
13 klaviyo: ['klaviyo.js', 'list-manage.klaviyo.com', 'klaviyo.com/forms'],
14 shopifyEmail: ['email.shopify.com', 'shopify-email'],
15 brevo: ['brevo.com', 'sibautomation.com', 'sendinblue.com'],
16 mailchimp: ['chimpstatic.com', 'list-manage.com', 'mailchimp.com'],
17 activeCampaign: ['activehosted.com', 'activecampaign.com'],
18 omnisend: ['omnisend.com', 'omnisrc.com']
19}
20};
21
22// Fonction utilitaire pour créer des noms de clés sécurisés
23function createSafeKey(baseKey, maxLength = 240) {
24 // Remplacer les espaces par des tirets et ne garder que les caractères autorisés
25 return baseKey
26 .replace(/\s+/g, '-')
27 .replace(/[^a-zA-Z0-9!-_.'()]/g, '')
28 .substring(0, maxLength);
29}
30
31// Fonction pour simuler un comportement humain sur la page
32async function simulateHumanBehavior(page) {
33 await page.evaluate(() => {
34 return new Promise((resolve) => {
35 // Faire défiler la page lentement
36 const scrollHeight = Math.min(document.body.scrollHeight, 2000);
37 const scrollStep = 100;
38 let scrollY = 0;
39
40 const scroll = () => {
41 if (scrollY < scrollHeight) {
42 window.scrollBy(0, scrollStep);
43 scrollY += scrollStep;
44 setTimeout(scroll, 100 + Math.random() * 50);
45 } else {
46 resolve();
47 }
48 };
49
50 scroll();
51 });
52 });
53
54 // Attendre que le défilement soit terminé
55 await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 1000)));
56}
57
58// Fonction pour détecter les outils d'email marketing
59async function detectEmailTools(page) {
60 return await page.evaluate((emailTools) => {
61 const html = document.documentElement.outerHTML.toLowerCase();
62
63 const detectedTools = {};
64
65 for (const tool in emailTools) {
66 const indicators = emailTools[tool];
67 detectedTools[tool] = indicators.some(indicator => html.includes(indicator));
68 }
69
70 return detectedTools;
71 }, CONFIG.EMAIL_TOOLS);
72}
73
74// Cette fonction sera exécutée lorsque vous lancerez le script
75await Actor.main(async () => {
76 try {
77 // Obtenir les paramètres depuis l'entrée avec valeurs par défaut
78 const input = await Actor.getInput() || {};
79const {
80 keywords = ['vêtements'],
81 country = 'fr',
82 maxPages = 1,
83 maxUrlsToAnalyze = 20
84} = input;
85
86CONFIG.MAX_PAGES_PER_KEYWORD = maxPages;
87CONFIG.MAX_URLS_TO_ANALYZE = maxUrlsToAnalyze;
88CONFIG.DEFAULT_COUNTRY = country;
89
90 // Vérifier que les mots-clés sont valides
91 if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
92 throw new Error('Au moins un mot-clé est requis.');
93 }
94
95 log.info(`Nombre de mots-clés à analyser: ${keywords.length}`);
96 log.info(`Configuration: ${CONFIG.MAX_PAGES_PER_KEYWORD} page(s) Google par mot-clé, max ${CONFIG.MAX_URLS_TO_ANALYZE} URLs par mot-clé`);
97
98 // Structure pour stocker tous les résultats par mot-clé
99 const allResults = {};
100
101 // Traiter chaque mot-clé séquentiellement
102 for (const keyword of keywords) {
103 if (!keyword || keyword.trim() === '') {
104 log.warning('Mot-clé vide détecté, ignoré.');
105 continue;
106 }
107
108 log.info(`\n========== ANALYSE DU MOT-CLÉ: ${keyword} ==========`);
109
110 // Liste pour stocker les URLs uniques pour ce mot-clé
111 const uniqueUrls = new Set();
112
113 // ÉTAPE 1: Récupérer les URLs depuis Google pour ce mot-clé
114 const googleCrawler = new PuppeteerCrawler({
115 // Configuration avancée du proxy
116 proxyConfiguration: await Actor.createProxyConfiguration({
117 useApifyProxy: true,
118 apifyProxyGroups: ['RESIDENTIAL'],
119 countryCode: country.toUpperCase(),
120 }),
121
122 // Options de lancement du navigateur
123 launchContext: {
124 launchOptions: {
125 headless: true,
126 stealth: true,
127 args: [
128 '--disable-dev-shm-usage',
129 '--disable-setuid-sandbox',
130 '--no-sandbox',
131 '--disable-web-security',
132 '--disable-features=IsolateOrigins,site-per-process',
133 '--disable-site-isolation-trials',
134 '--lang=fr-FR,fr',
135 ],
136 },
137 },
138
139 navigationTimeoutSecs: 60,
140 maxConcurrency: 1, // Une requête à la fois pour éviter les blocages
141 maxRequestsPerCrawl: CONFIG.MAX_PAGES_PER_KEYWORD,
142
143 async requestHandler({ page, request, enqueueLinks }) {
144 const pageNumber = request.userData.pageNumber || 1;
145
146 try {
147 // Configurer des en-têtes plus réalistes
148 await page.setExtraHTTPHeaders({
149 'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
150 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
151 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
152 'Cache-Control': 'max-age=0',
153 'Connection': 'keep-alive',
154 });
155
156 // Ajouter un délai aléatoire avant la navigation
157 await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)));
158
159 // Navigation vers la page Google
160 await page.goto(request.url, {
161 waitUntil: 'networkidle2',
162 timeout: 30000
163 });
164
165 // Attendre que la page charge
166 await page.waitForSelector('body', { timeout: 20000 });
167
168 // Simuler un comportement humain
169 await simulateHumanBehavior(page);
170
171 // Vérifier s'il y a un CAPTCHA et le signaler
172 const hasCaptcha = await page.evaluate(() => {
173 return document.body.textContent.includes('captcha') ||
174 document.body.textContent.includes('robot') ||
175 document.body.textContent.includes('vérification') ||
176 document.body.textContent.includes('unusual traffic');
177 });
178
179 if (hasCaptcha) {
180 const safeKeyForCaptcha = createSafeKey(`captcha-screenshot-${keyword}-page${pageNumber}`);
181 await Actor.setValue(safeKeyForCaptcha, await page.screenshot({ fullPage: true }),
182 { contentType: 'image/png' });
183 log.error(`CAPTCHA détecté sur la page ${pageNumber} pour "${keyword}". Capture d'écran sauvegardée.`);
184 throw new Error('CAPTCHA détecté');
185 }
186
187 // Prendre une capture d'écran pour débogage avec nom de clé sécurisé
188 const safeKey = createSafeKey(`screenshot-google-${keyword}-page${pageNumber}`);
189 await Actor.setValue(safeKey, await page.screenshot({ fullPage: true }),
190 { contentType: 'image/png' });
191
192 // Extraire les URLs avec des sélecteurs améliorés
193 const allUrls = await page.evaluate(() => {
194 const links = [];
195 const seen = new Set();
196
197 // Différentes versions de sélecteurs Google pour s'adapter à tous les formats
198 const organicResultSelectors = [
199 'div.g a[href^="http"]:not([href*="google"])',
200 '.yuRUbf > a[href^="http"]',
201 'div[data-header-feature] a[href^="http"]',
202 'div[data-sokoban-container] a[href^="http"]',
203 '.rc .yuRUbf a[href^="http"]',
204 '.tF2Cxc a[href^="http"]',
205 'h3.LC20lb a[href^="http"], h3.LC20lb',
206 '.DKV0Md a[href^="http"]'
207 ];
208
209 // Explorer chaque sélecteur pour trouver des liens
210 for (const selector of organicResultSelectors) {
211 try {
212 document.querySelectorAll(selector).forEach(link => {
213 try {
214 // Si l'élément est un parent de lien, chercher le lien dedans
215 let url;
216 if (link.tagName === 'A') {
217 url = link.href;
218 } else {
219 const aElement = link.closest('a') || link.querySelector('a');
220 if (aElement) {
221 url = aElement.href;
222 }
223 }
224
225 if (url && url.startsWith('http')) {
226 const urlObj = new URL(url);
227 const hostname = urlObj.hostname;
228
229 // Filtrage amélioré
230 if (!seen.has(hostname) &&
231 !hostname.includes('google.') &&
232 !hostname.includes('youtube.') &&
233 !hostname.includes('facebook.') &&
234 !hostname.includes('instagram.') &&
235 !hostname.includes('twitter.') &&
236 !hostname.includes('linkedin.')) {
237 links.push(`${urlObj.protocol}//${hostname}`);
238 seen.add(hostname);
239 }
240 }
241 } catch (e) {
242 // Ignorer les erreurs individuelles
243 }
244 });
245 } catch (e) {
246 // Ignorer les erreurs de sélecteur
247 }
248 }
249
250 // Si on n'a trouvé aucun lien avec les sélecteurs spécifiques,
251 // essayer une approche plus générale
252 if (links.length === 0) {
253 document.querySelectorAll('a[href^="http"]').forEach(link => {
254 try {
255 const url = link.href;
256 const urlObj = new URL(url);
257 const hostname = urlObj.hostname;
258
259 // Exclure les domaines connus
260 if (!seen.has(hostname) &&
261 !hostname.includes('google.') &&
262 !hostname.includes('youtube.') &&
263 !hostname.includes('facebook.') &&
264 !hostname.includes('instagram.')) {
265 links.push(`${urlObj.protocol}//${hostname}`);
266 seen.add(hostname);
267 }
268 } catch (e) {
269 // Ignorer les erreurs
270 }
271 });
272 }
273
274 return links;
275 });
276
277 log.info(`Trouvé ${allUrls.length} domaines sur la page ${pageNumber} pour "${keyword}"`);
278
279 // Vérifier si des URLs ont été trouvées
280 if (allUrls.length === 0) {
281 log.warning(`Aucune URL trouvée sur la page ${pageNumber} pour "${keyword}". Capture d'écran sauvegardée.`);
282 }
283
284 // Ajouter à notre set global pour ce mot-clé
285 allUrls.forEach(url => uniqueUrls.add(url));
286
287 // Attendre un peu avant de chercher la page suivante
288 await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 2000)));
289
290 // Naviguer vers la page suivante si nécessaire
291 if (pageNumber < CONFIG.MAX_PAGES_PER_KEYWORD) {
292 const nextPageExists = await page.evaluate(() => {
293 return !!document.querySelector('#pnnext');
294 });
295
296 if (nextPageExists) {
297 await enqueueLinks({
298 selector: '#pnnext',
299 userData: { pageNumber: pageNumber + 1 }
300 });
301
302 log.info(`Page suivante (${pageNumber + 1}) trouvée pour "${keyword}"`);
303 } else {
304 log.info(`Pas de page suivante trouvée pour "${keyword}" après la page ${pageNumber}`);
305 }
306 }
307
308 } catch (error) {
309 log.error(`Erreur page ${pageNumber}: ${error.message}`);
310 }
311 },
312 retryOnBlocked: true,
313 maxRequestRetries: 3,
314 requestHandlerTimeoutSecs: 180,
315 });
316
317 // Lancer la première étape pour ce mot-clé avec paramètres spécifiques
318 await googleCrawler.addRequests([{
319 url: `https://www.google.${country}/search?q=${encodeURIComponent(keyword)}&hl=fr&gl=${country}&num=100`,
320 userData: { pageNumber: 1 }
321 }]);
322
323 await googleCrawler.run();
324
325 // Vérifier si des URLs ont été trouvées
326 if (uniqueUrls.size === 0) {
327 log.info(`Aucune URL trouvée pour le mot-clé "${keyword}". Passage au mot-clé suivant.`);
328
329 // Ajouter un résultat vide pour ce mot-clé
330 allResults[keyword] = {
331 message: "Aucune URL trouvée pour cette recherche.",
332 timestamp: new Date().toISOString()
333 };
334
335 continue; // Passer au mot-clé suivant
336 }
337
338 log.info(`Étape 1 pour "${keyword}": ${uniqueUrls.size} URLs trouvées.`);
339
340 // ÉTAPE 2: Analyser les URLs pour ce mot-clé
341 const urlsToAnalyze = [...uniqueUrls].slice(0, CONFIG.MAX_URLS_TO_ANALYZE);
342 log.info(`Analyse limitée aux ${urlsToAnalyze.length} premières URLs pour "${keyword}".`);
343
344 // Attendre avant de commencer l'analyse des sites
345 await new Promise(resolve => setTimeout(resolve, 5000));
346
347 // Résultats pour ce mot-clé avec les nouvelles plateformes d'email marketing
348
349 const results = {
350 shopify: [],
351 wordpress: [],
352 autres: [],
353 // Klaviyo
354 shopifyWithKlaviyo: [],
355 wordpressWithKlaviyo: [],
356 // Shopify Email
357 shopifyWithShopifyEmail: [],
358 wordpressWithShopifyEmail: [],
359 // Brevo
360 shopifyWithBrevo: [],
361 wordpressWithBrevo: [],
362 // Mailchimp
363 shopifyWithMailchimp: [],
364 wordpressWithMailchimp: [],
365 // ActiveCampaign
366 shopifyWithActiveCampaign: [],
367 wordpressWithActiveCampaign: [],
368 // Omnisend
369 shopifyWithOmnisend: [],
370 wordpressWithOmnisend: []
371 };
372
373 // Utiliser un PuppeteerCrawler pour l'analyse des CMS
374 const cmsCrawler = new PuppeteerCrawler({
375 proxyConfiguration: await Actor.createProxyConfiguration(),
376 launchContext: {
377 launchOptions: {
378 headless: true,
379 stealth: true,
380 args: [
381 '--disable-dev-shm-usage',
382 '--disable-setuid-sandbox',
383 '--no-sandbox',
384 '--disable-web-security',
385 '--disable-features=IsolateOrigins,site-per-process',
386 '--disable-site-isolation-trials',
387 '--lang=fr-FR,fr',
388 ],
389 },
390 },
391 navigationTimeoutSecs: 45,
392 maxConcurrency: 3,
393
394 async requestHandler({ page, request }) {
395 const url = request.url;
396 log.info(`Analyse du CMS pour: ${url}`);
397
398 try {
399 // Configurer la page
400 await page.setJavaScriptEnabled(true);
401
402 // Bloquer les ressources inutiles
403 await page.setRequestInterception(true);
404 page.on('request', (req) => {
405 const resourceType = req.resourceType();
406 if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
407 req.abort();
408 } else {
409 req.continue();
410 }
411 });
412
413 // Visiter la page
414 await page.goto(url, {
415 waitUntil: 'domcontentloaded',
416 timeout: 15000
417 });
418
419 // Attendre un peu pour que JavaScript s'exécute
420 await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 2000)));
421
422 // Détecter les technologies
423 const siteInfo = await page.evaluate(() => {
424 const html = document.documentElement.outerHTML.toLowerCase();
425
426 // Indices Shopify
427 const shopifyIndicators = [
428 'shopify',
429 'myshopify.com',
430 'cdn.shopify.com',
431 'shopify.theme',
432 'shopify-payment-button'
433 ];
434
435 // Indices WordPress
436 const wordpressIndicators = [
437 'wp-content',
438 'wp-includes',
439 'wp-json',
440 'wordpress',
441 'wp-block'
442 ];
443
444 // Vérifier chaque technologie
445 const isShopify = shopifyIndicators.some(indicator => html.includes(indicator));
446 const isWordPress = wordpressIndicators.some(indicator => html.includes(indicator));
447
448 return { isShopify, isWordPress };
449 });
450
451 // Détecter les outils d'email marketing
452 const emailInfo = await detectEmailTools(page);
453
454 // Catégoriser le site
455 if (siteInfo.isShopify) {
456 results.shopify.push(url);
457
458 // Vérifier tous les outils email marketing
459 for (const tool in emailInfo) {
460 if (emailInfo[tool]) {
461 results[`shopifyWith${tool.charAt(0).toUpperCase() + tool.slice(1)}`].push(url);
462 }
463 }
464
465 // Log des résultats
466 const emailTools = Object.keys(emailInfo).filter(tool => emailInfo[tool]);
467 if (emailTools.length > 0) {
468 log.info(`✅ ${url}: Shopify avec ${emailTools.join(', ')}`);
469 } else {
470 log.info(`✅ ${url}: Shopify sans outil d'email marketing détecté`);
471 }
472 } else if (siteInfo.isWordPress) {
473 results.wordpress.push(url);
474
475 // Même chose pour WordPress - vérifier tous les outils
476 for (const tool in emailInfo) {
477 if (emailInfo[tool]) {
478 results[`wordpressWith${tool.charAt(0).toUpperCase() + tool.slice(1)}`].push(url);
479 }
480 }
481
482 // Log des résultats
483 const emailTools = Object.keys(emailInfo).filter(tool => emailInfo[tool]);
484 if (emailTools.length > 0) {
485 log.info(`✅ ${url}: WordPress avec ${emailTools.join(', ')}`);
486 } else {
487 log.info(`✅ ${url}: WordPress sans outil d'email marketing détecté`);
488 }
489 } else {
490 results.autres.push(url);
491 log.info(`✅ ${url}: Autre plateforme`);
492 }
493
494 } catch (error) {
495 log.error(`❌ Erreur pour ${url}: ${error.message}`);
496 results.autres.push(url);
497 }
498 },
499 retryOnBlocked: false,
500 maxRequestRetries: 1,
501 });
502
503 // Ajouter toutes les URLs à analyser
504 for (const url of urlsToAnalyze) {
505 await cmsCrawler.addRequests([{ url }]);
506 }
507
508 await cmsCrawler.run();
509 log.info(`Analyse terminée pour "${keyword}". ${urlsToAnalyze.length} sites analysés.`);
510
511 // Calculer les pourcentages (éviter la division par zéro)
512 const shopifyKlaviyoPercentage = results.shopify.length > 0 ?
513 (results.shopifyWithKlaviyo.length / results.shopify.length * 100).toFixed(2) : 0;
514
515 const wordpressKlaviyoPercentage = results.wordpress.length > 0 ?
516 (results.wordpressWithKlaviyo.length / results.wordpress.length * 100).toFixed(2) : 0;
517
518 const shopifyEmailPercentage = results.shopify.length > 0 ?
519 (results.shopifyWithShopifyEmail.length / results.shopify.length * 100).toFixed(2) : 0;
520
521 const shopifyBrevoPercentage = results.shopify.length > 0 ?
522 (results.shopifyWithBrevo.length / results.shopify.length * 100).toFixed(2) : 0;
523
524 const shopifyMailchimpPercentage = results.shopify.length > 0 ?
525 (results.shopifyWithMailchimp.length / results.shopify.length * 100).toFixed(2) : 0;
526
527 const shopifyActiveCampaignPercentage = results.shopify.length > 0 ?
528 (results.shopifyWithActiveCampaign.length / results.shopify.length * 100).toFixed(2) : 0;
529
530 const shopifyOmnisendPercentage = results.shopify.length > 0 ?
531 (results.shopifyWithOmnisend.length / results.shopify.length * 100).toFixed(2) : 0;
532
533 const wordpressBrevoPercentage = results.wordpress.length > 0 ?
534 (results.wordpressWithBrevo.length / results.wordpress.length * 100).toFixed(2) : 0;
535
536 const wordpressMailchimpPercentage = results.wordpress.length > 0 ?
537 (results.wordpressWithMailchimp.length / results.wordpress.length * 100).toFixed(2) : 0;
538
539 const wordpressActiveCampaignPercentage = results.wordpress.length > 0 ?
540 (results.wordpressWithActiveCampaign.length / results.wordpress.length * 100).toFixed(2) : 0;
541
542 const wordpressOmnisendPercentage = results.wordpress.length > 0 ?
543 (results.wordpressWithOmnisend.length / results.wordpress.length * 100).toFixed(2) : 0;
544
545 // Stocker les résultats pour ce mot-clé
546 allResults[keyword] = {
547 timestamp: new Date().toISOString(),
548 resultats: {
549 shopify: {
550 count: results.shopify.length,
551 urls: results.shopify,
552 emailMarketing: {
553 klaviyo: {
554 count: results.shopifyWithKlaviyo.length,
555 urls: results.shopifyWithKlaviyo,
556 percentage: shopifyKlaviyoPercentage + '%'
557 },
558 shopifyEmail: {
559 count: results.shopifyWithShopifyEmail.length,
560 urls: results.shopifyWithShopifyEmail,
561 percentage: shopifyEmailPercentage + '%'
562 },
563 brevo: {
564 count: results.shopifyWithBrevo.length,
565 urls: results.shopifyWithBrevo,
566 percentage: shopifyBrevoPercentage + '%'
567 },
568 mailchimp: {
569 count: results.shopifyWithMailchimp.length,
570 urls: results.shopifyWithMailchimp,
571 percentage: shopifyMailchimpPercentage + '%'
572 },
573 activeCampaign: {
574 count: results.shopifyWithActiveCampaign.length,
575 urls: results.shopifyWithActiveCampaign,
576 percentage: shopifyActiveCampaignPercentage + '%'
577 },
578 omnisend: {
579 count: results.shopifyWithOmnisend.length,
580 urls: results.shopifyWithOmnisend,
581 percentage: shopifyOmnisendPercentage + '%'
582 }
583 }
584 },
585 wordpress: {
586 count: results.wordpress.length,
587 urls: results.wordpress,
588 emailMarketing: {
589 klaviyo: {
590 count: results.wordpressWithKlaviyo.length,
591 urls: results.wordpressWithKlaviyo,
592 percentage: wordpressKlaviyoPercentage + '%'
593 },
594 brevo: {
595 count: results.wordpressWithBrevo.length,
596 urls: results.wordpressWithBrevo,
597 percentage: wordpressBrevoPercentage + '%'
598 },
599 mailchimp: {
600 count: results.wordpressWithMailchimp.length,
601 urls: results.wordpressWithMailchimp,
602 percentage: wordpressMailchimpPercentage + '%'
603 },
604 activeCampaign: {
605 count: results.wordpressWithActiveCampaign.length,
606 urls: results.wordpressWithActiveCampaign,
607 percentage: wordpressActiveCampaignPercentage + '%'
608 },
609 omnisend: {
610 count: results.wordpressWithOmnisend.length,
611 urls: results.wordpressWithOmnisend,
612 percentage: wordpressOmnisendPercentage + '%'
613 }
614 }
615 },
616 autres: {
617 count: results.autres.length,
618 urls: results.autres
619 }
620 },
621 statistiques: {
622 total: urlsToAnalyze.length,
623 analysés: results.shopify.length + results.wordpress.length + results.autres.length
624 }
625 };
626 }
627
628 // Calculer des statistiques globales pour tous les mots-clés
629 const globalStats = {
630 totalKeywords: keywords.length,
631 totalSitesAnalyzed: 0,
632 shopifySites: 0,
633 wordpressSites: 0,
634 autresSites: 0,
635 emailMarketing: {
636 shopify: {},
637 wordpress: {}
638 }
639 };
640
641 // Initialiser les compteurs pour chaque outil d'email marketing
642 for (const tool in CONFIG.EMAIL_TOOLS) {
643 globalStats.emailMarketing.shopify[tool] = 0;
644 globalStats.emailMarketing.wordpress[tool] = 0;
645 }
646
647 // Compiler les statistiques globales
648 for (const keyword in allResults) {
649 if (allResults[keyword].resultats) {
650 const r = allResults[keyword].resultats;
651 globalStats.totalSitesAnalyzed += r.shopify.count + r.wordpress.count + r.autres.count;
652 globalStats.shopifySites += r.shopify.count;
653 globalStats.wordpressSites += r.wordpress.count;
654 globalStats.autresSites += r.autres.count;
655
656 // Totaux des outils d'email marketing
657 for (const tool in CONFIG.EMAIL_TOOLS) {
658 if (r.shopify.emailMarketing[tool]) {
659 globalStats.emailMarketing.shopify[tool] += r.shopify.emailMarketing[tool].count;
660 }
661 if (r.wordpress.emailMarketing[tool]) {
662 globalStats.emailMarketing.wordpress[tool] += r.wordpress.emailMarketing[tool].count;
663 }
664 }
665 }
666 }
667
668 // Calculer les pourcentages globaux
669 globalStats.shopifyPercentage = globalStats.totalSitesAnalyzed > 0 ?
670 (globalStats.shopifySites / globalStats.totalSitesAnalyzed * 100).toFixed(2) + '%' : '0%';
671
672 globalStats.wordpressPercentage = globalStats.totalSitesAnalyzed > 0 ?
673 (globalStats.wordpressSites / globalStats.totalSitesAnalyzed * 100).toFixed(2) + '%' : '0%';
674
675 // Pourcentages des outils email pour Shopify et WordPress
676 for (const platform of ['shopify', 'wordpress']) {
677 const emailStats = globalStats.emailMarketing[platform];
678 for (const tool in emailStats) {
679 const totalSites = platform === 'shopify' ? globalStats.shopifySites : globalStats.wordpressSites;
680 emailStats[`${tool}Percentage`] = totalSites > 0 ?
681 (emailStats[tool] / totalSites * 100).toFixed(2) + '%' : '0%';
682 }
683 }
684
685 // Préparer un format plus propre et structuré pour les résultats
686 const cleanResults = {
687 date: new Date().toISOString(),
688 sommaire: {
689 totalMotsClés: globalStats.totalKeywords,
690 totalSitesAnalysés: globalStats.totalSitesAnalyzed,
691 répartitionPlateforme: {
692 shopify: globalStats.shopifySites + " sites (" + globalStats.shopifyPercentage + ")",
693 wordpress: globalStats.wordpressSites + " sites (" + globalStats.wordpressPercentage + ")",
694 autres: globalStats.autresSites + " sites"
695 },
696 outilsEmailMarketing: {
697 shopify: {},
698 wordpress: {}
699 }
700 },
701 détails: []
702 };
703
704 // Remplir les statistiques d'email marketing dans le résumé
705 for (const platform of ['shopify', 'wordpress']) {
706 const emailStats = globalStats.emailMarketing[platform];
707 for (const tool in emailStats) {
708 if (tool.endsWith('Percentage')) continue;
709 const count = emailStats[tool];
710 const percentage = emailStats[`${tool}Percentage`];
711 const totalSites = platform === 'shopify' ? globalStats.shopifySites : globalStats.wordpressSites;
712 cleanResults.sommaire.outilsEmailMarketing[platform][tool] =
713 `${count} sur ${totalSites} (${percentage})`;
714 }
715 }
716
717 // Créer une liste structurée par mot-clé
718 for (const keyword in allResults) {
719 if (!allResults[keyword].resultats) continue;
720
721 const r = allResults[keyword].resultats;
722
723 // Extraire les URLs pour chaque outil email marketing
724 const shopifyEmails = {};
725 const wordpressEmails = {};
726 for (const tool in CONFIG.EMAIL_TOOLS) {
727 shopifyEmails[tool] = r.shopify.emailMarketing[tool]?.urls || [];
728 wordpressEmails[tool] = r.wordpress.emailMarketing[tool]?.urls || [];
729 }
730
731 // Trouver les sites sans outil d'email marketing
732 const usedEmailTools = new Set(Object.values(shopifyEmails).flat());
733 const shopifySansEmail = r.shopify.urls.filter(url => !usedEmailTools.has(url));
734
735 const usedWpEmailTools = new Set(Object.values(wordpressEmails).flat());
736 const wordpressSansEmail = r.wordpress.urls.filter(url => !usedWpEmailTools.has(url));
737
738 // Structure détaillée
739 const detail = {
740 motClé: keyword,
741 totalSites: r.shopify.count + r.wordpress.count + r.autres.count,
742 résultats: {
743 shopify: {
744 ...shopifyEmails,
745 sansEmailMarketing: shopifySansEmail
746 },
747 wordpress: {
748 ...wordpressEmails,
749 sansEmailMarketing: wordpressSansEmail
750 },
751 autresSites: r.autres.urls
752 }
753 };
754
755 cleanResults.détails.push(detail);
756 }
757
758 // Créer des listes consolidées tous mots-clés confondus
759 const consolidatedLists = {
760 shopify: {},
761 wordpress: {},
762 autresSites: []
763 };
764
765 for (const tool in CONFIG.EMAIL_TOOLS) {
766 consolidatedLists.shopify[`avec${tool.charAt(0).toUpperCase() + tool.slice(1)}`] = [];
767 consolidatedLists.wordpress[`avec${tool.charAt(0).toUpperCase() + tool.slice(1)}`] = [];
768 }
769 consolidatedLists.shopify.sansEmailMarketing = [];
770 consolidatedLists.wordpress.sansEmailMarketing = [];
771
772 // Remplir les listes consolidées
773 for (const detail of cleanResults.détails) {
774 for (const tool in CONFIG.EMAIL_TOOLS) {
775 consolidatedLists.shopify[`avec${tool.charAt(0).toUpperCase() + tool.slice(1)}`].push(...detail.résultats.shopify[tool]);
776 consolidatedLists.wordpress[`avec${tool.charAt(0).toUpperCase() + tool.slice(1)}`].push(...detail.résultats.wordpress[tool]);
777 }
778 consolidatedLists.shopify.sansEmailMarketing.push(...detail.résultats.shopify.sansEmailMarketing);
779 consolidatedLists.wordpress.sansEmailMarketing.push(...detail.résultats.wordpress.sansEmailMarketing);
780 consolidatedLists.autresSites.push(...detail.résultats.autresSites);
781 }
782
783 // Éliminer les doublons dans toutes les listes consolidées
784 for (const platform in consolidatedLists) {
785 for (const category in consolidatedLists[platform]) {
786 consolidatedLists[platform][category] = [...new Set(consolidatedLists[platform][category])];
787 }
788 }
789
790 cleanResults.listesSites = consolidatedLists;
791
792 // Sauvegarder les résultats finaux dans ce format plus propre
793 await Actor.pushData(cleanResults);
794
795// Étape A : Préparer un tableau plat pour le CSV
796const csvData = [];
797
798for (const detail of cleanResults.détails) {
799 const motCle = detail.motClé;
800
801 // Ajouter les sites Shopify avec leurs outils email marketing
802 for (const [outil, urls] of Object.entries(detail.résultats.shopify)) {
803 urls.forEach(url => {
804 csvData.push({
805 motCle,
806 plateforme: 'Shopify',
807 outilEmailMarketing: outil,
808 url
809 });
810 });
811 }
812
813 // Ajouter les sites WordPress avec leurs outils email marketing
814 for (const [outil, urls] of Object.entries(detail.résultats.wordpress)) {
815 urls.forEach(url => {
816 csvData.push({
817 motCle,
818 plateforme: 'WordPress',
819 outilEmailMarketing: outil,
820 url
821 });
822 });
823 }
824
825 // Ajouter les autres sites
826 detail.résultats.autresSites.forEach(url => {
827 csvData.push({
828 motCle,
829 plateforme: 'Autre',
830 outilEmailMarketing: 'Aucun',
831 url
832 });
833 });
834}
835
836// Étape B : Transformer ce tableau en CSV
837const csv = parse(csvData, { fields: ['motCle', 'plateforme', 'outilEmailMarketing', 'url'] });
838
839// Étape C : Sauvegarder le CSV sur Apify Storage
840await Actor.setValue('results.csv', csv, { contentType: 'text/csv' });
841
842
843 log.info(`\n========== ANALYSE TERMINÉE ==========`);
844 log.info(`Mots-clés analysés: ${globalStats.totalKeywords}`);
845 log.info(`Total des sites analysés: ${globalStats.totalSitesAnalyzed}`);
846 log.info(`Sites Shopify: ${globalStats.shopifySites} (${globalStats.shopifyPercentage})`);
847 log.info(`Sites WordPress: ${globalStats.wordpressSites} (${globalStats.wordpressPercentage})`);
848 log.info(`Autres sites: ${globalStats.autresSites}`);
849 log.info(`\n----- Utilisation des outils d'email marketing -----`);
850 log.info(`Sur Shopify:`);
851 for (const tool in CONFIG.EMAIL_TOOLS) {
852 const percentage = globalStats.emailMarketing.shopify[`${tool}Percentage`];
853 log.info(` - ${tool}: ${percentage}`);
854 }
855 log.info(`Sur WordPress:`);
856 for (const tool in CONFIG.EMAIL_TOOLS) {
857 const percentage = globalStats.emailMarketing.wordpress[`${tool}Percentage`];
858 log.info(` - ${tool}: ${percentage}`);
859 }
860
861 } catch (error) {
862 log.error(`Erreur principale: ${error.message}`);
863 // Enregistrer l'erreur dans les résultats
864 await Actor.pushData({
865 error: error.message,
866 timestamp: new Date().toISOString()
867 });
868 throw error;
869 }
870});