add monthly revenue and PE/PBR query scripts

- twse-monthly-revenue.py: fetch monthly revenue from Yahoo Finance TW via Browserless (MOPS blocks direct API)
- twse-pe-pbr.py: fetch PE ratio, PBR, dividend yield from TWSE BWIBBU API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 10:35:21 +08:00
parent cc2a67ac82
commit 3d14ad6e62
2 changed files with 282 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
查詢個股月營收Yahoo Finance TW + Browserless
用法:
python3 skills/stocks-query/scripts/twse-monthly-revenue.py 2330
python3 skills/stocks-query/scripts/twse-monthly-revenue.py 3481
資料來源Yahoo 奇摩股市(營收頁),營收單位:千元
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
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/revenue"
api = f"{ENDPOINT}/content?token={TOKEN}"
payload = json.dumps(
{
"url": url,
"gotoOptions": {"waitUntil": "domcontentloaded", "timeout": 60000},
},
ensure_ascii=False,
)
cmd = [
"curl", "-sS", "--max-time", "90",
"-X", "POST", api,
"-H", "Content-Type: application/json",
"-d", payload,
]
try:
text = subprocess.check_output(cmd, text=True, timeout=95)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Browserless 呼叫失敗: {e}") from e
except subprocess.TimeoutExpired as e:
raise RuntimeError("Browserless 呼叫逾時") from e
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_revenue(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\)", stock_no).strip()
# 找 JSON 中的月營收資料data 陣列內含 revenueMoM 欄位的物件)
obj_pat = re.compile(r'\{[^{}]*"revenueMoM":[^{}]*\}')
matches = obj_pat.findall(html)
if not matches:
raise RuntimeError("抓不到月營收資料(頁面結構可能變更)")
rows = []
for m in matches:
try:
obj = json.loads(m.replace("\\u002F", "/"))
rows.append(obj)
except json.JSONDecodeError:
continue
if not rows:
raise RuntimeError("無法解析月營收 JSON 資料")
# 依日期排序(最新在前)
rows.sort(key=lambda r: r.get("date", ""), reverse=True)
return {
"name": name,
"stock_no": stock_no,
"latest": rows[0],
"history": rows[:6],
}
def fmt_pct(val) -> str:
"""將小數比例轉為百分比字串。"""
try:
return f"{float(val) * 100:+.2f}%"
except (ValueError, TypeError):
return "-"
def fmt_rev(val: str) -> str:
"""千元營收加上千分位格式。"""
cleaned = str(val).replace(",", "")
try:
return f"{int(cleaned):,}"
except (ValueError, TypeError):
return val
def print_summary(d: dict) -> None:
name = d["name"]
stock_no = d["stock_no"]
latest = d["latest"]
date = latest.get("date", "-")
print(f"{name}{stock_no})|月營收摘要")
print(f"• 期間:{date}")
print(f"• 當月營收:{fmt_rev(latest.get('revenue', '-'))} 千元")
print(f"• 去年同月營收:{fmt_rev(latest.get('lastYearRevenue', '-'))} 千元")
print(f"• MoM月增率{fmt_pct(latest.get('revenueMoM'))}")
print(f"• YoY年增率{fmt_pct(latest.get('revenueYoY'))}")
print(f"• 當月累計營收:{fmt_rev(latest.get('revenueAcc', '-'))} 千元")
print(f"• 累計 YoY{fmt_pct(latest.get('revenueYoYAcc'))}")
# 近期趨勢
history = d.get("history", [])
if len(history) > 1:
print(f"\n{len(history)} 月趨勢:")
for row in history:
rd = row.get("date", "-")
rv = fmt_rev(row.get("revenue", "-"))
mom = fmt_pct(row.get("revenueMoM"))
yoy = fmt_pct(row.get("revenueYoY"))
print(f" {rd}{rv} 千元MoM {mom}YoY {yoy}")
if __name__ == "__main__":
try:
html = fetch_html(STOCK_NO)
data = parse_revenue(html, STOCK_NO)
print_summary(data)
except Exception as e:
print(f"取得失敗:{e}", file=sys.stderr)
sys.exit(1)