- Introduce app/ package with config, services (storage, notifications), API server, and crawler modules - Add BaseCrawler and BarronsCrawler; extract notifications and storage - Keep enhanced_crawler.py as back-compat entry delegating to app.runner - Add template crawler for future sites - Update README with new structure and usage - Extend .env.template with DATA_DIR/LOG_DIR options
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
from datetime import datetime
|
|
import hashlib
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from typing import List, Dict, Optional
|
|
|
|
import requests
|
|
|
|
from app.config import EmailConfig
|
|
|
|
|
|
def format_email_body(new_picks: List[Dict]) -> str:
|
|
body = f"發現 {len(new_picks)} 條新的股票推薦:\n\n"
|
|
for pick in new_picks:
|
|
body += f"📊 {pick['title']}\n"
|
|
if pick.get('link'):
|
|
body += f"🔗 {pick['link']}\n"
|
|
body += f"🕒 {pick.get('scraped_at', datetime.now().isoformat())}\n"
|
|
body += "-" * 60 + "\n"
|
|
return body
|
|
|
|
|
|
def send_email(new_picks: List[Dict], cfg: EmailConfig) -> None:
|
|
msg = MIMEMultipart()
|
|
msg['From'] = cfg.from_email
|
|
msg['To'] = cfg.to_email
|
|
msg['Subject'] = f"📈 Barron's 新股票推薦 ({len(new_picks)}條)"
|
|
msg.attach(MIMEText(format_email_body(new_picks), 'plain', 'utf-8'))
|
|
|
|
if cfg.smtp_security == 'ssl':
|
|
server = smtplib.SMTP_SSL(cfg.smtp_server, cfg.smtp_port)
|
|
else:
|
|
server = smtplib.SMTP(cfg.smtp_server, cfg.smtp_port)
|
|
server.ehlo()
|
|
if cfg.smtp_security == 'starttls':
|
|
server.starttls()
|
|
server.ehlo()
|
|
|
|
server.login(cfg.username, cfg.password)
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
|
|
def send_webhook(new_picks: List[Dict], url: str) -> None:
|
|
message = f"🚨 發現 {len(new_picks)} 條新的 Barron's 股票推薦!\n\n"
|
|
for pick in new_picks[:5]:
|
|
message += f"• {pick['title']}\n"
|
|
if pick.get('link'):
|
|
message += f" {pick['link']}\n"
|
|
message += "\n"
|
|
payload = {"text": message}
|
|
requests.post(url, json=payload, timeout=10)
|
|
|
|
|
|
def send_discord(new_picks: List[Dict], webhook: str) -> None:
|
|
embed = {
|
|
"title": "📈 Barron's 新股票推薦",
|
|
"description": f"發現 {len(new_picks)} 條新推薦",
|
|
"color": 0x00ff00,
|
|
"fields": [],
|
|
}
|
|
for pick in new_picks[:5]:
|
|
embed["fields"].append({
|
|
"name": pick['title'][:256],
|
|
"value": (pick.get('link') or '無連結')[:1024],
|
|
"inline": False,
|
|
})
|
|
requests.post(webhook, json={"embeds": [embed]}, timeout=10)
|
|
|
|
|
|
def build_test_pick() -> Dict:
|
|
return {
|
|
'title': f"[測試] Barron's 通知發送 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
'link': 'https://example.com/test',
|
|
'scraped_at': datetime.now().isoformat(),
|
|
'hash': hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8],
|
|
}
|
|
|