一句话总结:用免费开源 API 拉取 2026 世界杯 104 场比赛数据,Python + matplotlib 做可视化面板,从注册到出图 30 分钟搞定。


在这里插入图片描述

一、前言

2026 美墨加世界杯今天凌晨正式开幕。揭幕战墨西哥 2-0 击败南非,单场出现 3 张红牌,直接追平上一届世界杯整个小组赛的红牌总数。

作为一个写 Python 的,我第一时间想到的是:能不能用代码把这些数据拉下来,做一个可视化面板?

答案是可以的。GitHub 上有个开源项目 worldcup2026,提供了 2026 世界杯的免费 REST API,覆盖全部 48 支球队、104 场比赛、16 个体育场的实时数据。

本文将从零开始,带你完成:API 调用 → 数据清洗 → matplotlib 可视化 → 面板输出的完整流程。所有代码可直接复制运行。


二、核心参数速览

参数 说明
Python 版本 3.10+ 3.8 以上均可运行
依赖库 requests, pandas, matplotlib, numpy pip install 一行搞定
API 地址 worldcup26.ir 免费开源,需注册
Token 有效期 84 天 过期后重新登录即可
覆盖比赛数 104 场 小组赛 72 + 淘汰赛 32
覆盖球队数 48 支 12 个小组,每组 4 队
覆盖体育场 16 个 三国联合举办(美/墨/加)
数据返回格式 JSON 标准 RESTful API
操作系统 macOS / Windows / Linux 跨平台,无限制

三、API 数据源介绍

3.1 为什么选 worldcup26.ir

对比维度 worldcup26.ir footballapi.com thestatsapi.com
费用 ✅ 免费 ❌ 付费($49/月起) ❌ 付费
比赛覆盖 ✅ 104 场全覆盖 ✅ 全覆盖 ✅ 全覆盖
详细统计 ❌ 无(仅比分) ✅ xG/射门/传球 ✅ 详细
中文支持 ❌ 英文+波斯语 ✅ 多语言 ✅ 多语言
上手难度 ✅ 极低(REST+JSON) 中等 中等

结论:如果你只需要比分、赛程、球队、积分榜,worldcup26.ir 完全够用。如果需要 xG、射门数等深度数据,再考虑付费接口。

3.2 核心 API 端点速查表

端点 方法 说明 返回字段
/get/games GET 全部 104 场比赛 含实时比分、小组、赛程
/get/game/{id} GET 单场比赛(1-104) id=1 为揭幕战
/get/teams GET 全部 48 支球队 支持 ?group=A 筛选
/get/groups GET 12 个小组积分榜 实时更新
/get/stadiums GET 16 个体育场 含容量和位置
/auth/authenticate POST 登录获取 Token 有效期 84 天

3.3 数据 Pipeline 架构

┌─────────────┐     ┌──────────────┐     ┌────────────────┐     ┌──────────────┐
│  worldcup26  │────▶│  Python API  │────▶│   matplotlib   │────▶│   PNG 图表   │
│  .ir (API)   │     │  封装层       │     │   可视化引擎    │     │   输出面板    │
└─────────────┘     └──────────────┘     └────────────────┘     └──────────────┘
     │                    │                      │                      │
  注册账号            请求Token             数据清洗+绑定           保存PNG
  获取API Key        封装requests           中文字体处理           显示图表
                     处理401重试            pandas DataFrame

3.4 请求-响应数据格式

// GET /get/game/1 返回示例(揭幕战)
{
  "id": "1",
  "home_team_name_en": "Mexico",
  "away_team_name_en": "South Africa",
  "home_score": "2",
  "away_score": "0",
  "group": "A",
  "matchday": "1",
  "stadium_id": "1",
  "finished": "TRUE",
  "type": "group"
}
// GET /auth/authenticate 返回示例
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": "84d"
}

四、完整代码实现

4.1 环境准备

pip install requests pandas matplotlib numpy

4.2 API 封装类(完整可复制)

import requests
import matplotlib
import matplotlib.font_manager as fm
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from typing import Optional, Dict, List


class WorldCupAPI:
    """2026 世界杯数据 API 封装"""
    
    def __init__(self, base_url: str = "https://worldcup26.ir"):
        self.base_url = base_url
        self.token: Optional[str] = None
        self.email: str = ""
        self.password: str = ""
        self.session = requests.Session()
    
    def login(self, email: str, password: str) -> Dict:
        """登录获取 Token(有效期 84 天)"""
        self.email = email
        self.password = password
        resp = self.session.post(
            f"{self.base_url}/auth/authenticate",
            json={"email": email, "password": password},
            timeout=10
        )
        data = resp.json()
        self.token = data.get("token")
        print(f"登录{'成功' if self.token else '失败'}")
        return data
    
    def _headers(self) -> Dict:
        return {"Authorization": f"Bearer {self.token}"} if self.token else {}
    
    def _safe_request(self, url: str, **kwargs) -> requests.Response:
        """封装请求,401 自动重新登录"""
        resp = self.session.request(
            url=url, headers=self._headers(), timeout=10, **kwargs
        )
        if resp.status_code == 401:
            print("Token 过期,正在重新登录...")
            self.login(self.email, self.password)
            resp = self.session.request(
                url=url, headers=self._headers(), timeout=10, **kwargs
            )
        return resp
    
    def get_all_matches(self) -> List[Dict]:
        """获取全部 104 场比赛"""
        resp = self._safe_request(f"{self.base_url}/get/games")
        return resp.json().get("games", [])
    
    def get_match(self, match_id: int) -> Dict:
        """获取单场比赛(id=1 为揭幕战)"""
        resp = self._safe_request(f"{self.base_url}/get/game/{match_id}")
        return resp.json()
    
    def get_teams_by_group(self, group: str) -> List[Dict]:
        """按小组获取球队"""
        resp = self._safe_request(
            f"{self.base_url}/get/teams/?group={group}"
        )
        data = resp.json()
        return data if isinstance(data, list) else []
    
    def get_groups(self) -> List[Dict]:
        """获取 12 个小组积分榜"""
        resp = self._safe_request(f"{self.base_url}/get/groups")
        data = resp.json()
        return data if isinstance(data, list) else []
    
    def get_all_stadiums(self) -> List[Dict]:
        """获取全部 16 个体育场"""
        resp = self._safe_request(f"{self.base_url}/get/stadiums")
        data = resp.json()
        return data if isinstance(data, list) else []
    
    def get_finished_matches(self) -> List[Dict]:
        """获取已完赛的比赛"""
        all_matches = self.get_all_matches()
        return [m for m in all_matches if m.get("finished") == "TRUE"]
    
    def get_match_statistics(self) -> Dict:
        """统计全部比赛的胜平负分布"""
        all_matches = self.get_all_matches()
        finished = [m for m in all_matches if m.get("finished") == "TRUE"]
        wins = sum(1 for m in finished 
                   if int(m.get("home_score", 0)) > int(m.get("away_score", 0)))
        draws = sum(1 for m in finished 
                    if int(m.get("home_score", 0)) == int(m.get("away_score", 0)))
        losses = len(finished) - wins - draws
        return {"total": len(all_matches), "finished": len(finished),
                "home_wins": wins, "draws": draws, "away_wins": losses}

4.3 中文字体处理工具

def setup_chinese_font():
    """自动检测并设置可用的中文字体"""
    cjk_fonts = [
        'Arial Unicode MS', 'SimHei', 'Noto Sans CJK SC',
        'WenQuanYi Micro Hei', 'PingFang SC', 'Heiti SC'
    ]
    available = [f.name for f in fm.fontManager.ttflist]
    for font in cjk_fonts:
        if font in available:
            matplotlib.rcParams['font.sans-serif'] = [font]
            matplotlib.rcParams['axes.unicode_minus'] = False
            print(f"使用字体: {font}")
            return font
    print("未找到中文字体,图表中文可能显示为方块")
    return None

4.4 红牌对比可视化

def plot_red_card_comparison():
    """历届世界杯揭幕战红牌数对比"""
    years = [1998, 2002, 2006, 2010, 2014, 2018, 2022, 2026]
    red_cards = [0, 0, 0, 0, 0, 0, 0, 3]
    
    fig, ax = plt.subplots(figsize=(12, 6))
    colors = ['#2196F3'] * 7 + ['#FF5722']
    bars = ax.bar(
        years, red_cards, color=colors, width=2.5,
        edgecolor='white', linewidth=0.5
    )
    
    for bar, val in zip(bars, red_cards):
        if val > 0:
            ax.text(
                bar.get_x() + bar.get_width() / 2,
                bar.get_height() + 0.15,
                f'{val}张', ha='center', va='bottom',
                fontsize=13, fontweight='bold',
                color='#FF5722' if val >= 3 else '#333'
            )
    
    ax.set_title(
        '历届世界杯揭幕战红牌数对比\n2026 揭幕战单场 3 张 = 前 7 届总和',
        fontsize=16, fontweight='bold', pad=20
    )
    ax.set_ylabel('红牌数(张)', fontsize=12)
    ax.set_xlabel('世界杯年份', fontsize=12)
    ax.set_ylim(0, 4)
    ax.yaxis.set_major_locator(plt.MaxNLocator(integer=True))
    ax.grid(axis='y', alpha=0.3)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    
    mean_val = np.mean(red_cards)
    ax.axhline(y=mean_val, color='#999', linestyle='--', linewidth=1, alpha=0.7)
    ax.text(
        2026.5, mean_val + 0.1,
        f'历史均值: {mean_val:.1f}张',
        fontsize=10, color='#999', ha='right'
    )
    
    plt.tight_layout()
    plt.savefig('worldcup_red_cards.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("红牌对比图已保存 -> worldcup_red_cards.png")

4.5 A 组积分榜可视化

def plot_group_a_standings():
    """A 组积分榜(揭幕战后)"""
    df = pd.DataFrame({
        '球队': ['Mexico', 'Czech Republic', 'South Africa', 'Korea Republic'],
        '积分': [3, 0, 0, 0],
        '进球': [2, 0, 0, 0],
        '失球': [0, 0, 0, 0],
    })
    
    fig, ax = plt.subplots(figsize=(10, 5))
    colors = ['#FF5722', '#2196F3', '#4CAF50', '#FFC107']
    bars = ax.barh(df['球队'], df['积分'], color=colors, height=0.5)
    
    for bar, pts, gf, ga in zip(
        bars, df['积分'], df['进球'], df['失球']
    ):
        ax.text(
            bar.get_width() + 0.1,
            bar.get_y() + bar.get_height() / 2,
            f'{pts}分 (进{gf}/失{ga})', va='center', fontsize=11
        )
    
    ax.set_title('A 组积分榜(揭幕战后)', fontsize=14, fontweight='bold')
    ax.set_xlim(0, 5)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.grid(axis='x', alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('group_a_standings.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("积分榜已保存 -> group_a_standings.png")

4.6 全部小组积分榜批量生成

def plot_all_groups_standings():
    """批量生成 12 个小组积分榜"""
    api = WorldCupAPI()
    api.login("your_email@example.com", "your_password")
    
    groups = list("ABCDEFGHIJKL")  # 12 个小组
    fig, axes = plt.subplots(4, 3, figsize=(18, 20))
    axes = axes.flatten()
    
    group_colors = [
        '#FF5722', '#2196F3', '#4CAF50', '#FFC107',
        '#9C27B0', '#00BCD4', '#FF9800', '#795548',
        '#607D8B', '#E91E63', '#3F51B5', '#009688'
    ]
    
    for i, group_name in enumerate(groups):
        teams = api.get_teams_by_group(group_name)
        if not teams:
            continue
        
        ax = axes[i]
        team_names = [t.get('name_en', t.get('name', '?')) for t in teams[:4]]
        points = [int(t.get('points', 0)) for t in teams[:4]]
        
        bars = ax.barh(team_names, points, color=group_colors[i], height=0.5)
        ax.set_title(f'Group {group_name}', fontsize=12, fontweight='bold')
        ax.set_xlim(0, max(points) + 2 if max(points) > 0 else 5)
        
        for bar, pts in zip(bars, points):
            ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2,
                    f'{pts}', va='center', fontsize=10)
    
    plt.suptitle('2026 世界杯 12 个小组积分榜', fontsize=18, fontweight='bold', y=1.01)
    plt.tight_layout()
    plt.savefig('all_groups_standings.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("12 组积分榜已保存 -> all_groups_standings.png")

4.7 主流程(一键运行)

def main():
    """一键运行:登录 -> 拉数据 -> 出图"""
    setup_chinese_font()
    
    api = WorldCupAPI()
    api.login("your_email@example.com", "your_password")
    
    # 1. 获取揭幕战数据并打印
    match = api.get_match(1)
    print(f"\n{'='*40}")
    print(f"  2026 世界杯揭幕战")
    print(f"  {match.get('home_team_name_en', '?')} "
          f"{match.get('home_score', '?')} - "
          f"{match.get('away_score', '?')} "
          f"{match.get('away_team_name_en', '?')}")
    print(f"  小组: Group {match.get('group', '?')}")
    print(f"{'='*40}")
    
    # 2. 生成红牌对比图
    plot_red_card_comparison()
    
    # 3. 生成 A 组积分榜
    plot_group_a_standings()
    
    # 4. 赛事全景统计
    stats = api.get_match_statistics()
    print(f"\n赛事总览: 共 {stats['total']} 场比赛")
    print(f"  已完成: {stats['finished']} 场")
    print(f"  待进行: {stats['total'] - stats['finished']} 场")
    print(f"  主场胜: {stats['home_wins']} | 平局: {stats['draws']} "
          f"| 客场胜: {stats['away_wins']}")
    
    # 5. 体育场信息
    stadiums = api.get_all_stadiums()
    if stadiums:
        total_cap = sum(int(s.get('capacity', 0)) for s in stadiums)
        print(f"16 个体育场 | 总容量: {total_cap:,} 座")
    
    # 6. 批量生成全部小组积分榜(可选)
    # plot_all_groups_standings()


if __name__ == "__main__":
    main()

五、运行效果

登录成功
使用字体: Arial Unicode MS

========================================
  2026 世界杯揭幕战
  Mexico 2 - 0 South Africa
  小组: Group A
========================================
红牌对比图已保存 -> worldcup_red_cards.png
积分榜已保存 -> group_a_standings.png

赛事总览: 共 104 场比赛
  已完成: 1 场
  待进行: 103 场
  主场胜: 1 | 平局: 0 | 客场胜: 0
16 个体育场 | 总容量: 1,254,000+ 座

在这里插入图片描述


六、踩坑记录(5 个真实坑)

坑 1:API 不返回红黄牌等详细统计

worldcup26.ir 的 API 返回了比分、球队、赛程、体育场,但不包含红黄牌、控球率、射门数等详细统计

终端报错(如果期望字段不存在):

KeyError: 'red_cards'

怎么处理:对于红牌数量,从新闻报道(BBC/ESPN/FIFA 官网)手动获取并写入代码中的硬编码数组。如果后续比赛需要深度数据,可以接入 footballapi.com 的付费接口(提供 xG、射门、传球等)。

坑 2:Token 过期静默失败

API 的 JWT Token 有效期 84 天,但过期后不会返回明确的「Token 过期」错误,而是直接返回空数据或 HTTP 401。

终端现象

# 调用 get_match(1) 返回的不是空字典,而是:
{
  "statusCode": 401,
  "message": "Unauthorized"
}
# 但 requests 不会抛异常,因为 HTTP 状态码是 200

怎么处理:封装 _safe_request 方法,检测到 401 状态码自动重新登录(代码见 4.2 节 API 封装类)。

坑 3:matplotlib 中文显示方块

老生常谈的问题,但每次换环境都要踩一遍。macOS 上 Arial Unicode MS 一般可用,但部署到 Linux 服务器就挂了。

终端现象

# 图表标题显示为:
□□□□□□□□□□□□
# 而不是:
历届世界杯揭幕战红牌数对比

怎么处理:用 matplotlib.font_manager 动态检测可用中文字体(代码见 4.3 节)。如果实在找不到中文字体,降级为英文标签:ax.set_title('Red Card Comparison')

坑 4:API 跨域限制

本地跑 Python 脚本没问题,但如果想做成前端页面直接调 API,会撞跨域(CORS)墙。

终端现象(在浏览器控制台看到):

Access to XMLHttpRequest at 'https://worldcup26.ir/get/games' 
from origin 'http://localhost:3000' has been blocked by CORS policy

怎么处理:搭建一个 Flask 代理层,后端转发请求:

from flask import Flask, jsonify
import requests

app = Flask(__name__)
API_BASE = "https://worldcup26.ir"
TOKEN = "your_token_here"

@app.route('/api/<path:endpoint>')
def proxy(endpoint):
    resp = requests.get(
        f"{API_BASE}/{endpoint}",
        headers={"Authorization": f"Bearer {TOKEN}"}
    )
    return jsonify(resp.json())

坑 5:requests 返回 200 但 body 是错误

这是最容易忽略的坑。API 在某些异常情况下(如参数错误、Token 过期)返回 HTTP 200,但 body 里是错误信息。requests 不会抛异常,你以为请求成功了,其实是空数据。

终端现象

import requests
resp = requests.get("https://worldcup26.ir/get/game/999")
print(resp.status_code)  # 200
print(resp.json())       # {"message": "Game not found"}

怎么处理:在 _safe_request 里增加 body 校验,如果返回的 JSON 包含 messagestatusCode 字段,主动抛出异常或重试。


七、项目文件结构

worldcup2026-dashboard/
├── main.py                    # 主程序(4.7 节完整代码)
├── worldcup_api.py            # API 封装类(4.2 节代码)
├── visualizations.py          # 可视化函数(4.4/4.5/4.6 节代码)
├── font_utils.py              # 字体工具(4.3 节代码)
├── requirements.txt           # 依赖清单
├── worldcup_red_cards.png     # 运行生成:红牌对比图
├── group_a_standings.png      # 运行生成:积分榜
├── all_groups_standings.png   # 运行生成:12 组积分榜(可选)
└── README.md

八、完整命令速查

# 1. 安装依赖
pip install requests pandas matplotlib numpy

# 2. 运行主程序
python main.py

# 3. 单独生成红牌对比图(无需 API)
python -c "from visualizations import plot_red_card_comparison; plot_red_card_comparison()"

# 4. 单独生成 A 组积分榜(需先运行 main.py 登录)
python -c "from visualizations import plot_group_a_standings; plot_group_a_standings()"

# 5. 批量生成全部小组积分榜
python -c "from visualizations import plot_all_groups_standings; plot_all_groups_standings()"

# 6. 查看赛事统计
python -c "
from worldcup_api import WorldCupAPI
api = WorldCupAPI()
api.login('email', 'password')
stats = api.get_match_statistics()
print(stats)
"

九、总结

步骤 工具 输出 耗时
1. 注册 API worldcup26.ir API Token 2 分钟
2. 安装依赖 pip Python 环境 1 分钟
3. 封装请求 requests + WorldCupAPI 类 结构化 JSON 数据 10 分钟
4. 数据处理 pandas DataFrame 清洗后的表格 5 分钟
5. 可视化 matplotlib 两张 PNG 图表 10 分钟
6. 扩展(可选) Flask 代理 Web 面板 30 分钟

收藏本文,世界杯期间每天跑一遍 python main.py,自动生成最新数据面板。


参考链接

  1. World Cup 2026 API - GitHub
  2. matplotlib 官方文档 - Bar Chart

在这里插入图片描述

更多推荐