Recherche CMS avatar
Recherche CMS

Under maintenance

Pricing

Pay per usage

Go to Store
Recherche CMS

Recherche CMS

Under maintenance

Developed by

TML

Maintained by Community

0.0 (0)

Pricing

Pay per usage

1

Monthly users

4

Runs succeeded

>99%

Last modified

3 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.