每日备份 2026-03-27

This commit is contained in:
OpenClaw Backup
2026-03-27 23:38:45 +08:00
parent 4f11cd7b03
commit d09281e48c
827 changed files with 6991 additions and 148648 deletions
@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
A股实时行情抓取脚本
数据源:新浪财经(主)、东方财富(备)
用法:
python3 fetch_stock.py --code 600519
python3 fetch_stock.py --code 000001 sz000001 300750
python3 fetch_stock.py --index
python3 fetch_stock.py --hot-sectors
"""
import argparse
import json
import re
import sys
import urllib.request
import urllib.error
from datetime import datetime
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://finance.sina.com.cn",
"Accept-Language": "zh-CN,zh;q=0.9",
}
def get_market_prefix(code: str) -> str:
"""根据股票代码判断市场前缀"""
code = code.strip().upper()
if code.startswith("SH") or code.startswith("SZ"):
return code[:2].lower(), code[2:]
code = re.sub(r"[^0-9]", "", code)
if code.startswith(("60", "68", "51", "58", "11")):
return "sh", code
elif code.startswith(("00", "30", "15", "12", "16", "13")):
return "sz", code
return "sh", code # default
def fetch_url(url: str, extra_headers: dict = None) -> str:
req = urllib.request.Request(url, headers=HEADERS)
if extra_headers:
for k, v in extra_headers.items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
charset = "gbk" if "sina" in url or "sinajs" in url else "utf-8"
return resp.read().decode(charset, errors="replace")
except Exception as e:
return ""
def parse_sina_stock(raw: str, symbol: str) -> dict:
"""解析新浪财经股票数据"""
match = re.search(r'"([^"]*)"', raw)
if not match:
return {}
parts = match.group(1).split(",")
if len(parts) < 32:
return {}
try:
name = parts[0]
prev_close = float(parts[2]) if parts[2] else 0
open_price = float(parts[1]) if parts[1] else 0
current = float(parts[3]) if parts[3] else 0
high = float(parts[4]) if parts[4] else 0
low = float(parts[5]) if parts[5] else 0
volume = int(parts[8]) if parts[8] else 0 # 手
amount = float(parts[9]) if parts[9] else 0 # 元
date_str = parts[30] if len(parts) > 30 else ""
time_str = parts[31] if len(parts) > 31 else ""
change = current - prev_close
change_pct = (change / prev_close * 100) if prev_close else 0
turnover_approx = volume / 1000 # 粗略换手(无流通股数据)
return {
"symbol": symbol,
"name": name,
"current": round(current, 2),
"change": round(change, 2),
"change_pct": round(change_pct, 2),
"open": round(open_price, 2),
"high": round(high, 2),
"low": round(low, 2),
"prev_close": round(prev_close, 2),
"volume_lot": volume, # 手
"amount_yuan": round(amount, 2),
"amount_yi": round(amount / 1e8, 2),
"date": date_str,
"time": time_str,
"source": "新浪财经",
"fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
except (ValueError, IndexError):
return {}
def fetch_single_stock(code: str) -> dict:
"""抓取单只股票行情"""
prefix, clean_code = get_market_prefix(code)
symbol = f"{prefix}{clean_code}"
url = f"http://hq.sinajs.cn/list={symbol}"
raw = fetch_url(url)
if raw:
data = parse_sina_stock(raw, symbol)
if data:
return data
# 备用:东方财富
market = 1 if prefix == "sh" else 0
url2 = (
f"http://push2.eastmoney.com/api/qt/stock/get"
f"?secid={market}.{clean_code}"
f"&fields=f43,f44,f45,f46,f47,f48,f57,f58,f60,f107,f169,f170,f171"
)
raw2 = fetch_url(url2, {"Referer": "https://www.eastmoney.com"})
if raw2:
try:
obj = json.loads(raw2)
d = obj.get("data", {}) or {}
if d.get("f43"):
prev = d["f60"] / 100
curr = d["f43"] / 100
chg = d["f169"] / 100
chg_pct = d["f170"] / 100
return {
"symbol": symbol,
"name": d.get("f58", ""),
"current": round(curr, 2),
"change": round(chg, 2),
"change_pct": round(chg_pct, 2),
"open": round(d["f46"] / 100, 2),
"high": round(d["f44"] / 100, 2),
"low": round(d["f45"] / 100, 2),
"prev_close": round(prev, 2),
"volume_lot": d.get("f47", 0),
"amount_yuan": d.get("f48", 0),
"amount_yi": round(d.get("f48", 0) / 1e8, 2),
"turnover_pct": round(d.get("f171", 0) / 100, 2),
"source": "东方财富",
"fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
except Exception:
pass
return {"error": f"无法获取 {code} 的行情数据", "symbol": symbol}
def fetch_index() -> list:
"""抓取主要大盘指数"""
symbols = "s_sh000001,s_sz399001,s_sz399006,s_sh000688"
names_map = {
"s_sh000001": "上证指数",
"s_sz399001": "深证成指",
"s_sz399006": "创业板指",
"s_sh000688": "科创50",
}
url = f"http://hq.sinajs.cn/list={symbols}"
raw = fetch_url(url)
results = []
if raw:
for sym, name in names_map.items():
pattern = rf'hq_str_{re.escape(sym)}="([^"]*)"'
m = re.search(pattern, raw)
if m:
parts = m.group(1).split(",")
if len(parts) >= 5:
try:
results.append({
"name": parts[0] or name,
"current": float(parts[1]),
"change": float(parts[2]),
"change_pct": float(parts[3]),
"volume_yi_lot": round(float(parts[4]) / 1e8, 2),
"amount_yi": round(float(parts[5]) / 1e8, 2) if len(parts) > 5 else 0,
"fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"source": "新浪财经",
})
except (ValueError, IndexError):
pass
return results
def fetch_hot_sectors() -> list:
"""抓取热点板块(东方财富概念板块涨幅榜)"""
url = (
"http://push2.eastmoney.com/api/qt/clist/get"
"?pn=1&pz=20&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281"
"&fltt=2&invt=2&fid=f3"
"&fs=m:90+t:2+f:!50"
"&fields=f2,f3,f4,f12,f14,f20,f128,f136,f207,f208,f209"
)
raw = fetch_url(url, {"Referer": "https://www.eastmoney.com"})
results = []
if raw:
try:
obj = json.loads(raw)
items = obj.get("data", {}).get("diff", [])
for item in items:
results.append({
"name": item.get("f14", ""),
"change_pct": round(item.get("f3", 0), 2),
"leading_stock": item.get("f128", ""),
"leading_change_pct": round(item.get("f136", 0), 2),
"amount_yi": round(item.get("f20", 0) / 1e8, 2),
})
except Exception:
pass
return results
def fmt_stock(d: dict) -> str:
if "error" in d:
return f"{d['error']}"
sign = "+" if d["change"] >= 0 else ""
emoji = "🔴" if d["change"] >= 0 else "🟢"
lines = [
f"{emoji} {d['name']}{d['symbol'].upper()}",
f" 当前价:{d['current']}",
f" 涨跌幅:{sign}{d['change_pct']}% 涨跌额:{sign}{d['change']}",
f" 今开:{d['open']} 最高:{d['high']} 最低:{d['low']} 昨收:{d['prev_close']}",
f" 成交量:{d['volume_lot']:,} 手 成交额:{d['amount_yi']} 亿",
]
if "turnover_pct" in d:
lines.append(f" 换手率:{d['turnover_pct']}%")
lines.append(f" 数据来源:{d['source']} | 更新时间:{d.get('time', '')} {d['fetch_time']}")
return "\n".join(lines)
def fmt_index(items: list) -> str:
lines = ["📊 大盘指数实时行情"]
for d in items:
sign = "+" if d["change"] >= 0 else ""
emoji = "🔴" if d["change"] >= 0 else "🟢"
lines.append(
f" {emoji} {d['name']}: {d['current']:,.2f} {sign}{d['change']:+.2f} ({sign}{d['change_pct']}%)"
f" 成交额 {d['amount_yi']} 亿"
)
if items:
lines.append(f" 更新时间:{items[0]['fetch_time']}")
return "\n".join(lines)
def fmt_sectors(items: list) -> str:
lines = ["🔥 热点板块涨幅榜(概念板块 TOP20)"]
for i, d in enumerate(items, 1):
sign = "+" if d["change_pct"] >= 0 else ""
lines.append(
f" {i:2d}. {d['name']:<12} {sign}{d['change_pct']}%"
f" 龙头:{d['leading_stock']}({sign}{d['leading_change_pct']}%)"
f" 成交额:{d['amount_yi']}亿"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="A股实时行情抓取")
parser.add_argument("--code", nargs="+", help="股票代码,支持多个(如 600519 000001")
parser.add_argument("--index", action="store_true", help="查询大盘指数")
parser.add_argument("--hot-sectors", action="store_true", help="查询热点板块")
parser.add_argument("--json", action="store_true", help="输出原始JSON")
args = parser.parse_args()
if args.index:
data = fetch_index()
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2))
else:
print(fmt_index(data))
return
if args.hot_sectors:
data = fetch_hot_sectors()
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2))
else:
print(fmt_sectors(data))
return
if args.code:
results = []
for code in args.code:
d = fetch_single_stock(code)
results.append(d)
if args.json:
print(json.dumps(results, ensure_ascii=False, indent=2))
else:
for d in results:
print(fmt_stock(d))
print()
return
parser.print_help()
if __name__ == "__main__":
main()