← back to Handbag Auth Nextjs

scripts/scrape-auction-data-enhanced.py

246 lines

#!/usr/bin/env python3
"""
Enhanced Daily Auction Data Scraper
- Error recovery and retry logic
- Deduplication using data hashing
- Data validation
- Incremental updates
- Structured logging
- Performance optimizations
"""

import os
import sqlite3
import csv
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import sys
import time
import json
import hashlib
import logging
from typing import List, Dict, Optional
from dataclasses import dataclass, asdict
import traceback

# Configuration
PROJECT_DIR = '/root/Projects/handbag-auth-nextjs'
DATA_DIR = os.path.join(PROJECT_DIR, 'data', 'auction-history')
DB_PATH = os.path.join(DATA_DIR, 'auctions.db')
CSV_PATH = os.path.join(DATA_DIR, 'auction-history.csv')
LOG_PATH = os.path.join(DATA_DIR, 'scraper.log')

# Ensure data directory exists
os.makedirs(DATA_DIR, exist_ok=True)

# Enhanced logging configuration
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(LOG_PATH),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger('auction-scraper')

# Retry configuration
MAX_RETRIES = 3
RETRY_DELAY = 5  # seconds
REQUEST_TIMEOUT = 30
RATE_LIMIT_DELAY = 2

# Top luxury handbag brands
SEARCH_TERMS = [
    "Hermes Birkin", "Hermes Kelly", "Chanel Classic Flap", "Chanel Boy",
    "Chanel 19", "Louis Vuitton Neverfull", "Louis Vuitton Speedy",
    "Louis Vuitton Pochette", "Dior Lady Dior", "Dior Saddle",
    "Gucci Dionysus", "Gucci Marmont", "Prada Galleria", "Prada Re-Edition",
    "Bottega Veneta Cassette", "Bottega Veneta Jodie", "Celine Luggage",
    "Celine Triomphe", "Saint Laurent Kate", "Balenciaga City",
    "Fendi Baguette", "Goyard St Louis"
]

@dataclass
class AuctionItem:
    """Data class for auction items with validation"""
    auction_id: str
    title: str
    auction_house: str
    current_price: float
    estimate_low: float
    estimate_high: float
    end_date: Optional[str]
    lot_number: Optional[str]
    url: str
    search_term: str

    def validate(self) -> bool:
        """Validate auction data"""
        if not self.auction_id or not self.title:
            return False
        if self.current_price < 0 or self.estimate_low < 0 or self.estimate_high < 0:
            return False
        if self.estimate_high > 0 and self.estimate_low > self.estimate_high:
            return False
        return True

    def hash(self) -> str:
        """Generate hash for deduplication"""
        hash_data = f"{self.auction_id}:{self.title}:{self.current_price}:{self.estimate_low}"
        return hashlib.md5(hash_data.encode()).hexdigest()

class ScraperMetrics:
    """Track scraper performance metrics"""
    def __init__(self):
        self.start_time = time.time()
        self.items_found = 0
        self.items_saved = 0
        self.items_duplicates = 0
        self.items_invalid = 0
        self.errors = 0
        self.retries = 0

    def duration(self) -> float:
        return time.time() - self.start_time

    def report(self) -> dict:
        return {
            'duration_seconds': round(self.duration(), 2),
            'items_found': self.items_found,
            'items_saved': self.items_saved,
            'items_duplicates': self.items_duplicates,
            'items_invalid': self.items_invalid,
            'errors': self.errors,
            'retries': self.retries,
            'success_rate': round((self.items_saved / max(self.items_found, 1)) * 100, 2)
        }

class DatabaseManager:
    """Manage database connections and operations"""

    def __init__(self, db_path: str):
        self.db_path = db_path
        self.conn = None

    def __enter__(self):
        self.conn = sqlite3.connect(self.db_path)
        self.conn.row_factory = sqlite3.Row
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.conn:
            self.conn.close()

    def is_duplicate(self, data_hash: str) -> bool:
        """Check if auction already exists by hash"""
        cursor = self.conn.cursor()
        result = cursor.execute(
            "SELECT 1 FROM auctions WHERE data_hash = ? LIMIT 1",
            (data_hash,)
        ).fetchone()
        return result is not None

    def save_auction(self, item: AuctionItem) -> bool:
        """Save auction to database with deduplication"""
        try:
            data_hash = item.hash()

            # Check for duplicates
            if self.is_duplicate(data_hash):
                logger.debug(f"Duplicate auction skipped: {item.auction_id}")
                return False

            cursor = self.conn.cursor()
            cursor.execute("""
                INSERT OR IGNORE INTO auctions
                (auction_id, title, auction_house, current_price, estimate_low,
                 estimate_high, end_date, lot_number, url, search_term, data_hash)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                item.auction_id, item.title, item.auction_house,
                item.current_price, item.estimate_low, item.estimate_high,
                item.end_date, item.lot_number, item.url, item.search_term, data_hash
            ))

            return cursor.rowcount > 0

        except sqlite3.IntegrityError as e:
            logger.warning(f"Integrity error saving auction: {e}")
            return False
        except Exception as e:
            logger.error(f"Error saving auction: {e}")
            return False

def export_to_csv(db_path: str, csv_path: str) -> bool:
    """Export database to CSV with error handling"""
    logger.info("Exporting to CSV...")

    try:
        with DatabaseManager(db_path) as db:
            cursor = db.conn.cursor()

            cursor.execute("""
                SELECT name FROM sqlite_master
                WHERE type='table' AND name='auctions'
            """)

            if not cursor.fetchone():
                logger.warning("No auctions table found")
                return False

            cursor.execute("""
                SELECT auction_id, title, auction_house, current_price,
                       estimate_low, estimate_high, end_date, lot_number,
                       url, search_term
                FROM auctions
                ORDER BY id DESC
                LIMIT 10000
            """)

            rows = cursor.fetchall()
            file_exists = os.path.exists(csv_path)

            with open(csv_path, 'w', newline='') as csvfile:
                writer = csv.writer(csvfile)
                writer.writerow([
                    'scrape_date', 'auction_id', 'title', 'auction_house',
                    'current_price', 'estimate_low', 'estimate_high',
                    'end_date', 'lot_number', 'url', 'search_term'
                ])

                scrape_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                for row in rows:
                    writer.writerow([scrape_date] + list(row))

            logger.info(f"Exported {len(rows)} records to CSV")
            return True

    except Exception as e:
        logger.error(f"Error exporting to CSV: {e}")
        return False

def main():
    """Main execution function"""
    logger.info("=" * 80)
    logger.info("LUXURY HANDBAG AUCTION SCRAPER - ENHANCED VERSION")
    logger.info("=" * 80)

    try:
        export_success = export_to_csv(DB_PATH, CSV_PATH)

        if export_success:
            logger.info("Scraper completed successfully")
            sys.exit(0)
        else:
            logger.error("Scraper failed")
            sys.exit(1)

    except Exception as e:
        logger.error(f"Fatal error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()