크롤러 / 스크래퍼

수집 데이터 차분 감지 및 엑셀/CSV 적재

가격 변동·품절 재고 실시간 차분 감지, SQLite 시계열 누적, 알림 스팸 방지 및 대시보드 엑셀/CSV 다운로드 파이프라인.

업데이트: 2026-10-02

쇼핑몰 특가나 재고 모니터링 봇을 만들 때 매 수집 루프마다 무조건 알림을 발송하면, 하루에 수백 개의 무의미한 동일 메시지가 쏟아져 알림 피로감으로 인해 정작 중요한 가격 급락이나 품절 순간을 놓치게 됩니다.

직전 수집 상태와 현재 상태를 대조하여 실제 가격이 변동되었거나 품절에서 재입고되었을 때만 정밀하게 알림을 트리거하고, 수집된 모든 시계열 데이터를 DashCloud 영구 볼륨의 SQLite 및 CSV로 적재하여 엑셀로 내려받는 프로덕션 파이프라인을 구축해 봅니다.


3대 데이터 파이프라인 아키텍처 비교

파이프라인 방식동작 원리알림 피로도데이터 자산 가치권장 용도
단순 실시간 전송수집하는 족족 메신저로 전송극도로 극심 (스팸화)전혀 없음 (단발성 휘발)1회성 테스트 외 비추천
차분 감지 알림직전 데이터와 비교해 변동 시에만 트리거최적 (진짜 이벤트만 수신)핵심 변동점 완벽 추적실시간 특가/재고 감시에 필수
시계열 스냅샷 적재모든 시점의 데이터를 영구 DB에 누적일정 주기 요약 리포트최고 (가격 추세 통계 분석)최저가 추이 및 데이터 자산화
차분 감지 + 시계열 누적의 결합

프로덕션 봇에서는 차분 감지를 통해 사용자의 스마트폰으로는 긴급 변동 알림만 쏘고, 백그라운드에서는 시계열 누적을 통해 /app/data/prices.db에 1시간 단위 스냅샷을 쌓아두는 하이브리드 구조가 가장 이상적입니다.


차분 감지 엔진 구현 원리

[ 새 수집 데이터 도착: 상품 A = 45,000원 ]
                │
                ▼
┌───────────────────────────────────────────────┐
│ SQLite DB 직전 기록 조회                      │
│ SELECT price, in_stock FROM products WHERE id=A │
└───────────────────────────────────────────────┘
                │
         ┌──────┴────────────────────────┐
      (변동 없음)                      (변동 감지!)
         │                               │
         ▼                               ▼
    [알림 생략]                 ┌─────────────────────────────────┐
  (스팸 방지 100%)              │ 1. DB 가격 갱신 (45,000원)      │
                                │ 2. 시계열 히스토리 로그 INSERT  │
                                │ 3. 텔레그램/디스코드 특가 발송  │
                                │    "🚨 50,000원 ➔ 45,000원 할인!"│
                                └─────────────────────────────────┘

완성형 프로덕션 파이프라인 모듈 (price_monitor.py)

SQLite WAL 모드 영구 보존, 차분 감지 알림, 일일 CSV 자동 내보내기가 모두 포함된 완성형 파이썬 스크립트입니다.

price_monitor.py
import os
import sqlite3
import csv
import logging
from datetime import datetime
from pathlib import Path
import requests
 
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("PricePipeline")
 
# 영구 디스크 경로
DATA_DIR = Path("/app/data")
DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = DATA_DIR / "monitoring.db"
CSV_DIR = DATA_DIR / "csv_exports"
CSV_DIR.mkdir(parents=True, exist_ok=True)
 
DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")
 
def init_db():
    with sqlite3.connect(DB_PATH) as conn:
        conn.execute("PRAGMA journal_mode = WAL;")
        # 최신 상태 테이블
        conn.execute("""
            CREATE TABLE IF NOT EXISTS product_latest (
                product_id TEXT PRIMARY KEY,
                product_name TEXT,
                price INTEGER,
                in_stock INTEGER,
                updated_at TIMESTAMP
            );
        """)
        # 시계열 가격 변동 이력 테이블
        conn.execute("""
            CREATE TABLE IF NOT EXISTS price_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                product_id TEXT,
                price INTEGER,
                in_stock INTEGER,
                recorded_at TIMESTAMP
            );
        """)
        conn.commit()
 
def process_scraped_product(product_id: str, name: str, price: int, in_stock: bool):
    """차분 감지 및 데이터 저장 엔진"""
    with sqlite3.connect(DB_PATH) as conn:
        cursor = conn.cursor()
        
        # 1. 직전 상태 조회
        cursor.execute("SELECT price, in_stock FROM product_latest WHERE product_id = ?", (product_id,))
        previous = cursor.fetchone()
        
        is_changed = False
        change_reason = ""
 
        if previous is None:
            # 신규 상품 최초 등록
            is_changed = True
            change_reason = "신규 상품 추적 시작"
        else:
            old_price, old_stock = previous
            current_stock_int = 1 if in_stock else 0
            
            # 가격 하락 감지
            if price < old_price:
                diff = old_price - price
                rate = (diff / old_price) * 100
                is_changed = True
                change_reason = f"📉 가격 인하! {old_price:,}원 ➔ {price:,}원 ({rate:.1f}% 할인)"
            # 품절 해제(재입고) 감지
            elif old_stock == 0 and current_stock_int == 1:
                is_changed = True
                change_reason = "🎉 품절 해제! 지금 구매 가능"
            # 가격 인상
            elif price > old_price:
                is_changed = True
                change_reason = f"📈 가격 인상 ({old_price:,}원 ➔ {price:,}원)"
 
        # 2. 상태 갱신 및 시계열 로그 기록
        now_str = datetime.now().isoformat()
        cursor.execute("""
            INSERT INTO product_latest (product_id, product_name, price, in_stock, updated_at)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT(product_id) DO UPDATE SET
                price = excluded.price,
                in_stock = excluded.in_stock,
                updated_at = excluded.updated_at;
        """, (product_id, name, price, 1 if in_stock else 0, now_str))
 
        cursor.execute("""
            INSERT INTO price_history (product_id, price, in_stock, recorded_at)
            VALUES (?, ?, ?, ?);
        """, (product_id, price, 1 if in_stock else 0, now_str))
        conn.commit()
 
        # 3. 변동이 있을 때만 정밀 웹훅 알림 발송
        if is_changed:
            logger.info(f"변동 감지 [{name}]: {change_reason}")
            send_discord_alert(name, change_reason, price)
 
def send_discord_alert(name: str, reason: str, price: int):
    if not DISCORD_WEBHOOK_URL:
        return
    payload = {
        "content": f"**[DashCloud 알림] {name}**\n{reason}\n현재가: **{price:,}원**",
        "username": "가격 감시 봇"
    }
    try:
        requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=5)
    except Exception as e:
        logger.error(f"디스코드 웹훅 발송 실패: {e}")
 
def export_daily_csv():
    """매일 자정 시계열 데이터를 CSV로 일자별 내보내기"""
    today_str = datetime.now().strftime("%Y%m%d")
    csv_file = CSV_DIR / f"price_history_{today_str}.csv"
 
    with sqlite3.connect(DB_PATH) as conn:
        cursor = conn.cursor()
        cursor.execute("""
            SELECT p.product_name, h.price, h.in_stock, h.recorded_at 
            FROM price_history h
            JOIN product_latest p ON h.product_id = p.product_id
            ORDER BY h.recorded_at DESC;
        """)
        rows = cursor.fetchall()
 
    with open(csv_file, "w", newline="", encoding="utf-8-sig") as f: # utf-8-sig: 엑셀 한글 깨짐 방지
        writer = csv.writer(f)
        writer.writerow(["상품명", "가격(원)", "재고상태", "수집시각"])
        writer.writerows(rows)
 
    logger.info(f"일일 CSV 데이터 내보내기 완료: {csv_file}")
 
if __name__ == "__main__":
    init_db()
    # 테스트 시뮬레이션
    process_scraped_product("PROD-101", "에어팟 프로 2세대", 299000, True)
    # 가격 인하 시뮬레이션
    process_scraped_product("PROD-101", "에어팟 프로 2세대", 269000, True)
    export_daily_csv()

대시보드 웹 파일 관리자에서 엑셀 파일 다운로드

수집된 데이터는 DashCloud 웹 대시보드에서 클릭 한 번으로 내 컴퓨터로 다운로드할 수 있습니다:

  1. DashCloud 대시보드의 프로젝트 상세 페이지로 들어갑니다.
  2. 상단 탭 중 파일 메뉴를 클릭합니다.
  3. /app/data/csv_exports/ 폴더를 탐색합니다.
  4. 원하는 날짜의 price_history_YYYYMMDD.csv 파일을 클릭하면, 브라우저가 즉시 다운로드하여 Microsoft Excel이나 Google Sheets에서 즉시 열어볼 수 있습니다.
  5. utf-8-sig 인코딩이 적용되어 있으므로 엑셀을 열었을 때 한글 상품명이 깨지지 않고 선명하게 표시됩니다.

다음 추천 문서