Web Scraping avec Crawlee et TypeScript : guide complet du zéro au déploiement en production

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Le web scraping bien fait. Crawlee est le framework TypeScript open-source par Apify qui gère les parties difficiles — files de requêtes, nouvelles tentatives, rotation de proxies et anti-blocage — pour que vous puissiez vous concentrer sur l'extraction de données.

Ce que vous apprendrez

À la fin de ce tutoriel, vous saurez :

  • Configurer un projet Crawlee avec TypeScript depuis zéro
  • Construire des scrapers avec PlaywrightCrawler pour les sites riches en JavaScript
  • Utiliser CheerioCrawler pour le scraping HTML léger et rapide
  • Gérer les files de requêtes pour crawler des milliers de pages
  • Stocker les données extraites avec le système Dataset intégré de Crawlee
  • Implémenter la rotation de proxies et les stratégies anti-blocage
  • Gérer la pagination, le défilement infini et le contenu dynamique
  • Déployer votre scraper en production avec Docker

Prérequis

Avant de commencer, assurez-vous de disposer de :

  • Node.js 20+ installé (node --version)
  • Expérience en TypeScript (types, async/await, generics)
  • Connaissances de base en HTML/CSS (sélecteurs, structure DOM)
  • Un éditeur de code — VS Code ou Cursor recommandé
  • Docker installé (optionnel, pour le déploiement)

Pourquoi Crawlee ?

Le web scraping en Node.js signifie souvent assembler Puppeteer, Cheerio, des bibliothèques de requêtes, une logique de nouvelles tentatives et la gestion de files soi-même. Crawlee fournit tout cela dans un framework unique et cohérent :

FonctionnalitéCrawleeManuel (Puppeteer + Cheerio)Scrapy (Python)
LangageTypeScript/JavaScriptJavaScriptPython
Support navigateurPlaywright, PuppeteerConfiguration manuelleSplash/Selenium
File de requêtesIntégrée avec persistanceImplémentation manuelleIntégrée
Nouvelles tentativesConfigurable par requêteManuelIntégrées
Rotation de proxiesIntégrée avec gestion de sessionsManuelMiddleware
Anti-blocageEmpreintes, headersManuelMiddleware
Stockage de donnéesDataset + Key-Value StoreManuel (JSON/DB)Item pipelines
Sécurité de typesTypeScript completOptionnelNon

Crawlee vous donne la puissance de Scrapy avec la sécurité de TypeScript et une expérience développeur moderne.


Étape 1 : Configuration du projet

Commencez par créer un nouveau projet Crawlee. Le CLI génère tout ce dont vous avez besoin :

npx crawlee create my-scraper --template playwright-ts
cd my-scraper

Cela génère la structure de projet suivante :

my-scraper/
├── src/
│   ├── main.ts          # Point d'entrée
│   ├── routes.ts        # Gestionnaires de routes
│   └── types.ts         # Types personnalisés
├── storage/             # Créé automatiquement pour les données et files
├── package.json
├── tsconfig.json
└── Dockerfile           # Configuration Docker prête pour la production

Installez les dépendances :

npm install

Crawlee installe trois packages principaux :

  • crawlee — Le cœur du framework avec les crawlers, files et stockage
  • playwright — Automatisation de navigateur pour les pages rendues en JavaScript
  • @crawlee/playwright — Intégration Playwright pour Crawlee

Étape 2 : Comprendre l'architecture de Crawlee

Avant d'écrire du code, comprenons le fonctionnement de Crawlee :

┌─────────────────────────────────────────────┐
│                  Crawlee                      │
│                                               │
│  ┌──────────┐    ┌──────────┐    ┌─────────┐ │
│  │  File de │───▶│ Crawler  │───▶│ Dataset │ │
│  │ requêtes │    │ (Router) │    │ (Sortie)│ │
│  └──────────┘    └──────────┘    └─────────┘ │
│       │               │                       │
│       │          ┌────┴────┐                  │
│       │          │ Gestion │                  │
│       │          │ Proxies │                  │
│       │          └─────────┘                  │
│       ▼                                       │
│  Nouvelles tentatives automatiques            │
│  Contrôle de concurrence                      │
│  Limitation de débit                          │
└─────────────────────────────────────────────┘

Les composants clés :

  1. File de requêtes — Gère les URLs à scraper, gère la déduplication et persiste l'état entre les redémarrages
  2. Crawler — Traite chaque requête avec votre fonction de gestionnaire (Cheerio pour HTML, Playwright pour les pages rendues en JS)
  3. Router — Route les différents patterns d'URL vers différentes fonctions de traitement
  4. Dataset — Stocke les données extraites en lignes JSON, exportables en CSV, JSON ou tout format
  5. Gestionnaire de proxies — Effectue la rotation des proxies et gère les sessions pour éviter les blocages

Étape 3 : Construire un CheerioCrawler pour les pages statiques

Commençons avec le type de crawler le plus rapide — CheerioCrawler. Il télécharge le HTML brut et le parse avec Cheerio (API similaire à jQuery), sans lancer de navigateur. Parfait pour les sites qui ne nécessitent pas de rendu JavaScript.

Remplacez le contenu de src/main.ts :

import { CheerioCrawler, Dataset, log } from 'crawlee';
 
// Configurer les logs
log.setLevel(log.LEVELS.INFO);
 
// Créer le crawler
const crawler = new CheerioCrawler({
  // Nombre maximum de requêtes simultanées
  maxConcurrency: 10,
 
  // Nombre maximum de requêtes par minute (limitation de débit)
  maxRequestsPerMinute: 60,
 
  // Réessayer les requêtes échouées jusqu'à 3 fois
  maxRequestRetries: 3,
 
  // Gestionnaire pour chaque page
  async requestHandler({ request, $, enqueueLinks, pushData }) {
    const url = request.url;
    log.info(`Scraping : ${url}`);
 
    // Extraire les données de la page avec des sélecteurs CSS
    const title = $('h1').first().text().trim();
    const description = $('meta[name="description"]').attr('content') || '';
    const links = $('a[href]')
      .map((_, el) => $(el).attr('href'))
      .get()
      .filter((href) => href.startsWith('http'));
 
    // Pousser les données extraites dans le dataset
    await pushData({
      url,
      title,
      description,
      linksFound: links.length,
      scrapedAt: new Date().toISOString(),
    });
 
    // Suivre les liens de la page (crawling en largeur)
    await enqueueLinks({
      globs: ['https://example.com/**'],
      label: 'DETAIL',
    });
  },
 
  // Appelé quand une requête échoue après toutes les tentatives
  async failedRequestHandler({ request }) {
    log.error(`Échec : ${request.url} — ${request.errorMessages.join(', ')}`);
  },
});
 
// Démarrer le crawler avec des URLs de départ
await crawler.run([
  'https://example.com',
]);
 
// Exporter les résultats
const dataset = await Dataset.open();
await dataset.exportToJSON('results');
log.info('Scraping terminé ! Résultats sauvegardés dans storage/datasets/default/');

Exécutez-le :

npx tsx src/main.ts

Vos données extraites sont sauvegardées dans storage/datasets/default/ sous forme de fichiers JSON individuels.


Étape 4 : Construire un PlaywrightCrawler pour les sites dynamiques

De nombreux sites modernes rendent leur contenu avec JavaScript. Pour ceux-ci, vous avez besoin de PlaywrightCrawler, qui lance un vrai navigateur :

import { PlaywrightCrawler, Dataset, log } from 'crawlee';
 
const crawler = new PlaywrightCrawler({
  // Utiliser Chromium sans interface graphique
  launchContext: {
    launchOptions: {
      headless: true,
    },
  },
 
  // Les pages de navigateur sont coûteuses — limiter la concurrence
  maxConcurrency: 5,
 
  // Timeout par page (30 secondes)
  requestHandlerTimeoutSecs: 30,
 
  async requestHandler({ request, page, enqueueLinks, pushData }) {
    const url = request.url;
    log.info(`Scraping (navigateur) : ${url}`);
 
    // Attendre que le contenu principal soit rendu
    await page.waitForSelector('.product-card', { timeout: 10000 });
 
    // Extraire les données des produits
    const products = await page.$$eval('.product-card', (cards) =>
      cards.map((card) => ({
        name: card.querySelector('.product-name')?.textContent?.trim() || '',
        price: card.querySelector('.product-price')?.textContent?.trim() || '',
        rating: card.querySelector('.product-rating')?.textContent?.trim() || '',
        image: card.querySelector('img')?.getAttribute('src') || '',
      }))
    );
 
    // Pousser chaque produit dans le dataset
    for (const product of products) {
      await pushData({
        ...product,
        sourceUrl: url,
        scrapedAt: new Date().toISOString(),
      });
    }
 
    // Suivre les liens de pagination
    await enqueueLinks({
      selector: 'a.pagination-next',
      label: 'LISTING',
    });
  },
});
 
await crawler.run([
  'https://example-shop.com/products?page=1',
]);

Quand utiliser quel crawler

ScénarioCrawlerRaison
Sites HTML statiquesCheerioCrawler10x plus rapide, pas de surcharge navigateur
Contenu rendu en JavaScriptPlaywrightCrawlerExécute le JS, attend le rendu
Applications monopage (SPAs)PlaywrightCrawlerGère le routage côté client
APIs retournant du HTMLCheerioCrawlerBesoin seulement de parser le HTML
Sites nécessitant une connexionPlaywrightCrawlerPeut remplir des formulaires

Étape 5 : Utiliser le Router pour les patterns multi-pages

Les scrapers réels doivent gérer différents types de pages différemment — pages de liste, pages de détail, résultats de recherche. Le Router de Crawlee rend cela propre :

Créez src/routes.ts :

import { createPlaywrightRouter, Dataset } from 'crawlee';
 
export const router = createPlaywrightRouter();
 
// Gestionnaire par défaut — pages de liste
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
  log.info(`Traitement de la liste : ${request.url}`);
 
  // Extraire les liens vers les éléments individuels
  await enqueueLinks({
    selector: 'a.item-link',
    label: 'DETAIL',
  });
 
  // Gérer la pagination
  const nextButton = await page.$('a.next-page');
  if (nextButton) {
    await enqueueLinks({
      selector: 'a.next-page',
      label: 'LISTING',
    });
  }
});
 
// Gestionnaire de page de détail
router.addHandler('DETAIL', async ({ request, page, pushData, log }) => {
  log.info(`Traitement du détail : ${request.url}`);
 
  await page.waitForSelector('.article-content', { timeout: 10000 });
 
  const data = await page.evaluate(() => {
    const title = document.querySelector('h1')?.textContent?.trim() || '';
    const author = document.querySelector('.author-name')?.textContent?.trim() || '';
    const date = document.querySelector('time')?.getAttribute('datetime') || '';
    const content = document.querySelector('.article-content')?.textContent?.trim() || '';
    const tags = Array.from(document.querySelectorAll('.tag'))
      .map((tag) => tag.textContent?.trim() || '');
 
    return { title, author, date, content, tags };
  });
 
  await pushData({
    ...data,
    url: request.url,
    scrapedAt: new Date().toISOString(),
  });
});

Mettez à jour src/main.ts pour utiliser le routeur :

import { PlaywrightCrawler } from 'crawlee';
import { router } from './routes.js';
 
const crawler = new PlaywrightCrawler({
  requestHandler: router,
  maxConcurrency: 5,
  maxRequestsPerMinute: 30,
});
 
await crawler.run([
  { url: 'https://example-blog.com/articles', label: 'LISTING' },
  { url: 'https://example-blog.com/search?q=typescript', label: 'SEARCH' },
]);

Étape 6 : Gérer la pagination et le défilement infini

Pagination traditionnelle

Pour les sites avec des boutons "Suivant" ou des pages numérotées :

router.addHandler('LISTING', async ({ page, enqueueLinks, request, log }) => {
  const items = await page.$$eval('.item', (els) =>
    els.map((el) => ({
      title: el.querySelector('h2')?.textContent?.trim(),
      url: el.querySelector('a')?.href,
    }))
  );
 
  log.info(`Page ${request.userData.page || 1} : ${items.length} éléments trouvés`);
 
  for (const item of items) {
    if (item.url) {
      await enqueueLinks({
        urls: [item.url],
        label: 'DETAIL',
      });
    }
  }
 
  const nextUrl = await page.$eval('a.next', (el) => el.href).catch(() => null);
  if (nextUrl) {
    await enqueueLinks({
      urls: [nextUrl],
      label: 'LISTING',
      userData: { page: (request.userData.page || 1) + 1 },
    });
  }
});

Défilement infini

Pour les sites qui chargent plus de contenu en défilant :

router.addHandler('INFINITE', async ({ page, pushData, log }) => {
  let previousHeight = 0;
  let scrollAttempts = 0;
  const maxScrolls = 20;
 
  while (scrollAttempts < maxScrolls) {
    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
    await page.waitForTimeout(2000);
 
    const currentHeight = await page.evaluate(() => document.body.scrollHeight);
    if (currentHeight === previousHeight) {
      log.info('Plus de contenu à charger');
      break;
    }
 
    previousHeight = currentHeight;
    scrollAttempts++;
    log.info(`Défilement ${scrollAttempts}/${maxScrolls} — hauteur : ${currentHeight}`);
  }
 
  const allItems = await page.$$eval('.feed-item', (items) =>
    items.map((item) => ({
      text: item.querySelector('.content')?.textContent?.trim() || '',
      author: item.querySelector('.author')?.textContent?.trim() || '',
      timestamp: item.querySelector('time')?.getAttribute('datetime') || '',
    }))
  );
 
  log.info(`${allItems.length} éléments extraits au total`);
 
  for (const item of allItems) {
    await pushData(item);
  }
});

Étape 7 : Rotation de proxies et anti-blocage

Le blocage est le plus grand défi du web scraping. Crawlee dispose d'outils intégrés pour vous aider :

Rotation de proxies basique

import { PlaywrightCrawler, ProxyConfiguration } from 'crawlee';
 
const proxyConfiguration = new ProxyConfiguration({
  proxyUrls: [
    'http://user:pass@proxy1.example.com:8080',
    'http://user:pass@proxy2.example.com:8080',
    'http://user:pass@proxy3.example.com:8080',
  ],
});
 
const crawler = new PlaywrightCrawler({
  proxyConfiguration,
  requestHandler: router,
  useSessionPool: true,
  sessionPoolOptions: {
    maxPoolSize: 100,
    sessionOptions: {
      maxUsageCount: 50,
    },
  },
});

Bonnes pratiques anti-blocage

const crawler = new PlaywrightCrawler({
  maxRequestsPerMinute: 20,
 
  browserPoolOptions: {
    useFingerprints: true,
    fingerprintOptions: {
      fingerprintGeneratorOptions: {
        browsers: ['chrome', 'firefox'],
        operatingSystems: ['windows', 'macos', 'linux'],
        locales: ['en-US', 'en-GB'],
      },
    },
  },
 
  preNavigationHooks: [
    async ({ page }) => {
      const width = 1280 + Math.floor(Math.random() * 200);
      const height = 800 + Math.floor(Math.random() * 200);
      await page.setViewportSize({ width, height });
 
      await page.setExtraHTTPHeaders({
        'Accept-Language': 'en-US,en;q=0.9',
        'Accept-Encoding': 'gzip, deflate, br',
      });
    },
  ],
 
  requestHandler: router,
});

Étape 8 : Stockage de données et exportation

Crawlee fournit deux systèmes de stockage :

Dataset — Pour les données tabulaires

import { Dataset } from 'crawlee';
 
// Pousser des données pendant le scraping
await pushData({
  name: 'TypeScript Handbook',
  price: 29.99,
  category: 'Programming',
});
 
// Après le scraping, exporter dans différents formats
const dataset = await Dataset.open();
 
// Exporter en JSON
await dataset.exportToJSON('output');
 
// Exporter en CSV
await dataset.exportToCSV('output');
 
// Obtenir toutes les données en une fois
const { items } = await dataset.getData();
console.log(`Total des éléments : ${items.length}`);

Key-Value Store — Pour les données diverses

import { KeyValueStore } from 'crawlee';
 
const store = await KeyValueStore.open();
 
// Sauvegarder des captures d'écran
await store.setValue('homepage-screenshot', await page.screenshot(), {
  contentType: 'image/png',
});
 
// Sauvegarder la configuration ou l'état
await store.setValue('scraper-config', {
  lastRun: new Date().toISOString(),
  totalPages: 1500,
  errors: 12,
});

Étape 9 : Gestion des erreurs et résilience

Les scrapers en production doivent gérer les échecs avec élégance :

import { PlaywrightCrawler, log } from 'crawlee';
 
const crawler = new PlaywrightCrawler({
  maxRequestRetries: 3,
  requestHandlerTimeoutSecs: 60,
 
  async requestHandler({ request, page, pushData, session }) {
    try {
      const pageTitle = await page.title();
 
      // Détecter les blocages légers
      if (
        pageTitle.toLowerCase().includes('captcha') ||
        pageTitle.toLowerCase().includes('verify') ||
        pageTitle.toLowerCase().includes('access denied')
      ) {
        session?.retire();
        throw new Error(`Blocage détecté à ${request.url}`);
      }
 
      // Attendre le contenu avec un repli
      try {
        await page.waitForSelector('.main-content', { timeout: 10000 });
      } catch {
        log.warning(`Sélecteur de contenu non trouvé à ${request.url}, tentative de repli`);
        await page.waitForSelector('body', { timeout: 5000 });
      }
 
      const data = await page.evaluate(() => ({
        title: document.querySelector('h1')?.textContent?.trim() || 'Sans titre',
        content: document.querySelector('.main-content')?.textContent?.trim() || '',
      }));
 
      await pushData({
        ...data,
        url: request.url,
        retryCount: request.retryCount,
      });
    } catch (error) {
      log.error(`Erreur lors du scraping de ${request.url}`, {
        error: (error as Error).message,
        retryCount: request.retryCount,
      });
      throw error;
    }
  },
 
  async failedRequestHandler({ request, log }) {
    log.error(`Échec définitif : ${request.url}`, {
      errors: request.errorMessages,
      retries: request.retryCount,
    });
 
    const dataset = await Dataset.open('failed-requests');
    await dataset.pushData({
      url: request.url,
      errors: request.errorMessages,
      failedAt: new Date().toISOString(),
    });
  },
});

Étape 10 : Exemple concret — Scraping de tableau de bord emploi

Construisons un scraper complet qui extrait des offres d'emploi. Cet exemple démontre tous les concepts ensemble :

import { PlaywrightCrawler, Dataset, log } from 'crawlee';
 
interface JobListing {
  title: string;
  company: string;
  location: string;
  salary: string;
  description: string;
  tags: string[];
  postedAt: string;
  url: string;
  scrapedAt: string;
}
 
const crawler = new PlaywrightCrawler({
  maxConcurrency: 3,
  maxRequestsPerMinute: 15,
  maxRequestRetries: 3,
  requestHandlerTimeoutSecs: 45,
 
  browserPoolOptions: {
    useFingerprints: true,
  },
 
  preNavigationHooks: [
    async ({ page }) => {
      await page.route('**/*.{png,jpg,jpeg,gif,webp,woff,woff2}', (route) =>
        route.abort()
      );
    },
  ],
 
  async requestHandler({ request, page, enqueueLinks, pushData, log }) {
    const label = request.label || 'LISTING';
 
    if (label === 'LISTING') {
      log.info(`Scraping page de liste : ${request.url}`);
      await page.waitForSelector('.job-card', { timeout: 15000 });
 
      await enqueueLinks({
        selector: '.job-card a.job-title-link',
        label: 'JOB_DETAIL',
      });
 
      const hasNextPage = await page.$('a[aria-label="Next page"]');
      if (hasNextPage) {
        await enqueueLinks({
          selector: 'a[aria-label="Next page"]',
          label: 'LISTING',
        });
      }
    }
 
    if (label === 'JOB_DETAIL') {
      log.info(`Scraping détail emploi : ${request.url}`);
      await page.waitForSelector('.job-detail', { timeout: 15000 });
 
      const job: JobListing = await page.evaluate(() => {
        const getText = (selector: string): string =>
          document.querySelector(selector)?.textContent?.trim() || '';
 
        return {
          title: getText('h1.job-title'),
          company: getText('.company-name'),
          location: getText('.job-location'),
          salary: getText('.salary-range'),
          description: getText('.job-description'),
          tags: Array.from(document.querySelectorAll('.skill-tag')).map(
            (tag) => tag.textContent?.trim() || ''
          ),
          postedAt: getText('.posted-date'),
          url: window.location.href,
          scrapedAt: new Date().toISOString(),
        };
      });
 
      if (job.title && job.company) {
        await pushData(job);
        log.info(`Sauvegardé : ${job.title} chez ${job.company}`);
      }
    }
  },
});
 
log.info("Démarrage du scraper d'offres d'emploi...");
await crawler.run([
  { url: 'https://example-jobs.com/jobs?q=typescript', label: 'LISTING' },
]);
 
const dataset = await Dataset.open();
const { items } = await dataset.getData();
log.info(`Scraping terminé ! ${items.length} offres d'emploi extraites.`);
await dataset.exportToJSON('jobs');

Étape 11 : Déployer avec Docker

Les projets Crawlee sont livrés avec un Dockerfile prêt pour la production :

FROM node:20-slim AS builder
 
RUN npx playwright install-deps chromium
 
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
 
FROM node:20-slim
 
RUN npx playwright install chromium
RUN npx playwright install-deps chromium
 
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
 
RUN mkdir -p storage
 
ENV NODE_ENV=production
ENV CRAWLEE_STORAGE_DIR=./storage
 
CMD ["node", "dist/main.js"]

Construisez et exécutez :

docker build -t my-scraper .
docker run -v $(pwd)/output:/app/storage my-scraper

Étape 12 : Patterns avancés

Crawling reprise automatique

Crawlee persiste sa file de requêtes sur le disque. Si votre scraper plante, redémarrez-le simplement — il reprend là où il s'est arrêté :

const crawler = new PlaywrightCrawler({
  requestHandler: router,
});
 
// Premier lancement : traite toutes les URLs
await crawler.run(['https://example.com/page1', 'https://example.com/page2']);
 
// Si vous redémarrez, les URLs déjà traitées sont automatiquement ignorées

Interception des requêtes réseau

Surveillez et modifiez le trafic réseau pendant le scraping :

const crawler = new PlaywrightCrawler({
  preNavigationHooks: [
    async ({ page }) => {
      page.on('response', async (response) => {
        const url = response.url();
        if (url.includes('/api/products')) {
          try {
            const data = await response.json();
            const store = await KeyValueStore.open();
            await store.setValue(`api-response-${Date.now()}`, data);
          } catch {
            // La réponse pourrait ne pas être en JSON
          }
        }
      });
 
      await page.route('**/*', (route) => {
        const type = route.request().resourceType();
        if (['image', 'font', 'stylesheet'].includes(type)) {
          return route.abort();
        }
        return route.continue();
      });
    },
  ],
 
  async requestHandler({ page, pushData }) {
    await page.waitForSelector('.content');
    const title = await page.title();
    await pushData({ title });
  },
});

Dépannage

Problèmes courants

Le navigateur ne se lance pas dans Docker : Assurez-vous d'installer les dépendances Playwright :

npx playwright install-deps chromium

Blocages fréquents :

  • Réduisez maxConcurrency et maxRequestsPerMinute
  • Activez la rotation de proxies
  • Utilisez useFingerprints: true
  • Ajoutez des délais aléatoires entre les requêtes

Problèmes de mémoire avec de grands crawls :

  • Utilisez CheerioCrawler au lieu de PlaywrightCrawler quand possible
  • Limitez maxConcurrency pour réduire la consommation mémoire
  • Définissez maxRequestsPerCrawl pour traiter par lots

Prochaines étapes

Maintenant que vous avez un scraper Crawlee fonctionnel, voici comment le prolonger :


Conclusion

Vous avez construit un scraper web de niveau production avec Crawlee et TypeScript. Vous savez maintenant :

  • Choisir entre CheerioCrawler et PlaywrightCrawler selon votre site cible
  • Utiliser les routeurs pour gérer proprement différents types de pages
  • Implémenter la gestion de pagination et du défilement infini
  • Effectuer la rotation de proxies et gérer les sessions pour éviter les blocages
  • Stocker et exporter les données dans plusieurs formats
  • Déployer votre scraper avec Docker pour la production

Crawlee gère les parties difficiles — gestion des files, nouvelles tentatives, rotation de proxies, empreintes de navigateur — pour que vous puissiez vous concentrer sur la logique d'extraction qui compte.

Bon scraping !


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Next.js 15 Partial Prerendering (PPR) : Construire un Dashboard Ultra-Rapide avec le Rendu Hybride.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·