Dans python_email
npm install @sendgrid/mail
Créer .env et met
SENDGRID_API_KEY=SG.jj_YZZqtTYOYSoxxs15YWw.hx6Vb3pc19hKZ9xbiAllcj6EfgDb6d8TfgmMTnEn4C
E
Et dans [Link] met
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import multer from 'multer';
import xlsx from 'xlsx';
import fs from 'fs';
import path from 'path';
import sgMail from '@sendgrid/mail';
import { fileURLToPath } from 'url';
import templates from './[Link]'; // Assure-toi que c'est bien un export ES module
const __filename = fileURLToPath([Link]);
const __dirname = [Link](__filename);
const app = express();
const PORT = 3000;
[Link]([Link].SENDGRID_API_KEY);
[Link](cors());
[Link]([Link]());
[Link]([Link]([Link](__dirname, '..', 'frontend')));
const upload = multer({ dest: 'uploads/' });
[Link]('/api/send-from-excel', [Link]('file'), async (req, res) => {
const { templateId, subject, message } = [Link];
const file = [Link];
[Link](' Body reçu :', [Link]);
[Link](' Fichier reçu :', file ? [Link] : 'Aucun fichier');
if (!file) return [Link](400).json({ error: 'Fichier manquant' });
if (!subject) return [Link](400).json({ error: 'Sujet manquant' });
if (!templateId && (!message || [Link]() === '')) {
return [Link](400).json({ error: 'Template ou message requis' });
let baseContent = (message && [Link]() !== '') ? message : templates[templateId];
if (!baseContent) return [Link](404).json({ error: 'Template introuvable' });
try {
const workbook = [Link]([Link]);
const sheetName = [Link][0];
const worksheet = [Link][sheetName];
const rows = [Link].sheet_to_json(worksheet);
[Link](' Données Excel chargées:', rows);
[Link]([Link]);
const results = [];
for (const [index, row] of [Link]()) {
[Link](` Ligne ${index + 1}:`, row);
// Recherche clé email sans tenir compte de la casse
const emailKey = [Link](row).find(k => [Link]() === 'email');
if (!emailKey || !row[emailKey]) {
[Link](` Ligne ${index + 1}: Email manquant ou clé incorrecte`);
[Link]({ row, status: 'Erreur: Email manquant' });
continue;
let emailContent = baseContent;
for (const key in row) {
if ([Link](key)) {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'gi');
emailContent = [Link](regex, row[key]);
const msg = {
to: row[emailKey],
from: 'comparateur@[Link]',
subject,
text: emailContent,
};
try {
await [Link](msg);
[Link](` Email envoyé à ${row[emailKey]}`);
[Link]({ row, status: 'Envoyé' });
} catch (err) {
[Link](` Erreur envoi email à ${row[emailKey]}:`, [Link]);
[Link]({ row, status: 'Erreur: ' + [Link] });
}
[Link]({ message: 'Envoi depuis fichier Excel terminé', details: results });
} catch (err) {
[Link](' Erreur lors du traitement du fichier:', err);
[Link](500).json({ error: 'Erreur lors du traitement du fichier' });
});
[Link]('/api/templates', (req, res) => {
[Link](templates);
});
[Link](PORT, () => {
[Link](`Serveur backend démarré sur [Link]
});
Dans [Link] met
const templates = {
'1': `Bonjour {{ prénom }},
Nous avons une offre spéciale rien que pour vous !
Cordialement,
L’équipe Dashboard Automations`,
'2': `Bonjour {{ prénom }},
Vous êtes cordialement invité(e) à notre prochain événement.
Au plaisir de vous y voir !
L’équipe Dashboard Automations`,
'3': `Bonjour {{ prénom }},
Je me permets de revenir vers vous concernant notre dernière discussion.
N’hésitez pas à me contacter.
L’équipe Dashboard Automations`
};
export default templates;
et dans [Link] met
"name": "python_email",
"version": "1.0.0",
"type": "module",
Pour scrapping met dans [Link]
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import fs from "fs";
import cliProgress from "cli-progress";
import { Parser } from "json2csv";
[Link](StealthPlugin());
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
function isBrowserActive(browser) {
return browser && [Link]();
function savePartialResults(result, filePath = "results_detailed.json") {
let existingResults = [];
if ([Link](filePath)) {
try {
const fileData = [Link](filePath, "utf-8");
existingResults = fileData ? [Link](fileData) : [];
} catch (error) {
[Link]("⚠ Erreur lors de la lecture du JSON :", [Link]);
existingResults = [];
[Link](result);
[Link](filePath, [Link](existingResults, null, 2), "utf-8");
async function scrapeGoogleMaps(searchQuery) {
const browser = await [Link]({ headless: false, args: ["--no-sandbox"] });
if (!isBrowserActive(browser)) {
[Link](" Le navigateur Puppeteer ne s'est pas lancé correctement.");
return [];
const page = await [Link]();
try {
await [Link]("[Link] { waitUntil: "networkidle2", timeout: 60000
});
await [Link]("body", { timeout: 10000 });
[Link](` Recherche en cours pour : ${searchQuery}`);
await delay(5000);
await [Link]('div[role="feed"]', { timeout: 60000 });
[Link](" Chargement des résultats...");
await [Link](async () => {
const wrapper = [Link]('div[role="feed"]');
await new Promise(resolve => {
let previousHeight = 0;
let sameHeightCount = 0;
const distance = 1000;
const scrollDelay = 1000;
const timer = setInterval(() => {
[Link](0, distance);
if (previousHeight === [Link]) {
sameHeightCount++;
} else {
sameHeightCount = 0;
previousHeight = [Link];
if (sameHeightCount >= 3) {
clearInterval(timer);
resolve();
}
}, scrollDelay);
});
});
const results = await [Link](() => {
return [Link]([Link]('div[role="feed"] > div'))
.map(item => ({
title: [Link]('.fontHeadlineSmall')?.innerText || "N/A",
link: [Link]('a')?.href || "N/A"
}))
.filter(result => [Link] !== "N/A");
});
[Link](` ${[Link]} lieux trouvés.`);
if (!isBrowserActive(browser)) {
throw new Error("⚠ Le navigateur Puppeteer s'est fermé prématurément avant d'extraire les
détails.");
const progressBar = new [Link]({}, [Link].shades_classic);
[Link]([Link], 0);
const detailedResults = [];
for (let i = 0; i < [Link]; i++) {
const detail = await scrapeDetailPage(results[i], browser);
savePartialResults(detail);
[Link](detail);
[Link](i + 1);
}
[Link]();
[Link]("results_detailed.json", [Link](detailedResults, null, 2), "utf-8");
[Link](" Les résultats ont été sauvegardés dans `results_detailed.json`");
try {
const parser = new Parser();
const csv = [Link](detailedResults);
[Link]("results_detailed.csv", csv, "utf-8");
[Link](" Les résultats ont également été sauvegardés dans `results_detailed.csv`");
} catch (csvError) {
[Link](" Erreur lors de la conversion en CSV :", csvError);
return detailedResults;
} catch (error) {
[Link](" Erreur lors du scraping :", [Link]);
[Link]([Link]);
return [];
} finally {
if (isBrowserActive(browser)) {
[Link](" Fermeture du navigateur...");
await [Link]();
async function scrapeDetailPage(result, browser) {
if (!isBrowserActive(browser)) {
[Link](`⚠ Le navigateur Puppeteer est fermé. Impossible de récupérer ${[Link]}`);
return { title: [Link], error: "Navigateur fermé avant extraction" };
}
let detailPage;
try {
detailPage = await [Link]();
await [Link]([Link], { waitUntil: "networkidle2", timeout: 30000 });
await delay(1000);
const detail = await [Link](() => {
const title = [Link]("h1[class^='DUwDvf'], h1[class*='x3AX1']")?.innerText ||
"Non disponible";
const rating = [Link]("span.F7nice, span[aria-label*='étoiles']")?.textContent
|| "Non disponible";
const reviews = [Link]("button[aria-label*='avis'], span[aria-
label*='avis']")?.textContent || "Non disponible";
const address = [Link]("button[data-item-id='address'], div[aria-
label='Adresse']")?.innerText || "Non disponible";
let phone = [Link]("button[data-tooltip='Copier le numéro de téléphone'],
span[aria-label*='Téléphone']")?.innerText || "Non disponible";
const website = [Link]("a[data-tooltip='Ouvrir le site Web'],
a[href*='http']")?.href || "Non disponible";
if (phone === "Non disponible") {
const pageText = [Link];
const phonePattern = /(?:\+?\d{1,3})?\s?(?:\(?\d{2,4}\)?\s?)?\d{2,4}[-.\s]?\d{2,4}[-
.\s]?\d{2,4}/g;
const phoneMatch = [Link](phonePattern);
if (phoneMatch) {
phone = phoneMatch[0];
return { title, rating, reviews, address, phone, website };
});
[Link](` Détails récupérés pour : ${[Link]}`);
return detail;
} catch (error) {
[Link](` Erreur pour ${[Link]} :`, [Link]);
return { title: [Link], error: [Link] };
} finally {
if (detailPage && ![Link]()) {
await [Link]();
// Export pour utilisation dans Express
export async function scrapeGoogleMapsAPI(searchQuery) {
return scrapeGoogleMaps(searchQuery);
// Si script lancé directement en CLI
if ([Link] === `[Link] {
const searchQuery = [Link][2] || "café lac1";
scrapeGoogleMaps(searchQuery);
Et dans [Link]
import express from "express";
import cors from "cors";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { scrapeGoogleMapsAPI } from "./[Link]";
const __filename = fileURLToPath([Link]);
const __dirname = dirname(__filename);
const app = express();
const PORT = 4000;
// Middleware pour autoriser CORS et parser JSON
[Link](cors());
[Link]([Link]());
// Log à chaque requête reçue (middleware)
[Link]((req, res, next) => {
[Link](`[${new Date().toISOString()}] Requête reçue : ${[Link]} ${[Link]}`);
next();
});
[Link]("/scrape", async (req, res) => {
[Link](" Route POST /scrape appelée");
[Link](" Body reçu :", [Link]);
try {
const { keyword } = [Link];
if (!keyword) {
[Link]("⚠ Mot-clé absent dans la requête");
return [Link](400).json({ error: "Le mot-clé est requis." });
[Link](" Démarrage du scraping avec keyword :", keyword);
const results = await scrapeGoogleMapsAPI(keyword);
[Link](` Scraping terminé avec ${[Link]} résultats`);
[Link](200).json({ message: "Scraping terminé avec succès.", results });
} catch (error) {
[Link](" Erreur capturée dans /scrape :", error);
[Link](500).json({ error: "Erreur lors du scraping", details: [Link] });
});
[Link]("/", (req, res) => {
[Link]("GET / - Envoi de la page [Link]");
[Link]([Link](__dirname, "frontend", "[Link]"));
});
[Link](PORT, () => {
[Link](` Serveur démarré sur [Link]
});