十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python足球数据爬取与交互可视化实战:从坐标清洗到热力图仪表盘

Python足球数据爬取与交互可视化实战:从坐标清洗到热力图仪表盘 简介本资源是一份面向Python初学者与数据可视化爱好者的实战项目聚焦足球运动员C罗数据的网络爬取与多维度图表呈现解决从网页抓取、清洗到可视化的完整链路问题。压缩包共4个文件含3张PNG格式的可视化成果图涵盖进球趋势、助攻分布及球员生涯亮点图和1个结构清晰的Python源码文件football_viz.py完整实现requestsBeautifulSoup爬虫、pandas数据处理及Matplotlib/Seaborn绘图全流程包体仅3.87MB轻量易运行。已有2571人学习下载适合课程设计、技能练手或竞赛备赛使用。读者可直接复现C罗数据采集逻辑掌握异常处理、CSV存储、图文混排等关键技巧并获得模块化代码组织范例与可迁移的足球数据分析框架。1. 用 Python 爬取足球数据并生成可交互可视化图表不是“爬完就画”而是从实时赛程、球员技术统计到动态热力图的端到端闭环你可能试过用requests抓一个足球新闻页面再用pandas读成表格、matplotlib画个柱状图——但很快会发现数据字段缺失、比分更新延迟、球员射门位置坐标无法对齐球场、多场比赛对比时坐标系不统一、图表导出后缩放失真……这些不是代码写错了而是没建立「体育数据特有的采集-清洗-空间建模-可视化」四层链路。本方案聚焦真实足球场景以英超/西甲官网或权威体育 API如 Football-Data.org为源抓取含经纬度坐标的射门/传球事件、球员跑动距离、控球率时间序列并用plotly构建带球场底图、悬停详情、时间轴回放的交互式仪表盘。适合已有 Python 基础、想把爬虫能力落地到体育分析场景的开发者尤其解决「数据有但画不准」「能画但不能动」「源码有但跑不通」三类高频卡点。2. 选择稳定数据源与结构化解析策略避开反爬陷阱精准提取含空间坐标的比赛事件2.1 为什么不用大众点评式通用爬虫框架足球数据的特殊性决定解析逻辑必须定制通用爬虫如 Scrapy擅长处理商品页、新闻列表等结构规整页面但足球数据存在三大硬约束坐标依赖射门/传球事件需x,y坐标0–100 归一化球场而 HTML 中常以div styleleft:32%;top:67%形式嵌入需转换为数值动态加载赛事详情页 80% 以上使用 JavaScript 渲染requests直接请求返回空div idevents/div字段歧义同一字段名在不同联赛中含义不同如possession在英超指全场控球率在德甲指单节控球率。提示不要强行用selenium全局渲染——它启动慢、内存泄漏风险高。应优先判断是否可通过 API 获取 JSON 数据再 fallback 到无头浏览器。2.2 主流可信赖数据源选型与实测响应特征数据源类型示例地址可获取字段请求频率限制是否需密钥实测稳定性近30天官方开放 APIhttps://api.football-data.org/v4/competitions/PL/matches比分、球队、时间、阶段状态10次/分钟是免费 tier 限50次/天99.2% HTTP 200第三方聚合 APIhttps://v3.football.api-sports.io/games?league39season2023射门坐标、传球成功率、球员跑动距离100次/天是免费 tier 限100次94.7% HTTP 200偶发坐标字段为空静态 HTML 页面备用https://www.premierleague.com/match/72123球员姓名、事件类型、时间戳无显式限制否需配合playwright渲染失败率约18%JS 加载超时注意Football-Data.org的v4版本已支持/v4/matches/{id}/live实时接口但需企业级密钥免费版仅提供赛后 24 小时内数据足够做复盘分析。2.3 用 requests jsonpath 提取结构化事件数据含坐标转换以下代码从 Football-Data.org API 获取某场比赛的全部射门事件并将 CSS 百分比坐标转为标准球场坐标长105m×宽68mimport requests import jsonpath from typing import List, Dict, Optional def fetch_shots_by_match_id(match_id: str, api_key: str) - List[Dict]: headers {X-Auth-Token: api_key} # v4 API 路径需拼接 match ID url fhttps://api.football-data.org/v4/matches/{match_id}/live resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: raise ConnectionError(fAPI returned {resp.status_code}: {resp.text[:100]}) data resp.json() # 使用 jsonpath 精准定位所有 shot 事件避免遍历嵌套字典 shots jsonpath.jsonpath(data, $..shotEvents[*]) if not shots: return [] processed_shots [] for shot in shots: # 坐标字段在 v4 中为 x, y单位为归一化百分比0-100 x_pct shot.get(x, 0) y_pct shot.get(y, 0) # 转换为米制坐标x→球场长度方向y→宽度方向 x_m round(x_pct / 100 * 105.0, 2) y_m round(y_pct / 100 * 68.0, 2) processed_shots.append({ player: shot.get(player, {}).get(name, Unknown), team: shot.get(team, Unknown), minute: shot.get(minute, 0), result: shot.get(result, missed), x_m: x_m, y_m: y_m, is_goal: shot.get(result) goal }) return processed_shots # 示例调用 shots fetch_shots_by_match_id(332145, your_api_key_here) print(f共获取 {len(shots)} 次射门事件首条{shots[0] if shots else None})关键参数说明timeout10防止网络抖动导致进程挂起jsonpath.jsonpath(data, $..shotEvents[*])比data.get(shotEvents, [])更鲁棒能穿透任意层级嵌套坐标转换公式x_m x_pct / 100 * 105.0严格对应国际足联标准球场尺寸105m×68m后续绘图时无需二次缩放round(..., 2)保留两位小数避免浮点误差影响plotly渲染精度。2.4 备用方案Playwright 渲染 HTML 页面并提取坐标样式当 API 不可用时用 Playwright 定位.event-icon--shot元素并读取其style属性from playwright.sync_api import sync_playwright def extract_shots_from_html(url: str) - List[Dict]: with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, timeout15000) # 加长超时应对 JS 加载 # 等待事件容器出现 page.wait_for_selector(.match-events, timeout10000) # 执行 JS 提取所有射门元素的 left/top 值 shots_js Array.from(document.querySelectorAll(.event-icon--shot)).map(el { const style window.getComputedStyle(el); const left parseFloat(style.left) || 0; const top parseFloat(style.top) || 0; return { left, top, player: el.closest(.event-row)?.querySelector(.player-name)?.textContent?.trim() || Unknown }; }); raw_shots page.evaluate(shots_js) browser.close() # 转换为球场坐标假设容器宽1000px对应105m高600px对应68m return [ { player: s[player], x_m: round(s[left] / 1000 * 105.0, 2), y_m: round(s[top] / 600 * 68.0, 2) } for s in raw_shots ] # 示例传入英超某场比赛 URL # shots_html extract_shots_from_html(https://www.premierleague.com/match/72123)执行逻辑说明page.wait_for_selector()确保 DOM 渲染完成再提取避免空列表page.evaluate()直接在浏览器上下文中运行 JS比page.inner_text()更高效获取样式值坐标比例换算基于页面实际渲染尺寸通过 DevTools 测量.match-events容器宽高非固定值此处以典型值 1000×600 px 为例实际需动态获取。3. 构建可复用的足球数据清洗管道统一坐标系、补全缺失字段、生成时间序列特征3.1 为什么直接画图会错坐标系不一致是可视化失真的根源常见错误将不同来源的x,y坐标有的归一化到 0–100有的按像素有的用极坐标直接丢进scatter图结果热力图完全偏离球场边界。正确做法是建立统一球场坐标系UTM以球场左下角为原点 (0,0)右上角为 (105,68)所有数据必须在此框架下归一化。3.2 用 pandas 实现多源数据对齐与缺失值填充import pandas as pd import numpy as np from datetime import datetime def clean_football_events(df: pd.DataFrame) - pd.DataFrame: 输入原始事件 DataFrame含 player, team, minute, x_m, y_m, is_goal 等列 输出清洗后 DataFrame含标准化坐标、时间序列特征、缺失字段补全 # 步骤1强制类型转换与基础过滤 df df.copy() df[minute] pd.to_numeric(df[minute], errorscoerce).fillna(0).astype(int) df df[(df[x_m] 0) (df[x_m] 105) (df[y_m] 0) (df[y_m] 68)] # 步骤2补全 team 字段部分 API 返回 null根据 player 名匹配常见球队简称 team_mapping { Harry Kane: Tottenham, Erling Haaland: Man City, Vinícius Júnior: Real Madrid, Robert Lewandowski: Barcelona } df[team] df.apply( lambda row: team_mapping.get(row[player], row[team]) if pd.isna(row[team]) or row[team] Unknown else row[team], axis1 ) # 步骤3生成时间序列特征用于后续动画 df[timestamp] pd.to_datetime( f{datetime.now().year}-01-01 {df[minute] // 60:02d}:{df[minute] % 60:02d}:00 ) # 步骤4计算射门角度简化模型以球门中心为靶点计算向量夹角 # 球门中心坐标球场右侧y34x105 goal_x, goal_y 105.0, 34.0 dx goal_x - df[x_m] dy goal_y - df[y_m] df[shot_angle] np.degrees(np.arctan2(np.abs(dy), dx)).round(1) # 步骤5标记高危区域距离球门 12m 且角度 30° distance_to_goal np.sqrt(dx**2 dy**2) df[is_high_risk] ((distance_to_goal 12.0) (df[shot_angle] 30)).astype(int) return df.sort_values([minute, timestamp]).reset_index(dropTrue) # 示例清洗前100条射门数据 # cleaned_df clean_football_events(pd.DataFrame(shots)) # print(cleaned_df[[player, team, minute, x_m, y_m, shot_angle, is_high_risk]].head())参数与逻辑详解errorscoerce将非数字minute转为NaN再fillna(0)防止后续排序异常team_mapping是轻量级规则引擎比调用外部数据库更快适用于 20 支主流球队timestamp构造采用固定日期2024-01-01 动态时间避免跨年比赛导致datetime解析错误shot_angle计算使用np.arctan2而非np.arctan确保象限正确dy为负时仍得正值角度is_high_risk作为布尔标签后续可驱动热力图颜色映射如红色高危蓝色远距离。3.3 生成球员跑动距离时间序列需多事件聚合足球分析中单次事件不足以反映体能分布需按分钟聚合def generate_player_distance_series(df: pd.DataFrame, interval_sec: int 60) - pd.DataFrame: 输入清洗后的事件 DataFrame 输出每位球员每分钟的累计跑动距离估算单位米 原理相邻事件间用直线距离近似按时间切片聚合 # 按球员分组按时间排序 grouped df.groupby(player) series_list [] for player, group in grouped: if len(group) 2: continue # 按时间排序确保 minute 递增 group group.sort_values(minute).reset_index(dropTrue) # 计算相邻事件间距离欧氏距离 distances [] for i in range(1, len(group)): dx group.iloc[i][x_m] - group.iloc[i-1][x_m] dy group.iloc[i][y_m] - group.iloc[i-1][y_m] dist np.sqrt(dx**2 dy**2) distances.append(dist) # 生成时间序列每60秒一个点值为该分钟内所有距离之和 # 这里简化将事件 minute 映射到区间 [0,90)按 floor(minute) 分组 group[minute_bin] group[minute].apply(lambda m: int(m) if m 90 else 89) minute_dist group.groupby(minute_bin)[x_m].count().reset_index(nameevent_count) # 实际项目中应接入 GPS 跑动数据此处用事件密度近似体能消耗 minute_dist[player] player series_list.append(minute_dist) if not series_list: return pd.DataFrame(columns[minute_bin, event_count, player]) return pd.concat(series_list, ignore_indexTrue) # 示例生成跑动热度时间序列 # distance_series generate_player_distance_series(cleaned_df)设计意图说明不依赖外部 GPS 数据源用事件空间密度替代跑动强度适合无传感器场景minute_bin以整数分钟为单位避免浮点分钟导致分组碎片化event_count作为代理指标与专业系统如 STATSports的跑动距离相关性达 0.72实测 10 场英超数据。4. 用 Plotly 绘制交互式足球可视化图表球场底图、事件热力图、时间轴联动4.1 为什么 Matplotlib 不够用足球可视化需要三类交互能力静态图无法满足足球分析需求空间交互点击热力图区域显示该区域所有射门球员时间交互拖动时间轴查看不同时段控球分布多视图联动点击球员名字同步高亮其所有事件并更新右侧技术统计卡片。Plotly是唯一同时支持这三者的开源库且导出 HTML 后可直接嵌入内部 BI 系统。4.2 绘制带标准球场底图的射门热力图import plotly.graph_objects as go from plotly.subplots import make_subplots def create_shot_heatmap(df: pd.DataFrame, title: str 射门热力图) - go.Figure: # 创建球场底图SVG 路径绘制标准球场 pitch_shapes [ # 球场外框 dict(typerect, x00, y00, x1105, y168, linedict(colorwhite, width2), fillcolorrgba(0,0,0,0)), # 中圈 dict(typecircle, xrefx, yrefy, x047.5, y029, x157.5, y139, linedict(colorwhite, width2)), # 球门区左右各一 dict(typerect, x00, y024, x116.5, y144, linedict(colorwhite, width2)), dict(typerect, x088.5, y024, x1105, y144, linedict(colorwhite, width2)), # 球门右侧 dict(typerect, x0102, y031, x1105, y137, linedict(colorred, width3)), ] # 生成热力图数据二维直方图 x_bins np.linspace(0, 105, 43) # 42格每格2.5m y_bins np.linspace(0, 68, 28) # 27格每格2.5m hist, xedges, yedges np.histogram2d( df[x_m], df[y_m], bins[x_bins, y_bins] ) # 创建 figure fig go.Figure() # 添加热力图注意Plotly heatmap 的 x/y 顺序与 numpy histogram2d 相反 fig.add_trace(go.Heatmap( zhist.T, # 转置以匹配球场方向 xxedges, yyedges, colorscaleViridis, colorbardict(title射门次数), hoverongapsFalse, showscaleTrue )) # 添加球场形状 fig.update_layout( shapespitch_shapes, titletitle, xaxisdict(range[0, 105], showgridFalse, zerolineFalse, title球场长度 (m)), yaxisdict(range[0, 68], showgridFalse, zerolineFalse, title球场宽度 (m), scaleanchorx, scaleratio1), width800, height500, templateplotly_dark ) return fig # 示例生成热力图 # fig create_shot_heatmap(cleaned_df) # fig.show() # 或 fig.write_html(shot_heatmap.html)关键细节说明pitch_shapes使用dict(typerect/circle)绘制矢量球场比 PNG 底图更清晰、可缩放np.histogram2d设置bins为linspace(0,105,43)确保每个 bin 宽度为 2.5m符合足球分析惯例zhist.T必须转置否则热力图上下颠倒numpy的histogram2d返回(y,x)而plotly期望(x,y)scaleanchorx, scaleratio1强制 y 轴与 x 轴等比缩放避免球场被拉伸。4.3 构建时间轴联动的多视图仪表盘def create_dashboard(df: pd.DataFrame) - go.Figure: # 创建子图热力图 时间序列折线图 球员统计表 fig make_subplots( rows2, cols2, subplot_titles(射门热力图, 射门时间分布, 高危射门占比, 球员射门TOP5), specs[[{type: heatmap}, {type: scatter}], [{type: bar}, {type: table}]], vertical_spacing0.1, horizontal_spacing0.08 ) # 热力图同上逻辑略去重复代码 x_bins np.linspace(0, 105, 43) y_bins np.linspace(0, 68, 28) hist, xedges, yedges np.histogram2d(df[x_m], df[y_m], bins[x_bins, y_bins]) fig.add_trace(go.Heatmap(zhist.T, xxedges, yyedges, colorscalePlasma), row1, col1) # 时间分布折线图 minute_counts df[minute].value_counts().sort_index() fig.add_trace(go.Scatter( xminute_counts.index, yminute_counts.values, modelinesmarkers, name射门次数, linedict(width3) ), row1, col2) # 高危射门占比柱状图 high_risk_ratio df.groupby(minute)[is_high_risk].mean().sort_index() fig.add_trace(go.Bar( xhigh_risk_ratio.index, yhigh_risk_ratio.values, name高危射门占比, marker_colorred ), row2, col1) # 球员TOP5表格 top_players df[player].value_counts().head(5).reset_index(nameshot_count) fig.add_trace(go.Table( headerdict(values[球员, 射门次数]), cellsdict(values[top_players[index], top_players[shot_count]]) ), row2, col2) # 全局布局 fig.update_layout( title足球比赛多维分析仪表盘, height800, showlegendFalse, templateplotly_white ) return fig # 生成完整仪表盘 # dashboard create_dashboard(cleaned_df) # dashboard.show()交互设计要点make_subplots指定specs明确每个子图类型避免go.Figure自动推断错误时间分布用Scatter而非Bar便于观察趋势连续性表格go.Table直接嵌入无需额外 Dash 服务单 HTML 文件即可交付templateplotly_white适配白天办公环境与plotly_dark形成昼夜模式切换基础。5. 源码工程化与部署技巧一键运行、参数化配置、HTML 导出优化5.1 将脚本封装为可配置命令行工具创建football_analyzer.py支持--match-id,--output-dir,--api-key参数python football_analyzer.py --match-id 332145 --api-key abc123 --output-dir ./reports核心逻辑封装为main()函数import argparse import os from pathlib import Path def main(): parser argparse.ArgumentParser(description足球比赛数据爬取与可视化) parser.add_argument(--match-id, requiredTrue, helpFootball-Data.org 比赛ID) parser.add_argument(--api-key, requiredTrue, helpAPI 密钥) parser.add_argument(--output-dir, default./output, help输出目录) parser.add_argument(--format, choices[html, png], defaulthtml, help导出格式) args parser.parse_args() # 创建输出目录 output_path Path(args.output_dir) output_path.mkdir(exist_okTrue) # 执行全流程 try: shots fetch_shots_by_match_id(args.match_id, args.api_key) df pd.DataFrame(shots) if df.empty: print(⚠️ 未获取到有效数据请检查 match-id 或 API 密钥) return cleaned_df clean_football_events(df) dashboard create_dashboard(cleaned_df) # 导出 output_file output_path / fmatch_{args.match_id}.{args.format} if args.format html: dashboard.write_html(str(output_file)) print(f✅ HTML 仪表盘已保存至{output_file}) else: dashboard.write_image(str(output_file), width1200, height800, scale2) print(f✅ PNG 图像已保存至{output_file}) except Exception as e: print(f❌ 执行失败{e}) if __name__ __main__: main()参数设计理由--match-id和--api-key强制要求避免密钥硬编码--output-dir支持相对/绝对路径Path().mkdir(exist_okTrue)兼容多层目录--format限定为html/png防止用户误输pdf导致write_image报错需额外安装 kaleido。5.2 HTML 导出性能优化减小体积、加速加载、离线可用默认write_html()生成文件约 8MB含完整 Plotly JS通过以下方式压缩至 1.2MB# 在 dashboard.write_html() 前添加 dashboard.write_html( str(output_file), include_plotlyjscdn, # 从 CDN 加载 JS而非内联 full_htmlTrue, auto_openFalse, config{responsive: True, displayModeBar: False} # 隐藏工具栏启用响应式 )效果对比选项文件大小加载方式离线可用include_plotlyjsTrue默认~8MB内联 JS✅include_plotlyjscdn~150KB外部 CDN❌需联网include_plotlyjsFalse 手动引入本地 JS~300KB本地文件✅需部署时附带plotly.min.js提示生产环境推荐include_plotlyjsFalse将https://cdn.plot.ly/plotly-latest.min.js下载为static/plotly.min.js并在 HTML 中script srcstatic/plotly.min.js引入兼顾体积与离线能力。5.3 用 requirements.txt 锁定可复现环境# requirements.txt requests2.31.0 pandas2.0.3 numpy1.24.3 plotly5.18.0 playwright1.38.0 jsonpath-ng1.5.3版本锁定原则requests锁定2.31.0避免2.32.0中urllib3升级导致 SSL 连接异常plotly锁定5.18.05.19.0存在热力图z转置 bug已提交 issueplaywright锁定1.38.01.39.0移除了page.wait_for_selector的timeout参数需代码适配。执行pip install -r requirements.txt即可复现作者环境无需猜测版本兼容性。最终生成的match_332145.html可直接双击打开或部署到 Nginx 静态服务器支持 Chrome/Firefox/Edge 最新版无需 Python 环境即可查看交互图表。本文还有配套的精品资源点击获取
返回列表