世界杯进入淘汰赛后,赛程展示方式就变了。
小组赛适合用表格:
- 哪个小组
- 哪几支球队
- 积分多少
- 净胜球多少
但淘汰赛更适合用"路径图":
- 谁对谁
- 胜者进入哪一轮
- 下一场可能遇到谁
- 哪条半区更拥挤
这类结构如果只用文字写,会很乱。
比如今天 32 强赛有几场重点比赛:
- 科特迪瓦 vs 挪威
- 法国 vs 瑞典
- 墨西哥 vs 厄瓜多尔
如果只是写成列表,信息量很低。
但如果画成对阵树,读者一眼就能看懂"这些比赛是淘汰赛路径的一部分"。
所以今天这篇文章不写传统球评,也不写分析报告。
我们用 Python 画一个简单的世界杯淘汰赛对阵树。
注意:本文只做赛程可视化,不涉及博彩、赔率、下注,也不做胜负预测。

一、实现目标
我们要写一个小脚本,实现 4 件事:
- 维护今日淘汰赛对阵数据
- 用 matplotlib 绘制比赛卡片
- 用线条表示晋级路径
- 保存成 PNG 图片
最终生成:
worldcup_knockout_bracket.png
这个图片可以用于:
- CSDN 技术文章
- 世界杯赛程复盘
- 公众号配图
- 今日头条信息图
- 自己的体育数据看板
这比把赛程复制成纯文字列表强一点。人类终于想起图形化展示不是摆设了,可喜可贺。
二、安装依赖
本文只需要 matplotlib:
pip install matplotlib
检查版本:
python -c "import matplotlib; print(matplotlib.__version__)"
建议使用 Python 3.9 以上版本。
三、准备对阵数据
创建文件:
worldcup_knockout_bracket.py
先准备今日比赛数据:
matches = [
{
"id": "m1",
"round": "Round of 32",
"home": "科特迪瓦",
"away": "挪威",
"note": "锋线效率 vs 身体对抗",
},
{
"id": "m2",
"round": "Round of 32",
"home": "法国",
"away": "瑞典",
"note": "强队破局能力测试",
},
{
"id": "m3",
"round": "Round of 32",
"home": "墨西哥",
"away": "厄瓜多尔",
"note": "主场压力 vs 转换冲击",
},
]
这里的 note 字段不是预测结果,只是比赛看点。
后续如果比赛结束,可以继续加字段:
"home_score": 2,
"away_score": 1,
"winner": "法国"
这样就能从"赛前对阵树"扩展成"赛后晋级图"。
四、设计画布坐标

淘汰赛对阵树本质上就是:
- 左侧:本轮比赛
- 右侧:下一轮占位
- 中间:晋级连线
我们先给每场比赛一个坐标。
layout = {
"m1": (0.1, 0.78),
"m2": (0.1, 0.50),
"m3": (0.1, 0.22),
"next_1": (0.65, 0.64),
"next_2": (0.65, 0.28),
}
其中:
m1、m2、m3是今日 32 强赛next_1、next_2是下一轮占位卡片
为了演示,我们让:
m1和m2的胜者路径靠近next_1m3单独连接到next_2
真实完整 bracket 会更复杂,但本文先做一个最小可运行版。
一开始就想画完整 32 队大图,代码会迅速变成线条意大利面,调坐标调到怀疑人生。先做小图,活得久一点。
五、绘制比赛卡片
先写一个函数,负责绘制单张比赛卡片:
def draw_match_card(ax, x, y, title, home, away, note):
width = 0.36
height = 0.16
rect = plt.Rectangle(
(x, y - height / 2),
width,
height,
linewidth=1.5,
edgecolor="#334155",
facecolor="#f8fafc",
zorder=2,
)
ax.add_patch(rect)
ax.text(
x + 0.02,
y + 0.045,
title,
fontsize=9,
color="#64748b",
va="center",
)
ax.text(
x + 0.02,
y,
f"{home} vs {away}",
fontsize=12,
fontweight="bold",
color="#0f172a",
va="center",
)
ax.text(
x + 0.02,
y - 0.045,
note,
fontsize=9,
color="#475569",
va="center",
)
这里用到了:
plt.Rectangle绘制卡片背景ax.text绘制文字zorder控制图层顺序
卡片内容包括:
- 轮次
- 对阵
- 看点说明
六、绘制晋级连线
接下来写一个连线函数:
def draw_connector(ax, start, end):
start_x, start_y = start
end_x, end_y = end
ax.plot(
[start_x, end_x],
[start_y, end_y],
color="#94a3b8",
linewidth=1.8,
zorder=1,
)
ax.scatter(
[end_x],
[end_y],
s=18,
color="#2563eb",
zorder=3,
)
这个函数只画一条简单直线。
正式 bracket 里可以画成折线,比如:
水平线 → 垂直线 → 水平线
但入门版本先用直线就够了。
别一开始就追求"像官方赛事图一样优雅",通常结果是图没画完,人先优雅地破防。
七、完整代码
下面是完整可运行代码:
python
from pathlib import Path
import matplotlib.pyplot as plt
def setup_chinese_font() -> None:
"""
设置中文字体。
如果中文显示异常,可以根据系统修改字体名称。
"""
plt.rcParams["font.sans-serif"] = [
"Microsoft YaHei",
"SimHei",
"Arial Unicode MS",
"DejaVu Sans",
]
plt.rcParams["axes.unicode_minus"] = False
def draw_match_card(ax, x, y, title, home, away, note):
"""
绘制比赛卡片。
"""
width = 0.36
height = 0.16
rect = plt.Rectangle(
(x, y - height / 2),
width,
height,
linewidth=1.5,
edgecolor="#334155",
facecolor="#f8fafc",
zorder=2,
)
ax.add_patch(rect)
ax.text(
x + 0.02,
y + 0.045,
title,
fontsize=9,
color="#64748b",
va="center",
)
ax.text(
x + 0.02,
y,
f"{home} vs {away}",
fontsize=12,
fontweight="bold",
color="#0f172a",
va="center",
)
ax.text(
x + 0.02,
y - 0.045,
note,
fontsize=9,
color="#475569",
va="center",
)
def draw_next_round_card(ax, x, y, title):
"""
绘制下一轮占位卡片。
"""
width = 0.26
height = 0.12
rect = plt.Rectangle(
(x, y - height / 2),
width,
height,
linewidth=1.5,
linestyle="--",
edgecolor="#2563eb",
facecolor="#eff6ff",
zorder=2,
)
ax.add_patch(rect)
ax.text(
x + width / 2,
y,
title,
fontsize=11,
fontweight="bold",
color="#1d4ed8",
ha="center",
va="center",
)
def draw_connector(ax, start, end):
"""
绘制晋级路径连线。
"""
start_x, start_y = start
end_x, end_y = end
ax.plot(
[start_x, end_x],
[start_y, end_y],
color="#94a3b8",
linewidth=1.8,
zorder=1,
)
ax.scatter(
[end_x],
[end_y],
s=18,
color="#2563eb",
zorder=3,
)
def draw_bracket(matches, output_file: str) -> None:
"""
绘制世界杯淘汰赛对阵树。
"""
setup_chinese_font()
fig, ax = plt.subplots(figsize=(11, 7))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
ax.text(
0.5,
0.95,
"世界杯 32 强淘汰赛对阵树",
fontsize=18,
fontweight="bold",
ha="center",
color="#0f172a",
)
ax.text(
0.5,
0.90,
"示例:今日三场 Round of 32 比赛路径可视化",
fontsize=11,
ha="center",
color="#64748b",
)
layout = {
"m1": (0.08, 0.76),
"m2": (0.08, 0.50),
"m3": (0.08, 0.24),
"next_1": (0.66, 0.63),
"next_2": (0.66, 0.24),
}
for match in matches:
x, y = layout[match["id"]]
draw_match_card(
ax=ax,
x=x,
y=y,
title=match["round"],
home=match["home"],
away=match["away"],
note=match["note"],
)
draw_next_round_card(ax, *layout["next_1"], title="胜者进入下一轮")
draw_next_round_card(ax, *layout["next_2"], title="胜者进入下一轮")
# 比赛卡片右侧出口坐标
m1_exit = (0.08 + 0.36, 0.76)
m2_exit = (0.08 + 0.36, 0.50)
m3_exit = (0.08 + 0.36, 0.24)
# 下一轮卡片左侧入口坐标
next_1_entry = (0.66, 0.63)
next_2_entry = (0.66, 0.24)
draw_connector(ax, m1_exit, next_1_entry)
draw_connector(ax, m2_exit, next_1_entry)
draw_connector(ax, m3_exit, next_2_entry)
ax.text(
0.5,
0.06,
"说明:本文只做赛程可视化示例,不预测胜负,不涉及博彩、赔率或下注。",
fontsize=10,
ha="center",
color="#64748b",
)
output_path = Path(output_file)
fig.tight_layout()
fig.savefig(output_path, dpi=180, bbox_inches="tight")
plt.close(fig)
print(f"淘汰赛对阵树已生成:{output_path.resolve()}")
def main() -> None:
matches = [
{
"id": "m1",
"round": "Round of 32",
"home": "科特迪瓦",
"away": "挪威",
"note": "锋线效率 vs 身体对抗",
},
{
"id": "m2",
"round": "Round of 32",
"home": "法国",
"away": "瑞典",
"note": "强队破局能力测试",
},
{
"id": "m3",
"round": "Round of 32",
"home": "墨西哥",
"away": "厄瓜多尔",
"note": "主场压力 vs 转换冲击",
},
]
draw_bracket(
matches=matches,
output_file="worldcup_knockout_bracket.png",
)
if __name__ == "__main__":
main()
八、运行脚本
执行:
python worldcup_knockout_bracket.py
运行后会生成:
worldcup_knockout_bracket.png
如果中文显示成方块,修改字体配置:
plt.rcParams["font.sans-serif"] = [
"Microsoft YaHei",
"SimHei",
"Arial Unicode MS",
"DejaVu Sans",
]
Windows 推荐:
"Microsoft YaHei"
macOS 可以尝试:
"Arial Unicode MS"
Linux 建议先安装中文字体。
九、改成读取 JSON 数据
如果不想把赛程写死在 Python 里,可以创建 matches.json:
[
{
"id": "m1",
"round": "Round of 32",
"home": "科特迪瓦",
"away": "挪威",
"note": "锋线效率 vs 身体对抗"
},
{
"id": "m2",
"round": "Round of 32",
"home": "法国",
"away": "瑞典",
"note": "强队破局能力测试"
},
{
"id": "m3",
"round": "Round of 32",
"home": "墨西哥",
"away": "厄瓜多尔",
"note": "主场压力 vs 转换冲击"
}
]
然后增加读取函数:
import json
from pathlib import Path
def load_matches(file_path: str):
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"文件不存在:{file_path}")
with path.open("r", encoding="utf-8") as file:
data = json.load(file)
if not isinstance(data, list):
raise ValueError("matches.json 顶层必须是数组")
return data
修改 main():
def main() -> None:
matches = load_matches("matches.json")
draw_bracket(
matches=matches,
output_file="worldcup_knockout_bracket.png",
)
这样每天只需要更新 JSON 文件,不用改绘图逻辑。
十、加入赛果和晋级球队
比赛结束后,可以把数据改成这样:
{
"id": "m2",
"round": "Round of 32",
"home": "法国",
"away": "瑞典",
"home_score": 2,
"away_score": 1,
"winner": "法国",
"note": "法国晋级下一轮"
}
然后在卡片里显示比分:
def format_match_title(match):
home = match["home"]
away = match["away"]
if "home_score" in match and "away_score" in match:
return f"{home} {match['home_score']} - {match['away_score']} {away}"
return f"{home} vs {away}"
把原来这行:
f"{home} vs {away}"
替换成:
format_match_title(match)
这样脚本可以从"赛前对阵图"升级成"赛后晋级图"。
十一、扩展成完整 32 强 bracket
完整 32 强对阵树需要更多信息:
rounds = {
"Round of 32": [...],
"Round of 16": [...],
"Quarter-finals": [...],
"Semi-finals": [...],
"Final": [...]
}
每一轮都可以用类似结构:
{
"id": "r32_m1",
"home": "加拿大",
"away": "南非",
"winner": "加拿大",
"next_match": "r16_m1"
}
然后根据 next_match 自动连线。
核心思路是:
- 每场比赛有唯一 ID
- 每场比赛知道自己通向哪一场
- 绘图时根据 ID 找坐标
- 用连线连接当前比赛和下一轮比赛
这是一个很适合继续扩展的小项目。
后续可以做成:
- 静态 bracket 图片
- Web 页面
- Streamlit 看板
- Vue/React 对阵表组件
- 自动更新的淘汰赛页面
技术内容终于不只是"把比赛写成一段话",而是开始有点工程味了。
十二、为什么不用 Graphviz?
Graphviz 很适合画图结构,但本文选择 matplotlib,有两个原因:
第一,安装门槛低。
很多环境里直接安装 matplotlib 就能跑。
第二,样式可控。
比赛卡片、字体、颜色、线条、说明文字,都可以手动调整。
如果你想做更复杂的结构图,也可以用 Graphviz。
安装:
pip install graphviz
但注意,很多系统还要额外安装 Graphviz 程序本体。
这就是它麻烦的地方。
Python 包装好了,系统依赖还在后面冷笑。开发者的一生就是这样,不是在装依赖,就是在怀疑依赖。
十三、可以怎么继续优化?
1. 自动生成文件名
from datetime import date
output_file = f"worldcup_bracket_{date.today().isoformat()}.png"
这样每天生成不同图片,不会覆盖旧文件。
2. 按比赛状态改变颜色
未开赛:
facecolor = "#f8fafc"
已结束:
facecolor = "#ecfdf5"
爆冷或点球:
facecolor = "#fef3c7"
3. 显示比赛时间
给数据加字段:
"kickoff": "2026-06-30 17:00"
卡片里显示:
ax.text(..., kickoff)
4. 生成中文和英文双版本
可以把 home、away 拆成:
"home_cn": "法国",
"home_en": "France"
根据参数输出不同语言版本。
5. 做成命令行工具
python worldcup_knockout_bracket.py --input matches.json --output bracket.png
用 argparse 接参数:
import argparse
这样就更像正式工具。
十四、总结
本文用 Python 和 matplotlib 实现了一个简单的世界杯淘汰赛对阵树可视化工具。
它完成了几件事:
- 维护 32 强赛对阵数据
- 绘制比赛卡片
- 绘制晋级路径连线
- 保存为 PNG 图片
- 支持后续改成 JSON 输入
- 支持扩展成完整淘汰赛 bracket
以今日 32 强赛为例:
- 科特迪瓦 vs 挪威
- 法国 vs 瑞典
- 墨西哥 vs 厄瓜多尔
这些比赛如果只写成列表,信息很平。
但画成对阵树之后,读者更容易理解淘汰赛路径。
世界杯热点不一定只能写球评。
也可以用 Python 把赛程变成图。
对 CSDN 来说,这种内容更适合:
有代码。
有图表。
有真实场景。
还能继续扩展成项目。
这才是技术人追世界杯热点的合理方式。
轻量补充
如果你长期使用 ChatGPT、Claude、Gemini、Cursor 或 Kiro 这类 AI 工具做代码生成、图表解释、文章改写和数据整理,也可以把 gpt68.com 作为第三方 AI 会员充值平台入口之一了解。它解决的是 AI 工具订阅充值流程问题,不是替代工具本身。使用前建议看清套餐说明、到账说明和售后规则。