从虎扑评分到赛果看板:Python数据分析自动化实战
NIP 2-1 战胜 WBG这个比分不只是赛后战报里的一行字也是虎扑评分评论区里上万人讨论的结果。对普通观众来说评分就是赛后的一个数字但对做赛果复盘、内容选题、数据产品或者单纯想练手 Python 数据处理的人来说这个评分和评论区本身就是一套可以自动化采集、清洗、分析和可视化的数据源。这篇文章不聊场外话题只聊一条可落地的数据处理链路把“NIP 2-1 WBG”这个赛果和虎扑评分数据变成一张可重复运行的赛果复盘看板。全流程会用到 Python、pandas、matplotlib、jieba、wordcloud 和 Flask不需要 GPU一台普通办公电脑就能跑完。你会看到如何从页面 Network 面板找到真实接口如何把接口返回变成干净的 DataFrame如何做关键词提取和简单情感分析最后如何用本地 API 把结果暴露给外部脚本调用。正文按“核心信息速览 - 适用场景与边界 - 环境准备 - 数据采集 - 功能测试 - 接口与批量任务 - 资源占用 - 常见问题 - 最佳实践 - 总结”展开。由于输入资料中只给出了确定赛果和评分平台没有提供具体比赛日期、选手数据和真实接口字段文章里的 URL、字段名和返回结构都按通用模板处理实际使用时需要用你浏览器 Network 面板里看到的内容替换。下面开始。1. 核心能力速览能力项说明赛果数据NIP 2-1 WBGNIP 获胜评分/评论来源虎扑评分具体接口需自行抓包确认技术栈Python 3.8、requests、pandas、matplotlib、jieba、wordcloud、Flask硬件要求CPU 即可内存 4G 以上无独立显卡要求启动方式命令行脚本 / Jupyter Notebook / Flask 服务主要功能赛果结构化、评分数据采集、评论清洗、关键词提取、情感分析、可视化看板接口能力可封装为本地 HTTP API供其他脚本调用批量任务支持多场次循环抓取结果自动追加导出到 CSV适合场景赛果复盘、自媒体选题、电竞数据工具、文本分析练习需要说明的是表格里的“支持批量任务”是指抓取和分析流程可以设计成循环调用而不是指某个现成平台给你提供批量接口。实际抓取能力取决于目标页面的反爬策略和接口稳定性第一次搭建议先用单场测试再逐步放开。2. 适用场景与使用边界2.1 适用场景赛果复盘把比赛结果、队伍胜场、评分均值放到一起快速生成一张赛后数据卡片。内容制作写赛评文章或做短视频脚本时用评分数据和评论区关键词辅助找选题。数据看板给战队粉丝群、校园电竞社或内部运营团队搭一个自动更新的赛事数据页面。学习练习通过真实网页数据分析练习 requests、pandas、jieba 分词、情感分析和 Flask 接口封装。这类项目的核心价值在于“把非结构化讨论变成结构化数据”。同样一套流程今天可以用来分析 NIP 对 WBG 的比赛明天换一个比赛 ID 就能分析另一场。你不需要手工刷新页面看评论脚本会定时把数据拉下来存好后续做任何图表和分析都从本地 CSV 读取。2.2 使用边界只采集公开页面内容不采集需要登录才能访问的非公开接口不绕过平台权限限制。不伪造评分、不刷分、不批量注册账号干扰评论区。赛后评论往往带有强烈主观情绪发布二次整理内容前要过滤攻击性、侮辱性文本不对用户进行人身评价。不要在公开内容中直接展示用户昵称和个人信息展示数据前先做脱敏。虎扑评分代表的是虎扑用户群体的观点不是官方赛事评价。写成文章或报告时要明确标注“虎扑用户评分”避免误导读者。使用边界这部分不是套话。评分数据、评论文本和赛事结果三者合并之后很容易变成一篇带有明显倾向性的内容。数据工具只负责把客观结果呈现出来哪些话该写、哪些结论不该下是使用者的责任。3. 环境准备与前置条件3.1 基础环境推荐使用 Python 3.8 到 3.11 之间的版本64 位系统。测试环境不需要 GPU但建议保持良好的网络连接因为要访问虎扑的公开接口。如果你本机已经安装过 Anaconda直接用 conda 创建虚拟环境也可以。python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate安装依赖库pip install requests pandas matplotlib jieba wordcloud flask snownlp如果下载速度很慢可以使用国内镜像源pip install requests pandas matplotlib jieba wordcloud flask snownlp -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 数据源准备先手动打开虎扑对应的比赛评分页面按 F12 打开开发者工具切换到 Network 面板刷新页面过滤 XHR/Fetch 请求找到返回 JSON 的接口。需要记录四个信息接口 URL。请求方法一般是 GET 或 POST。请求参数例如 match_id、page、pageSize。返回 JSON 的字段结构。返回数据可能包含这些字段但请以实际接口为准比赛信息对阵双方、比分、赛事名称。评分项选手或队伍评分、评分人数。评论内容用户发布的短评、点赞数。分页信息总数、当前页、每页条数。如果你只拿到了 HTML 页面也可以用 requests 拉取页面后配合 pandas.read_html 尝试解析表格但通常直接抓 JSON 更稳定。先确认接口存在再写采集脚本这是避免返工的关键。3.3 目录规划建议创建以下目录结构match_analysis/ ├── config/ # 配置文件 ├── data/ # 原始数据 ├── output/ # 清洗后数据和图表 ├── scripts/ # 采集与可视化脚本 ├── app/ # Flask 服务 └── logs/ # 运行日志这样跑批量任务时不需要反复移动文件日志和输出也能按日期归档。4. 数据采集与赛果结构化4.1 赛果结构化以 NIP 2-1 WBG 为例先用一个字典保存赛果后续所有图表和分析共用这个结构match_schema { match_id: match_2025_001, team_a: NIP, team_b: WBG, score_a: 2, score_b: 1, winner: NIP, source: hupu_score }将这个结构写入 JSON 文件可以作为整个分析流程的输入元数据。后面不管采集多少次评分数据赛果都不会变。4.2 通用采集脚本下面是一个基础请求模板。真实接口地址、参数名、字段名都必须替换成你在浏览器 Network 面板里看到的内容import requests import pandas as pd import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36, Referer: https://example.com/ } def fetch_score_page(page): # 将 url 替换为真实接口地址 url https://example.com/api/match_score params {match: nip_vs_wbg, page: page} resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json() def collect_all(max_page3, delay1.0): rows [] for p in range(1, max_page 1): try: data fetch_score_page(p) except Exception as e: print(f第 {p} 页失败{e}) continue # 根据真实接口返回结构调整字段名 for item in data.get(list, []): rows.append({ page: p, nickname: item.get(nickname), score: item.get(score), comment: item.get(comment) }) time.sleep(delay) df pd.DataFrame(rows) df.to_csv(data/score.csv, indexFalse, encodingutf-8-sig) print(f采集完成共 {len(df)} 条)这里有几个容易踩坑的地方如果接口返回 403说明 User-Agent 或 Referer 被识别需要从浏览器复制完整请求头如果接口返回的是加密字段无法直接从网络请求中获取明文数据就不要强行破解改为使用平台公开提供的其他数据导出能力或者只做手动人工整理。不要编写绕过权限校验、破坏平台功能的代码。4.3 断点续传设计批量采集最怕中途断掉之后从头再来。建议把“已抓取的页码”记录到一个状态文件里import json def load_state(pathdata/state.json): try: with open(path, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return {completed_pages: []} def save_state(path, state): with open(path, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2)每次抓取完成一页就把页码追加到 completed_pages 里。这样即使脚本中途报错重跑时也能跳过已完成页面。5. 功能测试与效果验证5.1 测试 1接口连通性运行collect_all(max_page1)观察是否能正常返回数据。判断成功的标准状态码为 200。返回内容能被resp.json()解析。返回数据中包含与评分或评论相关的字段。如果得到 403 或返回 HTML 页面先检查请求头再检查参数是否正确。这一步不要跳过直接跑全量采集很容易浪费时间。5.2 测试 2数据清洗评分数值字段经常出现空值、字符串类型或越界值。清洗代码import pandas as pd df pd.read_csv(data/score.csv) df[score] pd.to_numeric(df[score], errorscoerce) df df.dropna(subset[score]) df df[(df[score] 0) (df[score] 10)] df df.drop_duplicates(subset[nickname, comment, score]) print(df.head())如果清洗后发现评分人数和页面显示的总分人数差距非常大说明分页没有抓全或者接口只返回了部分评论。这时需要检查分页总数参数。5.3 测试 3评分分布统计清洗完数据后先看整体的评分分布import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df[score].value_counts().sort_index().plot(kindbar, figsize(10, 4)) plt.title(NIP vs WBG 虎扑评分分布) plt.xlabel(评分) plt.ylabel(人数) plt.savefig(output/score_distribution.png, dpi150) plt.show()正常情况下评分分布应该呈现一定集中趋势而不是全部挤在一个极端值。如果所有评分都是 1 分或 10 分可能说明数据采集不完整或者样本量太小。5.4 测试 4评论关键词提取用 jieba 对评论做中文分词并统计高频词import jieba from collections import Counter stopwords set([的, 了, 和, 是, 在, 吗, 啊, 于, 就, 都]) def extract_keywords(text_series, topn20): words [] for text in text_series.fillna().astype(str): for w in jieba.cut(text): w w.strip() if w and len(w) 1 and w not in stopwords: words.append(w) counter Counter(words) return counter.most_common(topn) print(extract_keywords(df[comment], topn20))输出结果通常是“NIP”“WBG”“比赛”“团战”“运营”“翻盘”“MVP”“下路”等赛果相关词。如果关键词过于分散说明评论区讨论内容非常杂可以按正负情感分类后分别提取。5.5 测试 5简单情感分析这里先用规则词典做一个轻量情感判断保证无 GPU 也能跑positive_words [厉害, 强, 稳, MVP, carry, 翻盘, 精彩] negative_words [送, 拉胯, 失误, 菜, 离谱, 崩盘] def sentiment(text): text str(text) p sum(1 for w in positive_words if w in text) n sum(1 for w in negative_words if w in text) if p n: return 正面 if n p: return 负面 return 中性 df[sentiment] df[comment].apply(sentiment) print(df[sentiment].value_counts())规则词典的准确率有限因为电竞评论里有很多梗、反讽和缩写。如果要做更准确的情感分析可以换用预训练模型但这会引入模型文件下载和额外内存占用属于后续扩展项。5.6 测试 6输出复盘报告把以上结果汇总成一张 Markdown 报告def make_report(df): avg_score df[score].mean() total len(df) sentiment_dist df[sentiment].value_counts().to_dict() words extract_keywords(df[comment], topn10) return { avg_score: round(avg_score, 2), total_comments: total, sentiment_distribution: sentiment_dist, top_keywords: words } report make_report(df) print(report)这一步相当于把原始数据压缩成一份可阅读的赛果摘要。后续无论做图表还是写文章都以这份报告为基础。6. 接口 API 与批量任务6.1 用 Flask 封装分析结果为了让其他脚本、网页或即时通讯机器人能复用分析结果可以用 Flask 做一个本地接口from flask import Flask, request, jsonify import pandas as pd app Flask(__name__) app.route(/api/match_summary) def match_summary(): match_id request.args.get(match_id, default) df pd.read_csv(data/score.csv) result { match_id: match_id, avg_score: round(df[score].mean(), 2), total_comments: int(len(df)), sentiment_distribution: df[sentiment].value_counts().to_dict() } return jsonify(result) if __name__ __main__: app.run(host127.0.0.1, port8000)启动服务python app/server.py测试请求curl http://127.0.0.1:8000/api/match_summary?match_idmatch_2025_001如果返回 JSON 中包含评均分和评论总数说明接口跑通。接口服务只监听本地地址不要随意把 host 改成 0.0.0.0 暴露到公网。6.2 多场次批量任务批量任务的核心是“循环调用”加“状态管理”。把单场采集函数抽象出来MATCHES [ {match_id: match_001, team_a: NIP, team_b: WBG}, {match_id: match_002, team_a: A, team_b: B} ] for match in MATCHES: print(f开始处理 {match[match_id]}) fetch_and_save(match) # 内部包含延时、重试和状态记录 time.sleep(2)推荐做法每场比赛的评分数据单独存一个 CSV 文件用 match_id 做文件名。采集前先检查该场比赛的状态文件是否已存在避免重复抓取。每一页抓取失败后记录错误日志连续失败 3 次就跳过该场比赛。批量任务结束后生成一份汇总日志内容包括成功场次、失败场次、采集总条数。6.3 批量任务的失败重试网络请求具有随机性不能因为一次超时就放弃整场。一个简单的重试封装from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def fetch_with_retry(url, params, headers): resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()使用重试机制时一定要设置最大重试次数和等待时间避免无限重试占用资源。6.4 定时任务本地定时任务可以借助系统自带工具。以 Linux 为例编辑 crontab0 8 * * * cd /path/to/match_analysis /usr/bin/python scripts/collect_all.py logs/cron.log 21Windows 可以使用任务计划程序。定时任务适合每天早上自动拉取前一天的赛后评分数据拉完之后再生成复盘摘要。7. 资源占用与性能观察这个项目不需要 GPU整个环节的瓶颈主要集中在 CPU 和内存。7.1 抓取阶段单线程抓取时CPU 占用很低主要限制是网络延时和接口限速。观察指标是请求耗时和失败率。如果你发现自己被限流表现是接口响应变慢或者开始返回 403这时需要加大请求间隔比如从 1 秒改成 3 秒。7.2 数据处理阶段几千条评论在 pandas 里清洗、分词的耗时通常在几秒到几十秒之间内存占用一般在几百 MB 以内。如果评论量达到几十万条建议用下面的方式降低内存压力分块读取 CSVpd.read_csv(score.csv, chunksize10000)去掉不需要的列usecols[score, comment]分词前先过滤过短评论减少无效计算7.3 可视化阶段wordcloud 生成词云时评论量越大生成时间越长。如果遇到卡顿可以先抽样 5000 条评论再做词云不影响整体趋势判断。7.4 服务运行阶段Flask 服务在单机低并发调用时内存占用不高。但如果同时打开多个浏览器页面反复请求建议在 Flask 上层加一个简单的计算缓存避免每次请求都重新读 CSVcache {} def load_data(): if df not in cache: cache[df] pd.read_csv(data/score.csv) return cache[df]缓存只适合评分数据不会频繁变化的场景。如果每天定时更新数据缓存要在更新后主动清理。8. 常见问题与排查方法问题现象可能原因排查方式解决方案请求返回 403缺少防盗链头或 User-Agent 被识别检查响应头和日志补全 Referer、User-Agent降低请求频率返回 JSON 但字段为空接口字段名与代码不一致打印原始 JSON 对比按真实字段修改 item.get() 的键名中文乱码文件编码或终端编码不一致查看 CSV 前几行读写文件统一使用 utf-8-sigrequests 拿不到数据数据由 JS 异步请求产生打开 DevTools 看 XHR 请求直接请求真实 XHR 接口或改用 Playwright评分数据全为 NaNscore 字段是字符串或表情符号查看原始值用 pd.to_numeric 转换后 dropna批量任务中途停止某个页面超时或触发限流查看日志确认卡点增加超时时间、失败重试、断点续传wordcloud 中文显示为方框默认字体不支持中文查看生成图片指定中文字体路径例如 simhei.ttfFlask 端口被占用8000 端口已被其他进程占用执行netstat -ano查看换端口或结束占用进程评论量太大导致分词慢未做抽样或分块查看 CPU 占用和耗时分块处理或先抽样排查思路基本遵循“先看原始返回再改代码”。绝大多数采集脚本问题都能在打印一次原始 JSON 后定位。不要凭感觉猜字段名每次先确认数据结构再批量处理。9. 最佳实践与使用建议第一轮先小规模测试。只抓 1 到 2 页数据确认字段结构、分页逻辑、请求频率限制再跑完整批量任务。原始数据、清洗数据、最终图表分开目录存放。输出图片和 CSV 按日期命名方便回溯。每次抓取前记录时间戳。评分数据会随时间变动文件里要同时记录比赛发生时间和数据抓取时间。对评论做脱敏处理。展示时不要直接带出用户名不要在文章里逐条截图负面评论。不要把“虎扑评分”等同于官方评分。发布内容时表述为“虎扑用户评分”避免误导。长期运行时使用配置文件管理接口地址、请求间隔、输出目录不要把路径和参数硬编码在脚本里。涉及选手名、队伍名的关键词分析不要刻意加入侮辱性词汇的统计项这类内容对比赛复盘没有正面价值。如果后续接入更多数据源保持同一套字段标准例如统一使用“team_a_score”“team_b_score”这样的命名方便多个场次合并分析。如果要把这套流程分享给团队使用建议再补一个 README 文件写上数据来源、采集频率、运行命令和常见报错记录。这样即使你不在电脑前其他成员也能自己启动服务。10. 总结与下一步NIP 2-1 WBG 这场比赛本身只是一个输入样例。真正有价值的是把赛果、评分和评论整合成一套可复用的复盘工具先抓公开接口再做清洗和分析最后用图表和本地 API 展示。对这个流程而言最值得先验证的是接口连通性和字段结构绝大多数采集脚本写不下去的原因都出在这两步。后续可以扩展的方向包括接入更多赛事的公开数据把多场比赛结果合并成趋势图用预训练情感模型替换规则词典提升评论情感判断的准确率把 Flask 服务替换成 FastAPI加上数据库存储和更完备的定时任务也可以把关键词结果接入大模型自动生成一段赛果摘要。先把这套最小流程跑通再根据实际需要叠加功能。