This commit is contained in:
2026-09-21 12:38:13 +08:00
parent bcacbed427
commit ed6d4ee7a5
14 changed files with 2458 additions and 1073 deletions

View File

@@ -0,0 +1,127 @@
"""抓取全市场 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())