Recherche CMS avatar
Recherche CMS

Pricing

Pay per usage

Go to Store
Recherche CMS

Recherche CMS

Developed by

TML

Maintained by Community

0.0 (0)

Pricing

Pay per usage

1

Monthly users

4

Runs succeeded

>99%

Last modified

2 days ago

.actor/Dockerfile

1# Specify the base Docker image. You can read more about
2# the available images at https://crawlee.dev/docs/guides/docker-images
3# You can also use any other image from Docker Hub.
4FROM apify/actor-node-puppeteer-chrome:20
5
6# Check preinstalled packages
7RUN npm ls crawlee apify puppeteer playwright
8
9# Copy just package.json and package-lock.json
10# to speed up the build using Docker layer cache.
11COPY --chown=myuser package*.json ./
12
13# Install NPM packages, skip optional and development dependencies to
14# keep the image small. Avoid logging too much and print the dependency
15# tree for debugging
16RUN npm --quiet set progress=false \
17    && npm install --omit=dev --omit=optional \
18    && echo "Installed NPM packages:" \
19    && (npm list --omit=dev --all || true) \
20    && echo "Node.js version:" \
21    && node --version \
22    && echo "NPM version:" \
23    && npm --version \
24    && rm -r ~/.npm
25
26# Next, copy the remaining files and directories with the source code.
27# Since we do this after NPM install, quick build will be really fast
28# for most source file changes.
29COPY --chown=myuser . ./
30
31# Run the image. If you know you won't need headful browsers,
32# you can remove the XVFB start script for a micro perf gain.
33CMD ./start_xvfb_and_run_cmd.sh && npm start --silent

.actor/actor.json

1{
2    "actorSpecification": 1,
3    "name": "my-actor-1",
4    "title": "Project Puppeteer Crawler JavaScript",
5    "description": "Crawlee and Puppeteer project in JavaScript.",
6    "version": "0.0",
7    "meta": {
8        "templateId": "js-crawlee-puppeteer-chrome"
9    },
10    "input": "./input_schema.json",
11    "dockerfile": "./Dockerfile"
12}

.actor/input_schema.json

1{
2    "title": "Analyse CMS et Marketing Email",
3    "type": "object",
4    "schemaVersion": 1,
5    "description": "Analysez les sites web pour détecter leur CMS (Shopify/WordPress) et leurs outils d'email marketing (Klaviyo, Brevo, Mailchimp, etc.)",
6    "properties": {
7        "keywords": {
8            "title": "Termes de recherche",
9            "type": "array",
10            "description": "Entrez un ou plusieurs mots-clés à rechercher sur Google (un par ligne)",
11            "editor": "stringList",
12            "default": ["vêtements", "mode"],
13            "sectionCaption": "Paramètres de recherche",
14            "sectionDescription": "Configurez votre recherche Google"
15        },
16        "maxPages": {
17            "title": "Nombre de pages Google par mot-clé",
18            "type": "integer",
19            "description": "Nombre de pages de résultats Google à analyser pour chaque mot-clé",
20            "default": 1,
21            "minimum": 1,
22            "maximum": 100,
23            "unit": "page(s)"
24        },
25        "maxUrlsToAnalyze": {
26            "title": "Nombre de sites à analyser par mot-clé",
27            "type": "integer",
28            "description": "Combien de sites souhaitez-vous scanner pour chaque mot-clé",
29            "default": 20,
30            "minimum": 5,
31            "maximum": 300,
32            "unit": "site(s)"
33        },
34        "country": {
35            "title": "Pays pour les résultats",
36            "type": "string",
37            "description": "Choisissez le pays pour lequel vous souhaitez voir les résultats Google",
38            "editor": "select",
39            "default": "fr",
40            "enum": ["fr", "be", "ch", "ca", "ma", "us", "gb"],
41            "enumTitles": ["France", "Belgique", "Suisse", "Canada", "Maroc", "États-Unis", "Royaume-Uni"]
42        }
43    },
44    "required": ["keywords"]
45}

src/main.js

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

src/routes.js

1import { Dataset, createPuppeteerRouter } from 'crawlee';
2
3export const router = createPuppeteerRouter();
4
5router.addDefaultHandler(async ({ enqueueLinks, log }) => {
6    log.info(`enqueueing new URLs`);
7    await enqueueLinks({
8        globs: ['https://apify.com/*'],
9        label: 'detail',
10    });
11});
12
13router.addHandler('detail', async ({ request, page, log }) => {
14    const title = await page.title();
15    log.info(`${title}`, { url: request.loadedUrl });
16
17    await Dataset.pushData({
18        url: request.loadedUrl,
19        title,
20    });
21});

.dockerignore

1# configurations
2.idea
3
4# crawlee and apify storage folders
5apify_storage
6crawlee_storage
7storage
8
9# installed files
10node_modules
11
12# git folder
13.git

.editorconfig

1root = true
2
3[*]
4indent_style = space
5indent_size = 4
6charset = utf-8
7trim_trailing_whitespace = true
8insert_final_newline = true
9end_of_line = lf

.eslintrc

1{
2    "extends": "@apify",
3    "root": true
4}

.gitignore

1# This file tells Git which files shouldn't be added to source control
2
3.DS_Store
4.idea
5dist
6node_modules
7apify_storage
8storage

package.json

1{
2    "name": "crawlee-puppeteer-javascript",
3    "version": "0.0.1",
4    "type": "module",
5    "description": "This is an example of an Apify actor.",
6    "dependencies": {
7        "apify": "^3.2.6",
8        "crawlee": "^3.11.5",
9        "puppeteer": "*",
10        "json2csv": "^5.0.7"
11    },
12    "devDependencies": {
13        "@apify/eslint-config": "^0.4.0",
14        "eslint": "^8.50.0"
15    },
16    "scripts": {
17        "start": "node src/main.js",
18        "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1"
19    },
20    "author": "It's not you it's me",
21    "license": "ISC"
22}

Pricing

Pricing model

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage.