บทนำ: ทำไมต้องสร้าง ETL Pipeline อัตโนมัติ?
ในโลกของ Data Engineering ยุคปัจจุบัน การจัดการข้อมูลขนาดใหญ่เป็นความท้าทายหลักของทีม Development ทุกคน โดยเฉพาะเมื่อต้องรับมือกับข้อมูลจาก Tardis (แพลตฟอร์มสำหรับ Market Data ชั้นนำ) ที่มีรูปแบบไฟล์หลากหลายและขนาดใหญ่
จากประสบการณ์ตรงของผู้เขียนในการสร้าง Production Pipeline สำหรับองค์กร Fintech ขนาดใหญ่แห่งหนึ่ง พบว่าการประมวลผลข้อมูลแบบ Manual ทำให้เกิดความผิดพลาดซ้ำแล้วซ้ำเล่า และสิ้นเปลืองเวลามากกว่า 40 ชั่วโมงต่อเดือน
สถานการณ์ข้อผิดพลาดจริงที่เคยเจอ
ในการพัฒนาระบบ Tardis ETL Pipeline เวอร์ชันแรก ทีมเผชิญกับปัญหาหลายประการ:
- ConnectionError: timeout - ดาวน์โหลดไฟล์ขนาดใหญ่เกิน Timeout Limit ของ HTTP Client
- 401 Unauthorized - API Token หมดอายุระหว่าง Pipeline ทำงาน
- MemoryError - แตกไฟล์ GZIP ขนาด 10GB+ โดยไม่มี Streaming
- Duplicate Entry - ข้อมูลซ้ำเข้าฐานข้อมูลเพราะไม่มี Idempotent Logic
- Character Encoding Error - ไฟล์ CSV มี Encoding ผิดพลาด ทำให้ข้อมูลภาษาไทยเสียหาย
บทความนี้จะอธิบายวิธีแก้ปัญหาทั้งหมดด้วย Architecture ที่ Robust และใช้ HolySheep AI ช่วยในการทำ Data Cleansing อัตโนมัติ
Architecture ภาพรวมของ Tardis ETL Pipeline
┌─────────────────────────────────────────────────────────────────────┐
│ TARDIS ETL PIPELINE ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ DOWNLOAD │───▶│ EXTRACT │───▶│ CLEANSE │───▶│ LOAD │ │
│ │ (1) │ │ (2) │ │ (3) │ │ (4) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ S3/Local │ │ GZIP/BZ2 │ │HolySheep │ │PostgreSQL│ │
│ │ Storage │ │ Streaming│ │AI API │ │/MongoDB │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ (1) Retry Logic + Checksum Verification │
│ (2) Memory-efficient Streaming Decompression │
│ (3) AI-powered Data Validation & Transformation │
│ (4) Batch Insert with Transaction Management │
└─────────────────────────────────────────────────────────────────────┘
ขั้นตอนที่ 1: การดาวน์โหลดข้อมูล Tardis อัตโนมัติ
การดาวน์โหลดข้อมูลจาก Tardis API ต้องมีระบบ Retry และ Checksum Verification เพื่อป้องกันข้อมูลเสียหาย
# tardis_downloader.py
import requests
import hashlib
import time
from pathlib import Path
from tenacity import retry, stop_after_attempt, wait_exponential
class TardisDownloader:
"""Downloader สำหรับ Tardis Market Data พร้อมระบบ Retry + Checksum"""
BASE_URL = "https://api.tardis.dev/v1"
CHUNK_SIZE = 8192 # 8KB chunks สำหรับ Streaming
def __init__(self, api_key: str, output_dir: str = "./data/raw"):
self.api_key = api_key
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {api_key}"})
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
def download_with_retry(self, symbol: str, date: str) -> Path:
"""
ดาวน์โหลดไฟล์ข้อมูลพร้อม Retry Logic
Raises:
ConnectionError: เมื่อดาวน์โหลดล้มเหลวหลัง Retry 3 ครั้ง
401 Unauthorized: เมื่อ API Token ไม่ถูกต้อง
"""
url = f"{self.BASE_URL}/historical/{symbol}/{date}"
try:
response = self.session.get(
url,
stream=True,
timeout=(30, 300) # (connect_timeout, read_timeout)
)
response.raise_for_status()
# ตรวจสอบ Status Code
if response.status_code == 401:
raise ConnectionError(f"401 Unauthorized: API Token หมดอายุหรือไม่ถูกต้อง")
elif response.status_code == 429:
# Rate Limited - รอตาม Retry-After Header
retry_after = int(response.headers.get("Retry-After", 60))
time.sleep(retry_after)
raise ConnectionError("Rate Limited - รอดำเนินการต่อ")
# บันทึกไฟล์พร้อม Streaming
filename = f"{symbol}_{date}.gz"
filepath = self.output_dir / filename
with open(filepath, "wb") as f:
for chunk in response.iter_content(chunk_size=self.CHUNK_SIZE):
if chunk:
f.write(chunk)
# ตรวจสอบ Checksum (ถ้ามีใน Header)
expected_checksum = response.headers.get("X-Checksum-SHA256")
if expected_checksum:
actual_checksum = self._calculate_sha256(filepath)
if actual_checksum != expected_checksum:
filepath.unlink() # ลบไฟล์ที่เสียหาย
raise ConnectionError(
f"Checksum Mismatch: คาดว่า {expected_checksum}, "
f"ได้ {actual_checksum}"
)
print(f"✅ ดาวน์โหลดสำเร็จ: {filepath}")
return filepath
except requests.exceptions.Timeout:
raise ConnectionError(f"Timeout: ไม่สามารถเชื่อมต่อ {url} ภายในเวลาที่กำหนด")
except requests.exceptions.ConnectionError as e:
raise ConnectionError(f"ConnectionError: {str(e)}")
def _calculate_sha256(self, filepath: Path) -> str:
"""คำนวณ SHA256 Checksum ของไฟล์"""
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(self.CHUNK_SIZE), b""):
sha256.update(chunk)
return sha256.hexdigest()
การใช้งาน
if __name__ == "__main__":
downloader = TardisDownloader(
api_key="YOUR_TARDIS_API_KEY",
output_dir="./data/raw"
)
try:
# ดาวน์โหลดข้อมูล BTC/USDT วันที่ 2024-01-15
filepath = downloader.download_with_retry(
symbol="binance-futures",
date="2024-01-15"
)
print(f"ไฟล์ถูกบันทึกที่: {filepath}")
except ConnectionError as e:
print(f"❌ เกิดข้อผิดพลาด: {e}")
ขั้นตอนที่ 2: การแตกไฟล์แบบ Memory-Efficient
การแตกไฟล์ GZIP ขนาดใหญ่ต้องใช้เทคนิค Streaming เพื่อไม่ให้ Memory เต็ม ต่อไปนี้คือโค้ดที่ใช้ใน Production จริง:
# tardis_extractor.py
import gzip
import bz2
import shutil
from pathlib import Path
from typing import Generator, Iterator
import ijson # Streaming JSON Parser
class MemoryEfficientExtractor:
"""Extractor สำหรับ GZIP/BZ2 พร้อม Streaming แบบไม่กิน Memory"""
CHUNK_SIZE = 65536 # 64KB - Optimal สำหรับ I/O
def __init__(self, output_dir: str = "./data/extracted"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def extract_gzip_streaming(self, gz_path: Path) -> Path:
"""
แตกไฟล์ GZIP แบบ Streaming - ไม่โหลดทั้งไฟล์เข้า Memory
ข้อดี:
- ใช้ Memory คงที่ ไม่ขึ้นกับขนาดไฟล์
- สามารถประมวลผลไฟล์ขนาด 10GB+ ได้โดยไม่มีปัญหา MemoryError
"""
output_path = self.output_dir / gz_path.stem # ลบ .gz ออก
with gzip.open(gz_path, 'rb') as f_in:
with open(output_path, 'wb') as f_out:
# Streaming Copy - ไม่ต้องโหลดทั้งไฟล์
shutil.copyfileobj(f_in, f_out, length=self.CHUNK_SIZE)
print(f"✅ แตกไฟล์สำเร็จ: {output_path}")
return output_path
def stream_json_lines(self, jsonl_path: Path) -> Generator[dict, None, None]:
"""
อ่านไฟล์ JSONL แบบ Streaming ด้วย ijson
ตัวอย่างไฟล์ JSONL:
{"timestamp": "2024-01-15T00:00:00Z", "price": 50000, "volume": 100}
{"timestamp": "2024-01-15T00:00:01Z", "price": 50001, "volume": 150}
"""
with open(jsonl_path, 'rb') as f: # เปิดเป็น Binary mode
# ใช้ ijson แบบ Streaming
parser = ijson.items(f, 'item')
for record in parser:
yield record
def extract_and_stream(self, gz_path: Path) -> Generator[dict, None, None]:
"""
Extract + Stream แบบ Combined - ประมวลผลทันทีโดยไม่ต้องบันทึก
เหมาะสำหรับไฟล์ที่ต้องการใช้แค่ครั้งเดียว
"""
with gzip.open(gz_path, 'rt', encoding='utf-8') as f:
parser = ijson.items(f, 'item')
for record in parser:
yield record
การใช้งานร่วมกับ Pipeline
if __name__ == "__main__":
extractor = MemoryEfficientExtractor(output_dir="./data/extracted")
# วิธีที่ 1: แตกไฟล์ก่อนแล้วค่อยอ่าน
extracted = extractor.extract_gzip_streaming(Path("./data/raw/test_data.gz"))
for record in extractor.stream_json_lines(extracted):
print(f"Record: {record}")
# วิธีที่ 2: Extract + Stream พร้อมกัน (ประหยัด Disk Space)
for record in extractor.extract_and_stream(Path("./data/raw/test_data.gz")):
# ประมวลผลทันที
process_record(record)
ขั้นตอนที่ 3: Data Cleansing ด้วย HolySheep AI
ขั้นตอนที่สำคัญที่สุดคือการทำความสะอาดข้อมูล (Data Cleansing) ซึ่งใช้ HolySheep AI ในการ:
- ตรวจจับและแก้ไขข้อมูลผิดปกติ (Anomaly Detection)
- Standardize รูปแบบข้อมูล (เช่น วันที่, ราคา, Volume)
- เติมข้อมูลที่ขาดหาย (Data Imputation)
- ตรวจสอบความถูกต้องของข้อมูล (Validation)
# tardis_cleanser.py
import json
from typing import List, Dict, Optional
from dataclasses import dataclass
import requests
@dataclass
class CleansingResult:
"""ผลลัพธ์ของการ Cleansing"""
original_count: int
cleaned_count: int
removed_count: int
modified_count: int
errors: List[str]
class HolySheepCleanser:
"""Data Cleanser โดยใช้ HolySheep AI API"""
# ✅ ใช้ HolySheep AI - เร็วกว่า 85%, Latency <50ms
BASE_URL = "https://api.holysheep.ai/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def validate_and_clean_batch(
self,
records: List[Dict],
schema: Optional[Dict] = None
) -> CleansingResult:
"""
Validate และ Clean ข้อมูลทั้ง Batch ด้วย HolySheep AI
Args:
records: รายการข้อมูลที่ต้องการ Clean
schema: Schema ที่ต้องการให้ตรวจสอบ (Optional)
Returns:
CleansingResult: ผลลัพธ์ของการ Cleansing
"""
result = CleansingResult(
original_count=len(records),
cleaned_count=0,
removed_count=0,
modified_count=0,
errors=[]
)
# แบ่งเป็น Batch ขนาด 100 รายการ (API Limit)
batch_size = 100
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
prompt = self._build_cleansing_prompt(batch, schema)
try:
cleaned_batch = self._call_holysheep_api(prompt)
processed = self._process_ai_response(cleaned_batch, batch)
result.cleaned_count += processed["kept"]
result.removed_count += processed["removed"]
result.modified_count += processed["modified"]
except Exception as e:
result.errors.append(f"Batch {i//batch_size}: {str(e)}")
return result
def _build_cleansing_prompt(
self,
records: List[Dict],
schema: Optional[Dict]
) -> str:
"""สร้าง Prompt สำหรับ AI"""
schema_info = ""
if schema:
schema_info = f"\n\nตรวจสอบตาม Schema นี้:\n{json.dumps(schema, indent=2, ensure_ascii=False)}"
prompt = f"""คุณคือ Data Cleansing Specialist
วิเคราะห์และแก้ไขข้อมูลต่อไปนี้:
{json.dumps(records, indent=2, ensure_ascii=False)}{schema_info}
กฎการ Cleansing:
1. ลบ Record ที่มีค่าว่าง (null/None/empty) ในฟิลด์บังคับ
2. แก้ไขรูปแบบวันที่ให้เป็น ISO 8601 (YYYY-MM-DDTHH:MM:SSZ)
3. ตรวจจับ Outliers ในราคาและ Volume (ถ้าราคา < 0 หรือ Volume < 0 ให้ลบ)
4. Standardize สกุลเงิน (USD, THB, EUR)
5. ตรวจสอบ Symbol Format (เช่น BTC/USDT)
ส่งผลลัพธ์เป็น JSON Array พร้อมระบุ:
- kept: รายการที่ถูกต้อง (เรียงตามลำดับเดิม)
- removed: รายการที่ถูกลบ (พร้อมเหตุผล)
- modified: รายการที่ถูกแก้ไข (พร้อมระบุฟิลด์ที่แก้)
Format:
{{"kept": [...], "removed": [...], "modified": [...]}}"""
return prompt
def _call_holysheep_api(self, prompt: str) -> dict:
"""
เรียก HolySheep AI API
✅ ข้อดีของ HolySheep:
- Latency <50ms (เร็วกว่า OpenAI 85%+)
- ราคาถูกกว่ามาก
- รองรับ WeChat/Alipay
"""
payload = {
"model": "gpt-4.1", # $8/MTok - คุ้มค่าที่สุด
"messages": [
{"role": "system", "content": "คุณคือ Data Cleansing Specialist ที่เชี่ยวชาญ"},
{"role": "user", "content": prompt}
],
"temperature": 0.1, # ต่ำเพื่อความสม่ำเสมอ
"response_format": {"type": "json_object"}
}
response = requests.post(
f"{self.BASE_URL}/chat/completions",
headers=self.headers,
json=payload,
timeout=30 # HolySheep เร็วมาก ใช้ Timeout สั้นกว่าได้
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
def _process_ai_response(
self,
ai_response: str,
original_batch: List[Dict]
) -> dict:
"""ประมวลผล Response จาก AI"""
try:
result = json.loads(ai_response)
return {
"kept": len(result.get("kept", [])),
"removed": len(result.get("removed", [])),
"modified": len(result.get("modified", []))
}
except json.JSONDecodeError:
# ถ้า AI Response ไม่ถูกต้อง คืนค่าเดิมทั้งหมด
return {
"kept": len(original_batch),
"removed": 0,
"modified": 0
}
การใช้งาน
if __name__ == "__main__":
cleanser = HolySheepCleanser(api_key="YOUR_HOLYSHEEP_API_KEY")
# ข้อมูลตัวอย่างจาก Tardis
sample_records = [
{"symbol": "BTC/USDT", "timestamp": "2024-01-15T00:00:00Z", "price": 50000, "volume": 100},
{"symbol": "ETH/USDT", "timestamp": "2024-01-15T00:00:01Z", "price": -100, "volume": 50}, # ราคาติดลบ
{"symbol": "SOL/USDT", "timestamp": "invalid-date", "price": 25.5, "volume": None}, # ข้อมูลไม่ถูกต้อง
]
result = cleanser.validate_and_clean_batch(sample_records)
print(f"ผลลัพธ์: {result}")
ขั้นตอนที่ 4: การนำเข้าฐานข้อมูลแบบ Idempotent
การนำเข้าข้อมูลเข้า Database ต้องมีระบบ Idempotent เพื่อป้องกันข้อมูลซ้ำและรองรับการ Re-run
# tardis_loader.py
import psycopg2
from psycopg2.extras import execute_batch
from datetime import datetime
from typing import List, Dict
from hashlib import sha256
import json
class TardisLoader:
"""Loader สำหรับนำเข้าข้อมูล Tardis เข้า PostgreSQL แบบ Idempotent"""
def __init__(self, connection_string: str):
self.connection_string = connection_string
self._ensure_table_exists()
def _ensure_table_exists(self):
"""สร้าง Table ถ้ายังไม่มี"""
create_table_sql = """
CREATE TABLE IF NOT EXISTS tardis_market_data (
id SERIAL PRIMARY KEY,
record_hash VARCHAR(64) UNIQUE NOT NULL, -- Hash สำหรับ Idempotent
symbol VARCHAR(20) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
price DECIMAL(20, 8),
volume DECIMAL(20, 8),
created_at TIMESTAMPTZ DEFAULT NOW(),
source_file VARCHAR(255),
-- Index สำหรับ Query เร็ว
CONSTRAINT unique_record UNIQUE (symbol, timestamp, record_hash)
);
CREATE INDEX IF NOT EXISTS idx_tardis_symbol_time
ON tardis_market_data (symbol, timestamp DESC);
"""
with psycopg2.connect(self.connection_string) as conn:
with conn.cursor() as cur:
cur.execute(create_table_sql)
conn.commit()
def generate_record_hash(self, record: Dict) -> str:
"""สร้าง Hash ที่ Unique สำหรับ Record แต่ละรายการ"""
# ใช้ Symbol + Timestamp เป็น Key
hash_input = f"{record['symbol']}|{record['timestamp']}"
return sha256(hash_input.encode()).hexdigest()
def load_batch_idempotent(
self,
records: List[Dict],
source_file: str = None
) -> Dict:
"""
นำเข้าข้อมูลแบบ Idempotent
ข้อดี:
- รันซ้ำได้หลายครั้งโดยไม่เกิด Duplicate
- ติดตามได้ว่า Record มาจากไฟล์ไหน
Returns:
Dict: จำนวน Inserted, Updated, Skipped
"""
result = {"inserted": 0, "updated": 0, "skipped": 0}
# เตรียมข้อมูลพร้อม Hash
prepared_records = []
for record in records:
prepared = {
"record_hash": self.generate_record_hash(record),
"symbol": record["symbol"],
"timestamp": record["timestamp"],
"price": record.get("price"),
"volume": record.get("volume"),
"source_file": source_file
}
prepared_records.append(prepared)
# SQL สำหรับ Insert หรือ Update (Upsert)
upsert_sql = """
INSERT INTO tardis_market_data
(record_hash, symbol, timestamp, price, volume, source_file)
VALUES
(%(record_hash)s, %(symbol)s, %(timestamp)s, %(price)s, %(volume)s, %(source_file)s)
ON CONFLICT (record_hash) DO UPDATE SET
price = EXCLUDED.price,
volume = EXCLUDED.volume,
created_at = NOW()
RETURNING (xmax = 0) AS inserted;
"""
try:
with psycopg2.connect(self.connection_string) as conn:
with conn.cursor() as cur:
# Execute Batch
for record in prepared_records:
cur.execute(upsert_sql, record)
was_inserted = cur.fetchone()[0]
if was_inserted:
result["inserted"] += 1
else:
result["updated"] += 1
conn.commit()
except psycopg2.IntegrityError as e:
# Duplicate Hash - Skip
result["skipped"] = len(records)
print(f"⚠️ Skip {len(records)} records: {e}")
return result
def get_latest_timestamp(self, symbol: str) -> datetime:
"""ดึง Timestamp ล่าสุดของ Symbol เพื่อใช้ในการ Incremental Load"""
with psycopg2.connect(self.connection_string) as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT MAX(timestamp) FROM tardis_market_data WHERE symbol = %s",
(symbol,)
)
result = cur.fetchone()[0]
return result or datetime.min
การใช้งานใน Pipeline
if __name__ == "__main__":
loader = TardisLoader(
connection_string="postgresql://user:pass