Files
ewoooc/services/exporter.py

143 lines
6.6 KiB
Python

import os
import csv
import pandas as pd
from datetime import datetime
from config import EXCEL_EXPORT_DIR
from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url
class Exporter:
"""
報表匯出管理器
負責將數據轉換為 CSV/Excel 格式並儲存至 exports 目錄
"""
def __init__(self):
self.export_dir = EXCEL_EXPORT_DIR
if not os.path.exists(self.export_dir):
try:
os.makedirs(self.export_dir)
except OSError:
pass
def _write_csv(self, filename, headers, rows):
"""內部方法:寫入 CSV 檔案 (含 BOM 以支援 Excel 中文顯示)"""
filepath = os.path.join(self.export_dir, filename)
try:
with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
writer.writerow(headers)
writer.writerows(rows)
return filepath
except Exception as e:
print(f"❌ Export error: {e}")
return None
def _safe_product_url(self, product):
sku = str(getattr(product, 'i_code', '') or '')
return normalize_momo_product_url(getattr(product, 'url', None), sku) or build_momo_product_url(sku)
def generate_all_categories_report(self):
"""匯出所有分類商品快照"""
# 由於 app.py 呼叫此方法時未傳入數據,需自行查詢資料庫
from database.manager import DatabaseManager
from database.models import PriceRecord
from sqlalchemy import func
db = DatabaseManager()
session = db.get_session()
try:
# 查詢每個商品最新價格
subq = session.query(func.max(PriceRecord.id).label('max_id')).group_by(PriceRecord.product_id).subquery()
records = session.query(PriceRecord).join(subq, PriceRecord.id == subq.c.max_id).all()
rows = []
for r in records:
rows.append([r.product.category, r.product.name, r.price, self._safe_product_url(r.product), r.timestamp.strftime('%Y-%m-%d %H:%M')])
filename = f"All_Products_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL', 'Last Update'], rows)
finally:
session.close()
def generate_price_change_report(self, items):
"""匯出價格異動商品"""
rows = []
for item in items:
rec = item['record']
diff = item['yesterday_diff']
rows.append([rec.product.category, rec.product.name, rec.price, diff, self._safe_product_url(rec.product)])
filename = f"Price_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
return self._write_csv(filename, ['Category', 'Name', 'Price', 'Change', 'URL'], rows)
def generate_low_price_report(self):
return self._write_csv(f"Low_Price_{datetime.now().strftime('%Y%m%d')}.csv", ['Message'], [['Feature not fully implemented']])
def generate_custom_report(self, items, title):
rows = []
for item in items:
rec = item['record']
rows.append([rec.product.category, rec.product.name, rec.price, self._safe_product_url(rec.product)])
filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL'], rows)
def generate_delisted_report(self, items, title):
rows = []
for item in items:
p = item['product']
price = item['last_price']
rows.append([p.category, p.name, price, self._safe_product_url(p), "DELISTED"])
filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
return self._write_csv(filename, ['Category', 'Name', 'Last Price', 'URL', 'Status'], rows)
def generate_all_products_excel(self, items):
"""匯出所有商品至 Excel (依分類分頁)"""
data = []
for item in items:
rec = item['record']
data.append({
'分類': rec.product.category,
'商品名稱': rec.product.name,
'價格': rec.price,
'連結': self._safe_product_url(rec.product),
'更新時間': rec.timestamp.strftime('%Y-%m-%d %H:%M')
})
filename = f"MOMO_All_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
filepath = os.path.join(self.export_dir, filename)
df = pd.DataFrame(data)
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
for cat, group in df.groupby('分類'):
sheet_name = str(cat)[:30].replace('/', '_').replace(':', '') # 處理 Excel 工作表名稱限制
group.to_excel(writer, sheet_name=sheet_name, index=False)
return filepath
def generate_changes_excel(self, increase, decrease):
"""匯出漲跌商品至 Excel (兩個分頁)"""
filename = f"MOMO_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
filepath = os.path.join(self.export_dir, filename)
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
# 漲價
data_inc = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '漲幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in increase]
pd.DataFrame(data_inc).to_excel(writer, sheet_name='漲價商品', index=False)
# 跌價
data_dec = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '跌幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in decrease]
pd.DataFrame(data_dec).to_excel(writer, sheet_name='跌價商品', index=False)
return filepath
def generate_delisted_excel(self, delisted_items):
"""匯出下架商品至 Excel"""
filename = f"MOMO_Delisted_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
filepath = os.path.join(self.export_dir, filename)
data = [{'分類': i['product'].category, '商品名稱': i['product'].name, '最後價格': i['last_price'], '連結': self._safe_product_url(i['product']), '狀態': '下架'} for i in delisted_items]
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
pd.DataFrame(data).to_excel(writer, sheet_name='下架商品', index=False)
return filepath