128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""抓取全市场 ETF 列表(规模 + 成交额),用于挑选各热门板块的代表性标的。
|
||
|
||
数据源:东方财富行情接口(公开 JSON,无需登录)。
|
||
字段:f12=代码 f13=市场(0深/1沪) f14=名称 f2=最新价 f3=涨跌幅 f6=成交额 f20=总市值 f21=流通市值
|
||
|
||
用法: py -3.14 -B labs/analysis/etf/screen_etf.py [关键词...]
|
||
不带关键词时按板块关键词分组输出候选。
|
||
"""
|
||
|
||
import json
|
||
import sys
|
||
import time
|
||
import urllib.parse
|
||
import urllib.request
|
||
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
BASE_HOSTS = ("82.push2.eastmoney.com", "push2delay.eastmoney.com", "push2.eastmoney.com")
|
||
FS = "b:MK0021,b:MK0022,b:MK0023,b:MK0024" # 沪深 ETF/LOF 集合
|
||
FIELDS = "f12,f13,f14,f2,f3,f6,f20,f21"
|
||
HEADERS = {
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||
"Referer": "https://fund.eastmoney.com/",
|
||
}
|
||
|
||
# 每个板块的候选关键词(用于在名称里匹配);越靠前优先级越高
|
||
SECTORS = {
|
||
"AI": ["人工智能", "AIETF", "AIETF", "科创AI", "智能"],
|
||
"CPO": ["光通信", "通信ETF", "通信设备", "5G通信", "通信"],
|
||
"PCB": ["PCB", "电子ETF", "消费电子", "电子50", "电子"],
|
||
"人形机器人": ["机器人", "智能制造", "工业母机"],
|
||
"光": ["光伏", "新能源", "电池"],
|
||
"创新药": ["创新药", "医药", "生物医药", "医疗"],
|
||
"半导体": ["半导体", "芯片", "集成电路", "科创芯片"],
|
||
"存储": ["存储", "集成电路", "科创芯片"],
|
||
"半导体材料": ["半导体材料", "材料ETF", "新材料"],
|
||
"玻璃基板": ["玻璃", "建材", "新材料"],
|
||
"电力": ["电力", "公用事业", "电网", "绿电"],
|
||
"航空航天": ["航空", "航天", "军工", "国防"],
|
||
"金属": ["有色", "金属", "黄金", "稀土"],
|
||
"金融": ["证券", "银行", "保险", "金融"],
|
||
"能源": ["能源", "煤炭", "石油", "油气"],
|
||
}
|
||
|
||
|
||
def fetch_page(page: int, size: int = 100, retries: int = 4) -> tuple[list[dict], int]:
|
||
query = urllib.parse.urlencode({
|
||
"pn": page, "pz": size, "po": 1, "np": 1, "fltt": 2, "invt": 2,
|
||
"fid": "f3", "fs": FS, "fields": FIELDS,
|
||
"ut": "bd1d9ddb04089700cf9c27f6f7426281",
|
||
})
|
||
last_error = None
|
||
for attempt in range(retries):
|
||
# 主站 push2 容易被限流返回 502,轮换到镜像域名
|
||
host = BASE_HOSTS[attempt % len(BASE_HOSTS)]
|
||
request = urllib.request.Request(f"http://{host}/api/qt/clist/get?{query}", headers=HEADERS)
|
||
try:
|
||
with urllib.request.urlopen(request, timeout=30) as response:
|
||
payload = json.load(response)
|
||
data = payload.get("data") or {}
|
||
return data.get("diff") or [], int(data.get("total") or 0)
|
||
except Exception as exc: # 502/超时都换域名重试
|
||
last_error = exc
|
||
time.sleep(1.0 * (attempt + 1))
|
||
raise RuntimeError(f"第 {page} 页抓取失败:{last_error}")
|
||
|
||
|
||
def fetch_all() -> list[dict]:
|
||
rows, total = fetch_page(1)
|
||
page = 2
|
||
while len(rows) < total:
|
||
try:
|
||
more, _ = fetch_page(page)
|
||
except RuntimeError as exc:
|
||
print(f"警告:{exc},已抓 {len(rows)}/{total}", file=sys.stderr)
|
||
break
|
||
if not more:
|
||
break
|
||
rows.extend(more)
|
||
page += 1
|
||
time.sleep(0.6) # 轻量限速,避免被限流
|
||
return rows
|
||
|
||
|
||
def code_of(row: dict) -> str:
|
||
suffix = "SH" if str(row.get("f13")) == "1" else "SZ"
|
||
return f"{row['f12']}.{suffix}"
|
||
|
||
|
||
def size_yi(row: dict) -> float:
|
||
"""规模(亿元):接口给的是元。"""
|
||
value = row.get("f20") or row.get("f21") or 0
|
||
return float(value) / 1e8
|
||
|
||
|
||
def turnover_yi(row: dict) -> float:
|
||
"""成交额(亿元)。"""
|
||
return float(row.get("f6") or 0) / 1e8
|
||
|
||
|
||
def main() -> int:
|
||
keywords = sys.argv[1:]
|
||
rows = fetch_all()
|
||
print(f"抓取到 {len(rows)} 只 ETF/LOF")
|
||
if keywords:
|
||
for row in rows:
|
||
name = str(row.get("f14") or "")
|
||
if any(k in name for k in keywords):
|
||
print(f"{code_of(row):12} {name:24} 规模={size_yi(row):8.2f}亿 "
|
||
f"成交额={turnover_yi(row):7.3f}亿 涨跌={row.get('f3')}%")
|
||
return 0
|
||
|
||
for sector, words in SECTORS.items():
|
||
hits = [r for r in rows if any(w in str(r.get("f14") or "") for w in words)]
|
||
hits.sort(key=lambda r: -size_yi(r))
|
||
print(f"\n== {sector} 候选 {len(hits)} 只(按规模降序,取前 6)==")
|
||
for row in hits[:6]:
|
||
print(f" {code_of(row):12} {str(row.get('f14')):26} "
|
||
f"规模={size_yi(row):8.2f}亿 成交额={turnover_yi(row):7.3f}亿")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|