Recherche CMS
Pricing
Pay per usage
Go to Store
Recherche CMS
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 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 . ./
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 usageThis Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage.