# Data Extraction Pipelines: Von Roh-Daten zu Insights **Meta-Description:** Robuste Data Extraction Pipelines. ETL Patterns, Data Validation, Error Handling und Scalable Architecture. **Keywords:** Data Extraction, ETL Pipeline, Data Processing, Web Scraping Pipeline, Data Validation, Stream Processing --- ## Einführung Web Scraping ist nur der erste Schritt. **Data Extraction Pipelines** transformieren Rohdaten in nutzbare, validierte Datasets – mit Error Handling, Deduplication und Monitoring. --- ## Pipeline Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ DATA EXTRACTION PIPELINE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. EXTRACT │ │ ├── Web Scraping (Playwright/Puppeteer) │ │ ├── API Calls │ │ ├── File Imports (CSV, JSON, XML) │ │ └── Database Queries │ │ ↓ │ │ 2. TRANSFORM │ │ ├── Data Cleaning │ │ ├── Normalization │ │ ├── Validation (Zod) │ │ ├── Enrichment │ │ └── Deduplication │ │ ↓ │ │ 3. LOAD │ │ ├── Database Insert │ │ ├── File Export │ │ ├── API Push │ │ └── Event Stream │ │ ↓ │ │ 4. MONITOR │ │ ├── Success/Failure Rates │ │ ├── Data Quality Metrics │ │ └── Alerting │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Pipeline Class Structure ```typescript import { z } from 'zod'; import { EventEmitter } from 'events'; // Base Types interface PipelineResult { success: boolean; data?: T; errors?: string[]; metadata: { duration: number; recordsProcessed: number; recordsFailed: number; }; } // Abstract Pipeline abstract class DataPipeline extends EventEmitter { protected name: string; constructor(name: string) { super(); this.name = name; } async run(input: TInput): Promise> { const startTime = Date.now(); let recordsProcessed = 0; let recordsFailed = 0; const errors: string[] = []; try { this.emit('start', { pipeline: this.name }); // Extract this.emit('phase', { phase: 'extract' }); const rawData = await this.extract(input); // Transform this.emit('phase', { phase: 'transform' }); const transformedData: TOutput[] = []; for (const item of rawData) { try { const transformed = await this.transform(item); if (transformed) { transformedData.push(transformed); recordsProcessed++; } } catch (error) { recordsFailed++; errors.push(`Transform error: ${(error as Error).message}`); this.emit('error', { phase: 'transform', error }); } } // Load this.emit('phase', { phase: 'load' }); await this.load(transformedData); this.emit('complete', { pipeline: this.name, recordsProcessed, recordsFailed }); return { success: true, data: transformedData as any, errors: errors.length > 0 ? errors : undefined, metadata: { duration: Date.now() - startTime, recordsProcessed, recordsFailed } }; } catch (error) { this.emit('error', { phase: 'pipeline', error }); return { success: false, errors: [(error as Error).message], metadata: { duration: Date.now() - startTime, recordsProcessed, recordsFailed } }; } } protected abstract extract(input: TInput): Promise; protected abstract transform(item: any): Promise; protected abstract load(data: TOutput[]): Promise; } ``` --- ## Concrete Pipeline Implementation ```typescript import * as cheerio from 'cheerio'; import axios from 'axios'; // Schema Definition const ProductSchema = z.object({ id: z.string(), name: z.string().min(1), price: z.number().positive(), currency: z.enum(['EUR', 'USD', 'GBP']), description: z.string().optional(), category: z.string(), imageUrl: z.string().url().optional(), sourceUrl: z.string().url(), scrapedAt: z.date() }); type Product = z.infer; // Product Scraping Pipeline class ProductScrapingPipeline extends DataPipeline { private seenIds = new Set(); constructor() { super('ProductScrapingPipeline'); } protected async extract(urls: string[]): Promise { const rawProducts: any[] = []; for (const url of urls) { try { const { data: html } = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 }); const $ = cheerio.load(html); $('.product-card').each((_, element) => { const $el = $(element); rawProducts.push({ id: $el.data('product-id'), name: $el.find('.product-name').text().trim(), priceText: $el.find('.price').text().trim(), description: $el.find('.description').text().trim(), category: $el.find('.category').text().trim(), imageUrl: $el.find('img').attr('src'), sourceUrl: url }); }); // Rate Limiting await new Promise(r => setTimeout(r, 1000)); } catch (error) { this.emit('error', { phase: 'extract', url, error }); } } return rawProducts; } protected async transform(item: any): Promise { // Deduplication if (this.seenIds.has(item.id)) { return null; } this.seenIds.add(item.id); // Price Parsing const priceMatch = item.priceText?.match(/[\d,.]+/); if (!priceMatch) return null; const price = parseFloat(priceMatch[0].replace(',', '.')); // Currency Detection let currency: 'EUR' | 'USD' | 'GBP' = 'EUR'; if (item.priceText.includes('$')) currency = 'USD'; if (item.priceText.includes('£')) currency = 'GBP'; // Construct Product const product = { id: item.id, name: item.name, price, currency, description: item.description || undefined, category: item.category || 'Uncategorized', imageUrl: item.imageUrl?.startsWith('http') ? item.imageUrl : item.imageUrl ? new URL(item.imageUrl, item.sourceUrl).toString() : undefined, sourceUrl: item.sourceUrl, scrapedAt: new Date() }; // Validation const result = ProductSchema.safeParse(product); if (!result.success) { this.emit('validation_error', { item, errors: result.error.errors }); return null; } return result.data; } protected async load(products: Product[]): Promise { // Batch Insert const batchSize = 100; for (let i = 0; i < products.length; i += batchSize) { const batch = products.slice(i, i + batchSize); await db.product.createMany({ data: batch, skipDuplicates: true }); this.emit('progress', { loaded: i + batch.length, total: products.length }); } } } // Usage const pipeline = new ProductScrapingPipeline(); pipeline.on('start', (data) => console.log('Pipeline started:', data)); pipeline.on('phase', (data) => console.log('Phase:', data.phase)); pipeline.on('progress', (data) => console.log('Progress:', data)); pipeline.on('error', (data) => console.error('Error:', data)); pipeline.on('complete', (data) => console.log('Complete:', data)); const result = await pipeline.run([ 'https://shop.example.com/category/electronics?page=1', 'https://shop.example.com/category/electronics?page=2' ]); console.log('Result:', result.metadata); ``` --- ## Stream Processing für große Datasets ```typescript import { Transform, Readable, Writable } from 'stream'; import { pipeline } from 'stream/promises'; // Extract Stream function createExtractStream(urls: string[]): Readable { let index = 0; return new Readable({ objectMode: true, async read() { if (index >= urls.length) { this.push(null); return; } const url = urls[index++]; try { const { data: html } = await axios.get(url); const $ = cheerio.load(html); $('.item').each((_, el) => { this.push({ url, html: $(el).html() }); }); } catch (error) { console.error(`Failed to fetch ${url}`); } } }); } // Transform Stream function createTransformStream(schema: z.ZodSchema): Transform { return new Transform({ objectMode: true, transform(chunk, encoding, callback) { try { const $ = cheerio.load(chunk.html); const data = { name: $('h2').text().trim(), price: parseFloat($('.price').text().replace(/[^0-9.]/g, '')), sourceUrl: chunk.url }; const result = schema.safeParse(data); if (result.success) { callback(null, result.data); } else { callback(null); // Skip invalid items } } catch (error) { callback(error as Error); } } }); } // Load Stream function createLoadStream(batchSize: number = 100): Writable { let batch: any[] = []; return new Writable({ objectMode: true, async write(chunk, encoding, callback) { batch.push(chunk); if (batch.length >= batchSize) { await saveBatch(batch); batch = []; } callback(); }, async final(callback) { if (batch.length > 0) { await saveBatch(batch); } callback(); } }); } // Stream Pipeline async function runStreamPipeline(urls: string[]) { await pipeline( createExtractStream(urls), createTransformStream(ProductSchema), createLoadStream(100) ); } ``` --- ## Data Quality & Monitoring ```typescript interface DataQualityMetrics { totalRecords: number; validRecords: number; invalidRecords: number; duplicates: number; nullFields: Record; validationErrors: Record; } class DataQualityMonitor { private metrics: DataQualityMetrics = { totalRecords: 0, validRecords: 0, invalidRecords: 0, duplicates: 0, nullFields: {}, validationErrors: {} }; recordValid() { this.metrics.totalRecords++; this.metrics.validRecords++; } recordInvalid(errors: z.ZodError) { this.metrics.totalRecords++; this.metrics.invalidRecords++; errors.errors.forEach(err => { const path = err.path.join('.'); this.metrics.validationErrors[path] = (this.metrics.validationErrors[path] || 0) + 1; }); } recordDuplicate() { this.metrics.duplicates++; } recordNullField(fieldName: string) { this.metrics.nullFields[fieldName] = (this.metrics.nullFields[fieldName] || 0) + 1; } getReport(): DataQualityMetrics & { qualityScore: number } { const qualityScore = this.metrics.validRecords / this.metrics.totalRecords * 100; return { ...this.metrics, qualityScore: Math.round(qualityScore * 100) / 100 }; } async sendAlert(threshold: number = 80) { const report = this.getReport(); if (report.qualityScore < threshold) { // Alert senden (Email, Slack, etc.) console.warn(`Data quality below threshold: ${report.qualityScore}%`); } } } ``` --- ## Scheduling & Orchestration ```typescript import { CronJob } from 'cron'; // Scheduled Pipeline Execution const pipelineJob = new CronJob( '0 2 * * *', // Täglich um 2:00 Uhr async () => { console.log('Starting scheduled pipeline run'); const pipeline = new ProductScrapingPipeline(); const urls = await getUrlsToScrape(); const result = await pipeline.run(urls); // Metrics speichern await db.pipelineRun.create({ data: { pipelineName: 'ProductScrapingPipeline', status: result.success ? 'success' : 'failed', recordsProcessed: result.metadata.recordsProcessed, recordsFailed: result.metadata.recordsFailed, duration: result.metadata.duration, errors: result.errors } }); // Alerts bei Fehlern if (!result.success || result.metadata.recordsFailed > 100) { await sendPipelineAlert(result); } }, null, true, 'Europe/Berlin' ); // Manual Trigger async function triggerPipeline(pipelineId: string) { // Queue Job für async Execution await jobQueue.add('run-pipeline', { pipelineId }); } ``` --- ## Fazit Robuste Data Pipelines brauchen: 1. **Validation**: Zod für Schema-Enforcement 2. **Error Handling**: Graceful Degradation 3. **Monitoring**: Quality Metrics & Alerts 4. **Scalability**: Stream Processing für große Datasets Von Rohdaten zu strukturierten, vertrauenswürdigen Daten. --- ## Bildprompts 1. "Data flowing through pipeline stages, ETL visualization" 2. "Quality filter cleaning dirty data, data validation concept" 3. "Multiple data sources merging into single database, integration" --- ## Quellen - [Node.js Streams](https://nodejs.org/api/stream.html) - [Zod Documentation](https://zod.dev/) - [ETL Best Practices](https://www.databricks.com/glossary/etl) - [Data Quality Metrics](https://www.montecarlodata.com/blog-data-quality-metrics/)