Ce tutoriel explique comment j'ai créé un pipeline ETL (Extract, Transform, Load) pour analyser les articles de HackerNews.
J'ai utilisé Playwright pour scraper HackerNews car il gère bien le JavaScript moderne :
// scraper.js
const { chromium } = require('playwright');
const fs = require('fs').promises;
async function scrape() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com');
const articles = await page.evaluate(() => {
const items = document.querySelectorAll('.athing');
return Array.from(items).map(item => ({
id: item.getAttribute('id'),
title: item.querySelector('.titleline a').innerText,
url: item.querySelector('.titleline a').href
}));
});
await fs.writeFile('raw_data.json', JSON.stringify(articles, null, 2));
}
Pour la transformation, j'utilise pandas qui est parfait pour manipuler et analyser les données :
# transform.py
import pandas as pd
from urllib.parse import urlparse
def transform():
# Lecture et conversion en DataFrame
df = pd.DataFrame(raw_data)
# Enrichissement
df['title'] = df['title'].str.lower()
df['domain'] = df['url'].apply(lambda x: urlparse(x).netloc)
df['word_count'] = df['title'].str.split().str.len()
# Catégorisation intelligente
domain_categories = {
'github.com': 'development',
'stackoverflow.com': 'programming',
'aws.amazon.com': 'cloud',
'medium.com': 'blog',
'youtube.com': 'video'
}
df['category'] = df['domain'].map(domain_categories).fillna('other')
# Détection tech avec mots-clés
tech_keywords = [
'python', 'javascript', 'react', 'node', 'aws',
'cloud', 'api', 'docker', 'kubernetes'
]
df['is_tech'] = df['title'].str.contains('|'.join(tech_keywords))
Cassandra est une base de données NoSQL orientée colonnes, parfaite pour notre ETL :
Keyspace (≈ Database)
└── Table (≈ Column Family)
└── Row
└── Column
├── Name
├── Value
└── Timestamp
- Optimisé pour l'écriture (parfait pour ETL)
- Scalable horizontalement (scale out)
- Pas de JOINS (on dénormalise à l'écriture)
- Idéal pour les données temporelles
Scaling Vertical (Scale Up) :
┌────────────────┐
│ Grosse Machine │ <- Ajouter RAM/CPU à une seule machine
└────────────────┘
Scaling Horizontal (Scale Out) :
┌────────┐ ┌────────┐ ┌────────┐
│Machine1│ │Machine2│ │Machine3│ <- Ajouter plus de machines
└────────┘ └────────┘ └────────┘
- Vertical : Augmenter la puissance d'une machine (plus de RAM, meilleur CPU)
- Horizontal : Ajouter plus de machines au cluster
Cassandra est conçu pour le scaling horizontal :
- Les données sont automatiquement distribuées
- Pas de point unique de défaillance
- Parfait pour gérer de gros volumes de données
-- Keyspace pour notre application
CREATE KEYSPACE etl_data
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
-- Table principale
CREATE TABLE articles (
id text PRIMARY KEY, -- Clé de partitionnement
title text, -- Colonne standard
url text,
domain text,
word_count int,
category text,
is_tech boolean,
processed_at timestamp
);
// server.js
const express = require('express');
const { Client } = require('cassandra-driver');
const client = new Client({
contactPoints: ['localhost'],
localDataCenter: 'datacenter1'
});
// Création du schéma
await client.execute(`
CREATE TABLE IF NOT EXISTS articles (
id text PRIMARY KEY,
title text,
url text,
domain text,
word_count int,
category text,
is_tech boolean
)
`);
// API pour récupérer les données
app.get('/articles', async (req, res) => {
const result = await client.execute('SELECT * FROM articles');
res.json(result.rows);
});
Le frontend utilise React avec des hooks pour une UI réactive :
// front.jsx
function App() {
const [articles, setArticles] = useState([])
const [stats, setStats] = useState({
totalArticles: 0,
avgWordCount: 0,
techArticles: 0,
categoryBreakdown: {}
})
useEffect(() => {
fetch('/articles')
.then(res => res.json())
.then(data => {
setArticles(data)
// Calcul des stats
setStats({
totalArticles: data.length,
avgWordCount: data.reduce((acc, curr) => acc + curr.word_count, 0) / data.length,
techArticles: data.filter(a => a.is_tech).length,
categoryBreakdown: data.reduce((acc, curr) => {
acc[curr.category] = (acc[curr.category] || 0) + 1
return acc
}, {})
})
})
}, [])
}
Le pipeline s'exécute toutes les minutes :
// server.js
const cron = require('node-cron');
cron.schedule('* * * * *', async () => {
await scrape(); // Extract
await execAsync('python transform.py'); // Transform
await loadData(); // Load
});
- Architecture ETL : La séparation en trois étapes rend le code plus maintenable
- Pandas > JSON : Pandas simplifie énormément les transformations de données
- React moderne : Les hooks rendent le code plus lisible
- NoSQL : Cassandra est excellent pour les écritures fréquentes
# Installation des dépendances
npm install
pip install -r requirements.txt
# Démarrage de Cassandra (avec brew)
brew services start cassandra
# Dans un premier terminal (backend)
node server.js
# Dans un second terminal (frontend)
npm run front
.
├── scraper.js # Extraction (Playwright)
├── transform.py # Transformation (Pandas)
├── server.js # API + Cassandra
├── front.jsx # Interface React
├── styles.css # Styles
└── package.json # Dépendances
- Ajouter Kafka pour du vrai temps réel
- Utiliser TypeScript
- Ajouter des tests
- Dockeriser l'application
- Ajouter plus de visualisations (graphiques, etc.)
Le dashboard est accessible sur http://localhost:5173