Files
stocks-query/scripts/yahoo-dividend-browserless.py
Timmy 7d72aed984 restore and rebuild all stock query scripts
Recover 7 existing scripts from remote repo and rebuild 5 missing ones:
- twse-margin-trading.py (融資融券)
- twse-day-trading.py (當沖交易)
- twse-foreign-holdings.py (外資持股)
- yahoo-dividend-browserless.py (股利資訊)
- yahoo-broker-trading-browserless.py (主力券商進出)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:07:31 +08:00

191 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
用 Browserless 抓 Yahoo 股利政策頁,輸出精簡摘要
用法:
python3 skills/stocks-query/scripts/yahoo-dividend-browserless.py 2330
python3 skills/stocks-query/scripts/yahoo-dividend-browserless.py 0050
環境變數(可覆蓋):
BROWSERLESS_ENDPOINT=http://192.168.42.124:13000
BROWSERLESS_TOKEN=6R0W53R135510
"""
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path
def load_dotenv() -> None:
"""載入 .env優先目前目錄其次 workspace 根目錄)。"""
candidates = [
Path.cwd() / ".env",
Path(__file__).resolve().parents[3] / ".env",
]
for env_path in candidates:
if not env_path.exists():
continue
for raw in env_path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip()
v = v.strip().strip('"').strip("'")
if k and k not in os.environ:
os.environ[k] = v
break
load_dotenv()
ENDPOINT = os.getenv("BROWSERLESS_ENDPOINT")
TOKEN = os.getenv("BROWSERLESS_TOKEN")
if not ENDPOINT or not TOKEN:
raise RuntimeError("缺少必要環境變數BROWSERLESS_ENDPOINT / BROWSERLESS_TOKEN")
STOCK_NO = sys.argv[1] if len(sys.argv) > 1 else "2330"
def fetch_html(stock_no: str) -> str:
url = f"https://tw.stock.yahoo.com/quote/{stock_no}.TW/dividend"
api = f"{ENDPOINT}/content?token={TOKEN}"
payload = {
"url": url,
"gotoOptions": {"waitUntil": "domcontentloaded", "timeout": 60000},
}
req = urllib.request.Request(
api,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=90) as resp:
text = resp.read().decode("utf-8", errors="replace")
status = resp.getcode()
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else str(e)
raise RuntimeError(f"Browserless HTTP {e.code}: {body[:200]}") from e
if status != 200:
raise RuntimeError(f"Browserless HTTP {status}: {text[:200]}")
if "TimeoutError" in text:
raise RuntimeError("Browserless 導航逾時TimeoutError")
return text
def pick(html: str, pattern: str, default: str = "-") -> str:
m = re.search(pattern, html, flags=re.S)
return m.group(1) if m else default
def parse_dividend(html: str, stock_no: str) -> dict:
name = pick(html, r'property="og:title" content="([^"(]+)\([0-9]+\.TW\)', "").strip()
if not name:
name = pick(html, r"<title>([^<(]+)\([0-9]+\.TW\)", "-").strip()
# 殖利率
dividend_yield = pick(html, r">現金殖利率</span><span[^>]*>([\d.]+%?)")
# 年度股利統計(從 JSON-like 嵌入或 table 抓取)
# 嘗試從頁面的 script data 抓股利歷史
rows = []
# 方法1抓 dividendData JSON
json_match = re.search(r'"dividendData"\s*:\s*(\[[\s\S]*?\])\s*[,}]', html)
if json_match:
try:
raw_data = json.loads(json_match.group(1))
for item in raw_data[:5]: # 最近 5 年
year = item.get("year", item.get("formattedYear", "-"))
cash = item.get("cashDividend", item.get("cash", 0))
stock = item.get("stockDividend", item.get("stock", 0))
total = item.get("totalDividend", 0)
if total == 0:
try:
total = float(cash) + float(stock)
except (ValueError, TypeError):
total = "-"
rows.append({
"year": str(year),
"cash": str(cash),
"stock": str(stock),
"total": str(total),
})
except (json.JSONDecodeError, KeyError):
pass
# 方法2用 regex 抓 table rows
if not rows:
# 常見結構:年度、現金股利、股票股利、合計
row_pattern = re.compile(
r'<tr[^>]*>.*?<td[^>]*>(1\d{2,3})</td>'
r'.*?<td[^>]*>([\d.]+)</td>'
r'.*?<td[^>]*>([\d.]+)</td>'
r'.*?<td[^>]*>([\d.]+)</td>',
re.S,
)
for m in row_pattern.finditer(html):
rows.append({
"year": m.group(1),
"cash": m.group(2),
"stock": m.group(3),
"total": m.group(4),
})
if len(rows) >= 5:
break
# 方法3抓除息日相關
ex_date = pick(html, r">除息日</span><span[^>]*>([^<]+)")
pay_date = pick(html, r">發放日</span><span[^>]*>([^<]+)")
cash_div = pick(html, r">現金股利</span><span[^>]*>([\d.]+)")
stock_div = pick(html, r">股票股利</span><span[^>]*>([\d.]+)")
return {
"name": name,
"stock_no": stock_no,
"dividend_yield": dividend_yield,
"cash_div": cash_div,
"stock_div": stock_div,
"ex_date": ex_date,
"pay_date": pay_date,
"history": rows[:5],
}
def print_summary(d: dict) -> None:
print(f"{d['name']}{d['stock_no']})|股利摘要")
if d["dividend_yield"] != "-":
print(f"• 現金殖利率:{d['dividend_yield']}")
if d["cash_div"] != "-":
print(f"• 現金股利:{d['cash_div']}")
if d["stock_div"] != "-":
print(f"• 股票股利:{d['stock_div']}")
if d["ex_date"] != "-":
print(f"• 除息日:{d['ex_date']}")
if d["pay_date"] != "-":
print(f"• 發放日:{d['pay_date']}")
if d["history"]:
print("• 近年股利:")
for r in d["history"]:
print(f" - {r['year']}年:現金 {r['cash']}/股票 {r['stock']}/合計 {r['total']}")
else:
print("• 近年股利:無資料")
if __name__ == "__main__":
try:
html = fetch_html(STOCK_NO)
data = parse_dividend(html, STOCK_NO)
print_summary(data)
except Exception as e:
print(f"取得失敗:{e}", file=sys.stderr)
sys.exit(1)