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 有三种形态:

  1. 文字状态:Not Started / HT / FT / AET / Pen. / Postp. 等
  2. HH:MM 形态(如 19:00):未开赛,值就是 UTC 开球时间
  3. 纯数字形态(如 23、67):进行中,值就是当前比赛分钟

判断”正在踢”要靠形态判断,不能靠某个固定字符串。官方也不建议用 status 参数去过滤”进行中”,而是拉一个日期窗后在客户端自己判读。

2.5 哪些数据在哪个套餐里

数据端点最低套餐
赛程、实时比分、积分榜/matches /livescore /standings免费试用(7 天)
阵容、伤停、球员/lineups /injuries /playersBasic(99 USDT/月)
赛前赔率/odds/prematchPro(199 USDT/月)
滚球赔率、WebSocket/odds/live wss://…/ws/livePremium(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 秒轮询一轮
免费试用11201080 次/小时,开赛 7 分钟就撞墙
Basic12,000勉强够一晚,但没有余量
Pro310,000够
Premium620,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_marketmatch_winnerfulltime_result
胜平负选项名Home / Draw / Away1 / 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
比分2020020201score_update
赛况(事件 + 统计)2000020001situation_update
滚球赔率2010020101odds_live_update
赛前赔率2030020301odds_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 这样的字符串,前端常渲染成一排胜平负色块。榜单不会每秒变,拉一次缓存到下一个比赛日结束。

五、欧冠接入踩坑点速查表

#坑正确做法
1Key 传法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 名可能有双空格
9WebSocket 地址api.anysport.io,Key 放 URL,过滤条件放订阅 data
10WS 字段差异home_team / phase=2H / 赔率是字符串,和 REST 分开解析
11订阅失败40301 不断连,重发即可;type: error 才断连
12限频双重限制超任一返回 429;免费试用 120/小时撑不起比分墙,赛程和榜单必须缓存
13赛前 / 滚球标签Home/Draw/Away 与 1/X/2 要映射,handicap 位置不同

六、总结

把欧冠 2026/27 赛季接进产品,整条链路就是:

  1. 确认联赛和赛季:/leagues + /seasons,拿到 asl_33740788c32040dc88ecfcf1c2b23023 和 2026-2027
  2. 拉本轮赛程:/matches/window?league_id=…&from_days=0&to_days=2,UTC 转北京时间,status 三态分组
  3. 实时比分:/livescore 按 match_id 查,先算限频再写轮询,phase 判终场
  4. 赔率:同一个 match_id 查 /odds/prematch 和 /odds/live,标签和让球线做一层映射
  5. 秒级推送:WebSocket 一条连接,league_id 过滤,比分和滚球赔率同时订阅

AnySport 这套接口在欧冠场景下最省心的两点,一是一个 match_id 从赛程贯穿到赔率,做赔率对比页不用对表;二是WebSocket 按 league_id 订阅,一晚九场同时踢也只需要一条连接。剩下的就是把上面 13 个坑记住。

联赛阶段还有 7 轮,现在到 anysport.io 注册,免费试用 7 天,比分层的代码今晚就能跑起来;赔率层需要 Pro(199 USDT/月)或 Premium(399 USDT/月)。