"""抓取全市场 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())