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>
This commit is contained in:
2026-04-10 10:07:31 +08:00
commit 7d72aed984
16 changed files with 2153 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
用 Browserless 抓 Yahoo 三大法人頁,輸出精簡摘要(代號可變)
用法:
python3 stocks/yahoo-institutional-browserless.py 3481
python3 stocks/yahoo-institutional-browserless.py 2330
"""
import json
import os
from pathlib import Path
import re
import sys
import subprocess
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 "3481"
def fetch_html(stock_no: str) -> str:
url = f"https://tw.stock.yahoo.com/quote/{stock_no}.TW/institutional-trading"
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_inst(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()
# 抓 summary data避免大範圍 regex 回溯,先縮小搜尋範圍)
anchor = html.find('institutionBuySellSummaryData')
chunk = html[anchor: anchor + 250000] if anchor != -1 else html
rows = []
obj_pat = re.compile(r'\{[^{}]*"formattedDate":"[^"]+"[^{}]*\}')
for om in obj_pat.finditer(chunk):
s = om.group(0)
foreign_m = re.search(r'"foreignDiffVolK":(-?\d+)', s)
trust_m = re.search(r'"investmentTrustDiffVolK":(-?\d+)', s)
dealer_m = re.search(r'"dealerDiffVolK":(-?\d+)', s)
total_m = re.search(r'"totalDiffVolK":(-?\d+)', s)
date_m = re.search(r'"formattedDate":"([^"]+)"', s)
if foreign_m and trust_m and dealer_m and total_m and date_m:
rows.append((foreign_m.group(1), trust_m.group(1), dealer_m.group(1), total_m.group(1), date_m.group(1)))
if not rows:
raise RuntimeError("抓不到三大法人資料(頁面結構可能變更)")
def normalize_date(ds: str) -> str:
return ds.replace("\\u002F", "/")
def date_key(ds: str):
s = normalize_date(ds)
try:
y, m, d = s.split("/")
return int(y), int(m), int(d)
except Exception:
return (0, 0, 0)
foreign, trust, dealer, total, date = max(rows, key=lambda r: date_key(r[4]))
date = normalize_date(date)
return {
"name": name,
"stock_no": stock_no,
"date": date,
"foreign": int(foreign),
"trust": int(trust),
"dealer": int(dealer),
"total": int(total),
}
def sign(v: int) -> str:
return f"+{v:,}" if v >= 0 else f"{v:,}"
def print_summary(d: dict) -> None:
print(f"{d['name']}{d['stock_no']})|法人買賣超摘要")
print(f"• 日期:{d['date']}")
print(f"• 外資:{sign(d['foreign'])}")
print(f"• 投信:{sign(d['trust'])}")
print(f"• 自營商:{sign(d['dealer'])}")
print(f"• 三大法人合計:{sign(d['total'])}")
if __name__ == "__main__":
try:
html = fetch_html(STOCK_NO)
data = parse_inst(html, STOCK_NO)
print_summary(data)
except Exception as e:
print(f"取得失敗:{e}", file=sys.stderr)
sys.exit(1)