2026/27 赛季欧冠联赛已经在 9 月 8 日开打。36 支球队、联赛阶段 8 轮、每个比赛周二三两晚各塞进 8 到 9 场,而且开球时间几乎全挤在同一个时刻。想给自己的产品加一个”欧冠专区”——比分墙、进球提醒、赛前赔率对比、滚球盘口监控——真正卡住人的从来不是前端怎么画,而是三个上游问题:欧冠的赛程从哪拉、一晚上九场同时踢怎么不撞限频、赔率怎么和比分对到同一场比赛上。
这篇文章只解决这三个问题。全文以 AnySport 足球数据 API 为例,从欧冠的 league_id 出发,走通「确认赛季 → 拉本轮赛程 → 多场实时比分 → 赛前 / 滚球赔率 → WebSocket 按联赛订阅推送」这条链路。端点、字段、ID 都是真实的,复制就能跑。
一、欧冠数据能做成什么
围绕一个赛季的欧冠,常见的产品形态有这几种:
- 欧冠比分墙:一个页面同时显示当晚所有场次的比分、分钟数、阶段
- 进球 / 红牌提醒机器人:Telegram、Discord、企业微信里推”曼城 1-0 巴黎,第 23 分钟”
- 联赛阶段积分榜:新赛制 36 队一张大表,前 8 直接晋级、9 到 24 名打附加赛,榜单本身就是内容
- 赛前赔率对比页:每轮开赛前列出 18 场的胜平负、亚盘、大小球
- 滚球赔率监控:开赛后跟踪主盘口变化,做变盘提醒或回测
- 数据分析平台:历史赛季比分 + 赔率做模型,欧冠是最有流量的样本
这些场景的数据需求分两层:比分层(赛程、实时比分、事件)和赔率层(赛前、滚球)。下面先把接入前必须知道的几个设计讲清楚,再逐层写代码。
二、接入之前,先搞清楚这五件事
2.1 欧冠的 league_id 和赛季字符串
AnySport 的每个实体 ID 都带语义前缀:asl_ 联赛、ast_ 球队、as_ 比赛、asp_ 球员。欧冠的联赛 ID 是:
asl_33740788c32040dc88ecfcf1c2b23023赛季是个字符串。世界杯这种单年赛事是 2026,欧冠这种跨年赛事用连字符写全两年:2026-2027。不是 2026/27,不是 2026-27,也不是 2026。这是欧冠接入时第一个会翻车的地方,后面 4.2 会用 /seasons 接口确认。
2.2 一个 match_id 贯穿所有接口
同一场欧冠比赛,从赛程、实时比分、事件流、赛前赔率、滚球赔率到 WebSocket 推送,用的都是同一个 as_ 开头的 match_id。你只需要在赛程里找到比赛一次,后面所有接口直接复用,不用维护”赛程 ID 到赔率 ID”的映射表。做赔率对比页时,这一点能省掉一整层胶水代码。
2.3 所有时间都是 UTC,欧冠开球时间尤其要注意
AnySport 的所有时间字段一律 UTC。赛程里 date 和 time 是拆开的两个字段,需要自己拼成 2026-09-15T19:00:00Z 再转本地。
欧冠的开球时间是这个坑的重灾区:欧洲晚上 21:00(CEST)对应 UTC 19:00,也就是北京时间次日凌晨 3:00。你按北京日期 2026-09-16 去查,是查不到 9 月 15 日晚上那批比赛的,因为它们的 UTC 日期还是 2026-09-15。解决办法是用相对日期窗接口 /matches/window,而不是自己算绝对日期。
2.4 status 是三态字段,不是普通枚举
赛程接口的 status 有三种形态:
- 文字状态:
Not Started/HT/FT/AET/Pen./Postp.等 HH:MM形态(如19:00):未开赛,值就是 UTC 开球时间- 纯数字形态(如
23、67):进行中,值就是当前比赛分钟
判断”正在踢”要靠形态判断,不能靠某个固定字符串。官方也不建议用 status 参数去过滤”进行中”,而是拉一个日期窗后在客户端自己判读。
2.5 哪些数据在哪个套餐里
| 数据 | 端点 | 最低套餐 |
|---|---|---|
| 赛程、实时比分、积分榜 | /matches /livescore /standings | 免费试用(7 天) |
| 阵容、伤停、球员 | /lineups /injuries /players | Basic(99 USDT/月) |
| 赛前赔率 | /odds/prematch | Pro(199 USDT/月) |
| 滚球赔率、WebSocket | /odds/live wss://…/ws/live | Premium(399 USDT/月) |
比分层用免费试用就能把整条链路跑通,赔率层需要 Pro 或 Premium。
三、欧冠数据源横向对比
欧冠是所有服务商都覆盖的顶级赛事,差异不在”有没有”,而在实时性、赔率和价格:
| 服务商 | 定位 | 欧冠实时比分 | 欧冠赔率 | 特点 |
|---|---|---|---|---|
| Football-Data.org | 入门 / 开源友好 | 有(分钟级) | 无 | 免费档限流严格,无赔率、无推送 |
| TheSportsDB | 社区数据库 | 弱 | 无 | 偏静态资料,实时性差 |
| API-Football | 主流开发者选择 | 有 | 有 | 覆盖广、文档全,实时靠轮询 |
| SportMonks | 订阅制商业 | 有 | 有(附加包) | 分档订阅,赔率、阵容需加购 |
| The Odds API / BetsAPI | 赔率聚合 / 博彩向 | 部分 | 有 | 滚球深度参差,BetsAPI 合规门槛高 |
| Sportradar / Stats Perform (Opta) | 企业级官方数据 | 有(毫秒级) | 有 | 最快最全,合同制、价格极高、有准入门槛 |
| AnySport | 开发者友好 + 秒级推送 | 有(REST + WebSocket) | 有(赛前 + 滚球) | 单一 match_id,WebSocket 按联赛订阅,加密货币付费 |
(各家具体价格和覆盖以官网为准,此表只做定位参考。)
现实结论:Sportradar / Opta 是给博彩公司和电视台的,中小团队够不着;Football-Data.org 练手可以,做欧冠比分墙很快撞限流,且没有赔率。中间这档才是绝大多数开发者的选择,AnySport 的差异化是一个 match_id 贯穿比分和赔率,加上WebSocket 一条连接就能订阅整个欧冠,这对”一晚九场同时踢”的场景特别关键。
四、接入教程:从欧冠 league_id 到 WebSocket
4.1 认证与基础封装
到 dashboard.anysport.io 注册,验证邮箱后 API Key 显示在后台首页。认证只有一种方式:请求头 apikey,头名固定小写。
curl -H "apikey: YOUR_API_KEY" \
"https://data.anysport.io/v1/soccer/leagues?league_id=asl_33740788c32040dc88ecfcf1c2b23023"⚠️ 踩坑点 1:不支持
?api_key=查询参数,也不支持X-API-Key头。传错一律401 UNAUTHORIZED。WebSocket 是唯一例外,Key 反而要放 URL,见 4.7。
所有 REST 接口都在 https://data.anysport.io/v1/soccer 下,响应统一是 success / data / meta / traceId 信封。先封装一个 get(),后面全文复用:
import requests
from datetime import datetime, timezone, timedelta
API_KEY = "YOUR_API_KEY"
BASE = "https://data.anysport.io/v1/soccer"
HEADERS = {"apikey": API_KEY}
UCL_LEAGUE_ID = "asl_33740788c32040dc88ecfcf1c2b23023"
UCL_SEASON = "2026-2027" # 跨年赛季用连字符写全两年
def get(path, **params):
r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=10)
if r.status_code == 429:
raise RuntimeError("撞限频了,检查缓存策略")
r.raise_for_status()
body = r.json()
if not body["success"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} (traceId={body.get('traceId')})")
return body["data"]出问题找客服时带上 traceId,排查会快很多。
4.2 第一步:确认联赛和赛季
先用 league_id 反查一下联赛信息,顺便确认 is_cup:
league = get("/leagues", league_id=UCL_LEAGUE_ID)[0]
print(league["name"], league["country"], "is_cup =", league["is_cup"])/leagues 返回的字段是 league_id / name / country / country_id / is_cup。欧冠这类国际赛事的 is_cup 为 true。
然后用 /seasons 确认 2026-2027 这个赛季字符串是存在的,同时看它有没有积分榜和赛果数据:
seasons = get("/seasons", league_id=UCL_LEAGUE_ID)
for s in seasons:
if s["season"] == UCL_SEASON:
print(s)
# {'league_id': 'asl_3374...', 'season': '2026-2027', 'has_results': True, 'has_standings': True}⚠️ 踩坑点 2:
/seasons是season参数取值的唯一权威来源。跨年赛季是2026-2027,写成2026-27或2026不会报错,只会查到空数据或另一个赛季,很难排查。写代码前先跑一次/seasons把字符串确认下来。
4.3 第二步:拉本轮欧冠赛程
用相对日期窗接口 /matches/window,参数 from_days / to_days 表示”距今几天”,不用自己算日期,也不用传 season:
curl -H "apikey: YOUR_API_KEY" \
"https://data.anysport.io/v1/soccer/matches/window?from_days=0&to_days=2&league_id=asl_33740788c32040dc88ecfcf1c2b23023"每条比赛的结构如下(字段以文档为准,match_id 和 team_id 以实际返回为准):
{
"match_id": "as_…",
"date": "2026-09-15",
"time": "19:00",
"status": "19:00",
"league": "…",
"league_id": "asl_33740788c32040dc88ecfcf1c2b23023",
"season": "2026-2027",
"home": { "name": "…", "team_id": "ast_…" },
"away": { "name": "…", "team_id": "ast_…" },
"venue": ""
}已完赛的场次还会附带 ht_score / ft_score,有加时的附带 et_score,点球决胜的附带 penalty_score。
把当晚的比赛按状态分成三组,并把 UTC 开球时间转成北京时间:
CST = timezone(timedelta(hours=8))
def kickoff_local(m, tz=CST):
"""date + time 拆开的两个字段拼成 UTC 时间,再转本地"""
utc = datetime.strptime(f"{m['date']} {m['time']}", "%Y-%m-%d %H:%M").replace(tzinfo=timezone.utc)
return utc.astimezone(tz)
def classify(status: str) -> str:
"""status 三态判读"""
if status in ("HT", "ET", "P", "Break Time") or status.isdigit():
return "live"
if status in ("FT", "AET", "Pen.", "Awarded", "WO"):
return "finished"
return "upcoming" # HH:MM / Not Started / TBA / 空串 / Postp. 等
matches = get("/matches/window", from_days=0, to_days=2, league_id=UCL_LEAGUE_ID, limit=100)
groups = {"upcoming": [], "live": [], "finished": []}
for m in matches:
groups[classify(m.get("status", ""))].append(m)
for m in groups["upcoming"]:
print(kickoff_local(m).strftime("%m-%d %H:%M"), m["home"]["name"], "vs", m["away"]["name"], m["match_id"])
# 09-16 00:45 … (欧洲 18:45 场)
# 09-16 03:00 … (欧洲 21:00 场)⚠️ 踩坑点 3:欧冠一个比赛日通常两档开球,UTC 16:45 和 19:00,都落在北京时间次日凌晨。
from_days=0的”今天”是 UTC 的今天。想拉”北京时间明天凌晨”的比赛,窗口要覆盖 UTC 的今天,别按本地日期算。⚠️ 踩坑点 4:
/matches/window的to_days默认是 7、跨度最大 90 天,limit最大 100。欧冠一轮 18 场、两三个比赛日,to_days=2加limit=100一页就够;拉整个联赛阶段要翻页,看meta.total_pages。
4.4 第三步:多场实时比分,先算限频再写轮询
实时比分接口(/livescore) 按单场 match_id 查,match_id 必填,不能一次拉所有直播比赛:
def live_score(match_id):
rows = get("/livescore", match_id=match_id)
return rows[0] if rows else None响应结构(文档中的真实样例,来自 2026 世界杯阿根廷 3-1 瑞士,欧冠场次的字段完全一致):
{
"match_id": "as_3b37c382f39a4235aa93705f053b2da4",
"home": "Argentina",
"away": "Switzerland",
"league": "World Cup 2026",
"minute": 120,
"phase": "finished",
"home_score": 3,
"away_score": 1,
"ht_score": "1-0",
"reg_score": "1-1",
"et_score": "3-1",
"updated_at": "2026-07-12T03:45:50Z"
}⚠️ 踩坑点 5:
/livescore不返回status,只有phase(regular/extra_time/penalty/finished)。判终场用phase == "finished"。minute连续计时含补时不清零,判断是否进入加时用minute > 90,因为加时中普通攻防帧的phase也是regular。联赛阶段没有加时,淘汰赛阶段(2027 年 2 月起)这个坑才会真的踩到,但现在写对了以后不用改。
欧冠比分墙的关键问题是并发场次和限频。各套餐有每秒和每小时双重限制,超任一返回 429:
| 套餐 | 每秒 | 每小时 | 9 场同时踢,每 30 秒轮询一轮 |
|---|---|---|---|
| 免费试用 | 1 | 120 | 1080 次/小时,开赛 7 分钟就撞墙 |
| Basic | 1 | 2,000 | 勉强够一晚,但没有余量 |
| Pro | 3 | 10,000 | 够 |
| Premium | 6 | 20,000 | 够,且应该直接用 WebSocket |
所以 REST 轮询比分墙要这样写:只轮询 live 组,赛程列表本身缓存,每轮之间拉开间隔,并且请求串行以满足每秒上限:
import time
def poll_wall(live_matches, interval=60):
"""只轮询进行中的场次;赛程组每几分钟用 /matches/window 刷新一次"""
while live_matches:
still_live = []
for m in live_matches:
s = live_score(m["match_id"])
time.sleep(1.1) # 免费 / Basic 每秒 1 次,请求串行
if not s:
continue
print(f"{s['home']} {s['home_score']}-{s['away_score']} {s['away']} "
f"{s['minute']}' {s['phase']}")
if s["phase"] != "finished":
still_live.append(m)
live_matches = still_live
time.sleep(interval)这段代码在免费试用套餐下每小时大约 540 次请求(9 场、每分钟一轮),已经超过 120 的额度。免费试用只适合跑通链路,真正上线的欧冠比分墙要么 Pro 轮询,要么 Premium 走 WebSocket。 后者才是正解,见 4.7。
4.5 进阶:进球事件流
/livescore 只给结果。要做”曼城 1-0,第 23 分钟,哈兰德”这样的推送,用实时赛况接口 /matches/{match_id}/situation,它返回全场至今所有事件的全量快照加 11 项技术统计。每次轮询直接整体替换本地状态即可。
def new_goals(match_id, seen: set):
"""返回上次之后新增的进球事件;seen 存已处理的 seq"""
s = get(f"/matches/{match_id}/situation")
fresh = [e for e in s["events"] if e["type"] == "goal" and e["seq"] not in seen]
seen.update(e["seq"] for e in fresh)
return fresh
seen = set()
for e in new_goals("as_你的比赛ID", seen):
print(f"{e['minute']}' {e.get('team','')} {e['text']}")事件 type 有 7 类:goal / yellow_card / red_card / corner / penalty / substitution / event。stats 是固定 11 个键,值是 [主, 客] 数对,比如 "c": [8, 2] 表示角球主 8 客 2。
⚠️ 踩坑点 6:态势接口的
status(如home_corner/var_goal/full_time)和事件type都是开放枚举,官方会持续新增。遇到不认识的值要降级为普通事件,不要抛异常。另外比赛没开始时这个接口返回 404,业务上当”暂无数据”处理,不要当系统错误。
4.6 第四步:赛前赔率与滚球赔率
赔率也用同一个 match_id。赛前赔率用 /odds/prematch(Pro 及以上):
curl -H "apikey: YOUR_API_KEY" \
"https://data.anysport.io/v1/soccer/odds/prematch?match_id=as_你的比赛ID"响应结构(文档中的真实样例,来自世界杯半决赛英格兰 vs 阿根廷,节选):
{
"match_id": "as_37f0da0520bb4e9a9588f80994cefc5b",
"home": "England",
"away": "Argentina",
"bookmaker": "bet365",
"phase": "prematch",
"main_market": "match_winner",
"home_odd": 2.62,
"draw_odd": 3.0,
"away_odd": 3.0,
"markets": [
{ "market_code": "match_winner",
"options": [ { "name": "Home", "odd": 2.62 }, { "name": "Draw", "odd": 3.0 }, { "name": "Away", "odd": 3.0 } ] },
{ "market_code": "asian_handicap",
"options": [ { "name": "Home -1.25", "odd": 4.8 }, { "name": "Away +1.25", "odd": 1.18 }, { "name": "Home -1", "odd": 4.4 } ] }
]
}开赛后切到滚球赔率 /odds/live(Premium),同一个 match_id。滚球响应的顶层字段一样,但两处结构不同:
赛前 /odds/prematch | 滚球 /odds/live | |
|---|---|---|
主盘口 main_market | match_winner | fulltime_result |
| 胜平负选项名 | Home / Draw / Away | 1 / X / 2 |
| 让球线位置 | 写在选项名里(Home -1.25) | 独立字段 markets[].handicap,无让球概念的盘不下发 |
一个欧冠赛前赔率对比页的核心代码,给一轮 18 场批量拉主盘口并换算隐含概率:
def implied(odds_row):
raw = {"1": odds_row["home_odd"], "X": odds_row["draw_odd"], "2": odds_row["away_odd"]}
total = sum(1 / v for v in raw.values()) # 去水位归一化
return {k: round(1 / v / total, 3) for k, v in raw.items()}
for m in groups["upcoming"]:
rows = get("/odds/prematch", match_id=m["match_id"])
if not rows:
continue # 还没开盘
o = rows[0]
print(f"{o['home']} vs {o['away']} "
f"{o['home_odd']} / {o['draw_odd']} / {o['away_odd']} {implied(o)}")
time.sleep(0.4) # Pro 每秒 3 次⚠️ 踩坑点 7:赛前和滚球是两条物理数据流,
league字段的命名体系不同(文档给的例子是赛前World: World Championship - Play Offs、滚球World Cup 2026,同一赛事两种写法)。不要用league名字去过滤欧冠赔率,用赛程拉出来的match_id列表逐场查,这才是match_id贯穿设计的正确用法。⚠️ 踩坑点 8:滚球盘口动态收缩,比分变化后相关盘口会撤盘重挂,临近终场盘口数变少,终场后
markets可能是[]。写解析时不要假设某个market_code一定存在。”未来 N 分钟”类盘口的 option 名里有双空格(Goals/Over 0.5),用\s+拆。
4.7 第五步:WebSocket 一条连接订阅整个欧冠
Premium 套餐提供 WebSocket,服务器主动推送,是欧冠比分墙和滚球监控的正解。四条数据流可以在一条连接上同时订阅:
| 数据流 | 订阅 code | 订阅 ack | 推送 type |
|---|---|---|---|
| 比分 | 20200 | 20201 | score_update |
| 赛况(事件 + 统计) | 20000 | 20001 | situation_update |
| 滚球赔率 | 20100 | 20101 | odds_live_update |
| 赛前赔率 | 20300 | 20301 | odds_prematch_update |
| 心跳 | 20010 | 无 ack | — |
订阅消息的 data 支持 match_id / league_id / country_id 三个过滤字段。对欧冠场景,league_id 过滤是最省事的:不用逐场订阅,一条订阅覆盖当晚所有欧冠比赛。
import asyncio, json, uuid
import websockets # pip install websockets
API_KEY = "YOUR_API_KEY"
WS_URL = f"wss://api.anysport.io/v1/ws/live?api_key={API_KEY}"
UCL_LEAGUE_ID = "asl_33740788c32040dc88ecfcf1c2b23023"
def sub(code, **data):
return json.dumps({"code": code, "trace": str(uuid.uuid4()), "data": data})
async def heartbeat(ws):
while True:
await asyncio.sleep(30) # 服务端 60 秒无心跳断连
await ws.send(json.dumps({"code": 20010, "trace": "hb"}))
async def main():
async with websockets.connect(WS_URL) as ws:
print(await ws.recv()) # {"type": "connected", ...}
# 同一条连接:比分 + 滚球赔率,都按欧冠 league_id 过滤
await ws.send(sub(20200, league_id=UCL_LEAGUE_ID))
await ws.send(sub(20100, league_id=UCL_LEAGUE_ID))
asyncio.create_task(heartbeat(ws))
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "score_update":
d = msg["data"]
print(f"[比分] {d['home_team']} {d['home_score']}-{d['away_score']} {d['away_team']}"
f" {d.get('minute','')}' phase={d.get('phase','')}")
elif t == "odds_live_update":
d = msg["data"]
print(f"[滚球] {d['home_team']} vs {d['away_team']} "
f"{d.get('home_odd')} / {d.get('draw_odd')} / {d.get('away_odd')}")
elif t == "error":
print("连接级错误:", msg["message"]); break # 心跳超时 / 限频,需重连
elif "code" in msg:
print("业务应答:", msg["code"], msg.get("msg")) # 20201/20101 = 订阅成功
asyncio.run(main())比分推送长这样(文档样例):
{
"type": "score_update",
"data": {
"type": "score",
"match_id": "…",
"home_team": "Manchester United",
"away_team": "Arsenal",
"league": "Premier League",
"phase": "2H",
"minute": 67,
"home_score": 2,
"away_score": 1,
"ht_score": "1-0",
"reg_score": "2-1",
"et_score": null,
"penalty_score": null,
"source_updated_at": "2026-07-13T15:67:01Z"
},
"timestamp": "2026-07-13T15:67:01.250Z"
}⚠️ 踩坑点 9:WebSocket 的接入方式和 REST 有三处相反。端点域名是
api.anysport.io而不是data.anysport.io;Key 放 URL 的?api_key=(或订阅消息的data.api_key)而不是请求头;过滤条件放订阅消息的data里,不要写在 URL 上。握手被拒是 HTTP 层的401/403/429,收不到任何 WebSocket 消息,要在连接代码里单独处理。⚠️ 踩坑点 10:WS 推送的字段名和 REST 不一样。队名是
home_team/away_team(REST 是home/away),phase是1H/HT/2H/FT(REST 是regular/finished),滚球赔率推送里home_odd等是字符串(REST 是数字)。两套解析逻辑别混用。⚠️ 踩坑点 11:订阅失败返回
40301(无权限 / 套餐过期),连接不断,修正后重发即可。再次发同一数据流的订阅 code 会覆盖旧过滤条件,不用重连。只有type: "error"(心跳超时、请求频率超 60 次/分钟)才会断连,这时要带退避重连。league_id在文档里标为可选扩展字段,稳妥起见客户端再按 4.3 拉到的match_id集合做一层过滤。
4.8 顺手:联赛阶段积分榜
新赛制的 36 队大表是欧冠内容页的标配,/standings 免费试用就能用,注意 season 必填且要用 2026-2027:
table = get("/standings", league_id=UCL_LEAGUE_ID, season=UCL_SEASON)
for row in table[:8]: # 前 8 直接晋级 16 强
print(row["position"], row["team"], row["played"], row["points"], row["goal_difference"], row["form"])字段有 position / team / team_id / played / wins / draws / losses / goals_for / goals_against / goal_difference / points / form。form 是 WWD 这样的字符串,前端常渲染成一排胜平负色块。榜单不会每秒变,拉一次缓存到下一个比赛日结束。
五、欧冠接入踩坑点速查表
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | Key 传法 | REST 用请求头 apikey;不支持 ?api_key= 和 X-API-Key |
| 2 | 赛季字符串 | 跨年赛季 2026-2027,先用 /seasons 确认 |
| 3 | 开球时间 | 全部 UTC,欧冠 19:00 UTC 是北京次日 03:00,用 /matches/window 别按本地日期算 |
| 4 | 赛程分页 | 一轮 18 场用 limit=100,整个联赛阶段看 meta.total_pages |
| 5 | 判终场 | /livescore 用 phase == "finished",该接口没有 status |
| 6 | 开放枚举 | 态势 status / 事件 type / market_code 未知值降级不抛错,未开赛态势返回 404 |
| 7 | 赔率按联赛过滤 | 赛前 / 滚球 league 命名不同,用 match_id 列表逐场查 |
| 8 | 滚球盘口 | 会收缩,终场后 markets 可能为 [],option 名可能有双空格 |
| 9 | WebSocket 地址 | api.anysport.io,Key 放 URL,过滤条件放订阅 data |
| 10 | WS 字段差异 | home_team / phase=2H / 赔率是字符串,和 REST 分开解析 |
| 11 | 订阅失败 | 40301 不断连,重发即可;type: error 才断连 |
| 12 | 限频 | 双重限制超任一返回 429;免费试用 120/小时撑不起比分墙,赛程和榜单必须缓存 |
| 13 | 赛前 / 滚球标签 | Home/Draw/Away 与 1/X/2 要映射,handicap 位置不同 |
六、总结
把欧冠 2026/27 赛季接进产品,整条链路就是:
- 确认联赛和赛季:
/leagues+/seasons,拿到asl_33740788c32040dc88ecfcf1c2b23023和2026-2027 - 拉本轮赛程:
/matches/window?league_id=…&from_days=0&to_days=2,UTC 转北京时间,status三态分组 - 实时比分:
/livescore按match_id查,先算限频再写轮询,phase判终场 - 赔率:同一个
match_id查/odds/prematch和/odds/live,标签和让球线做一层映射 - 秒级推送:WebSocket 一条连接,
league_id过滤,比分和滚球赔率同时订阅
AnySport 这套接口在欧冠场景下最省心的两点,一是一个 match_id 从赛程贯穿到赔率,做赔率对比页不用对表;二是WebSocket 按 league_id 订阅,一晚九场同时踢也只需要一条连接。剩下的就是把上面 13 个坑记住。
联赛阶段还有 7 轮,现在到 anysport.io 注册,免费试用 7 天,比分层的代码今晚就能跑起来;赔率层需要 Pro(199 USDT/月)或 Premium(399 USDT/月)。