news 2026/8/31 2:11:21

Python爬虫实战:从NIP vs WBG虎扑评分学数据采集与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:从NIP vs WBG虎扑评分学数据采集与可视化

最近 LPL 赛场上 NIP 2:1 战胜 WBG,赛后虎扑用户会给选手逐人打分,这些散落的评分数据其实是一份不错的赛事分析素材。本文不聊比赛判罚,也不预测季后赛走势,而是把“NIP 2-1 WBG”当作一个数据分析案例,演示怎么把虎扑评分页面里的公开数据采集下来,清洗成结构化表格,再画成能直接放进复盘文档的图表。

这套流程不绑定特定比赛。换一场比赛、换一个队,只要页面结构相似,脚本微调后就能跑。整个过程不需要 GPU,不需要大模型,一台普通电脑加 Python 环境就够了。重点解决三个问题:数据从哪里拿、字段怎么解析、多场比赛怎么批量处理。

如果你平时做赛事内容、写复盘报告,或者想把社区评分做成自动化报表,这篇文章可以直接收藏。下面从项目能力、环境准备、采集思路、数据清洗、可视化和批量任务几个部分展开。

1. 核心能力速览

能力项说明
项目类型赛事评分数据采集与分析工程示例
案例背景NIP 2:1 WBG 赛后虎扑评分
数据源虎扑比赛评分页面(公开页面,需自行确认可访问性)
核心功能页面请求、评分解析、字段清洗、CSV/SQLite 存储、可视化
技术栈Python、requests、BeautifulSoup、Pandas、Matplotlib
硬件要求无 GPU 需求,普通 CPU 即可
显存占用
启动方式命令行运行 Python 脚本
是否支持 API可扩展为本地 HTTP 服务
是否支持批量任务支持,按比赛 ID 循环采集
适合场景赛果复盘、选手表现分析、舆情观察、自动化报表

从材料来看,这个工程示例的重点不是做一个成品软件,而是把“网页评分数据怎么变成分析图表”的完整链路跑通。核心工作量集中在页面结构确认、选择器调整和异常处理上。

2. 适用场景与使用边界

这个数据采集思路适合三类人:

  • 赛事内容作者:赛后需要快速整理选手评分,避免手动复制几十条数据。
  • 数据分析学习者:用真实网页数据练手,覆盖 requests、BeautifulSoup、Pandas 的完整流程。
  • 体育社区产品运营:观察评分人数、平均分变化,辅助内容运营决策。

不适合的场景:

  • 不适合作为商业数据源直接出售。虎扑评分数据属于平台公开内容,但商用前必须确认平台条款和授权范围。
  • 不适合采集登录后才可见的数据。本文只讨论公开页面的基础采集。
  • 不适合高频、大规模抓取。虎扑有反爬策略,单机高频请求容易被限制,也不应该用代理池绕限制。

使用边界必须说清楚:

  • 采集前先看目标页面是否允许爬取,确认 robots 协议和用户协议。
  • 控制请求频率,建议单次请求间隔 3 秒以上。
  • 只采集比赛结果、选手 ID、评分数值这类公开信息,不要去抓用户名、头像、个人主页等个人信息。
  • 如果页面明确出现验证码或访问限制,立即停止,不要尝试绕过。

这套流程只用于个人学习、技术演示和合理研究。公开发布分析结果时,不要伪造数据来源,使用真实请求结果并保留截图时间。

3. 环境准备与前置条件

3.1 基础环境

  • 操作系统:Windows 10/11、macOS 或 Linux 均可。
  • Python:建议 3.9 及以上版本。
  • 网络:能正常访问目标页面。如果目标网站在国内有 CDN,普通家庭网络即可。

3.2 安装依赖

推荐使用虚拟环境隔离依赖。

python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate

安装依赖库:

pip install requests beautifulsoup4 lxml pandas matplotlib openpyxl

各库作用:

用途
requests发起 HTTP 请求,获取页面 HTML
beautifulsoup4解析 HTML,提取评分字段
lxmlBeautifulSoup 的解析引擎,速度更快
pandas数据清洗、去重、统计分析
matplotlib绘制评分分布图、对比图
openpyxl导出 Excel 文件

3.3 开发工具准备

建议使用 VS Code 或 PyCharm。调试爬虫时,浏览器开发者工具是必需工具。打开目标评分页面,按 F12 进入 Elements 和 Network 面板,先人工确认数据是直接渲染在 HTML 里,还是通过异步接口返回。

4. 数据采集思路与页面分析

4.1 先确认数据位置,再写代码

赛事评分页面可能有三种渲染方式:

  • 静态 HTML:评分数据直接包含在 HTML 里,BeautifulSoup 直接解析。
  • XHR/JSON 接口:页面通过 JavaScript 请求 JSON 数据,需要在 Network 面板里找到接口地址。
  • 动态渲染:数据由前端框架渲染,且接口较难定位,可以改用 Playwright 渲染页面后解析。

最稳妥的做法是在 Network 面板刷新页面,看有没有返回 JSON 的请求。如果数据量不大且字段清晰,优先走 JSON 接口;如果找不到接口,再回退到 HTML 解析。

这一步是整个项目里最容易卡住的地方。页面结构会随版本更新变化,没有一劳永逸的选择器。下面给出的是通用模板,不是直接可用的成品。

4.2 通用 HTML 请求模板

import random import time import requests HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_html(url: str, timeout: int = 10, retries: int = 3): for attempt in range(retries): try: resp = requests.get(url, headers=HEADERS, timeout=timeout) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.RequestException as e: print(f"请求失败,第 {attempt + 1} 次重试: {e}") time.sleep(2 * (attempt + 1)) return None

4.3 通用解析模板

先用选择器定位评分卡片容器,再逐条提取选手名、评分、评分人数。

from bs4 import BeautifulSoup def parse_ratings(html: str): """ 解析评分页面,返回评分记录列表。 注意:CSS 选择器必须根据实际页面的 HTML 结构调整。 """ soup = BeautifulSoup(html, "lxml") items = [] # 示例选择器,实际使用时需要替换成页面真实类名 cards = soup.select(".rating-card") for card in cards: player_el = card.select_one(".player-name") score_el = card.select_one(".score") count_el = card.select_one(".rating-count") if player_el and score_el: items.append({ "player": player_el.get_text(strip=True), "score": score_el.get_text(strip=True), "rating_count": count_el.get_text(strip=True) if count_el else "" }) return items

这里必须说明:.rating-card.player-name都是占位选择器。跑脚本前,要用浏览器开发者工具确认页面真实类名,否则解析结果会是空列表。

4.4 如果数据来自 JSON 接口

在 Network 面板里找到评分接口后,优先直接请求 JSON:

import requests url = "https://example.com/api/game/ratings" # 需要替换为真实接口地址 params = {"game_id": "nip-vs-wbg"} resp = requests.get(url, headers=HEADERS, params=params, timeout=10) data = resp.json() for item in data: print(item.get("player"), item.get("score"))

注意接口地址、参数名、返回字段都需要按实际页面调整。返回结构不确定时,先打印resp.text前 500 个字符确认格式。

5. 功能测试与效果验证

5.1 测试一:页面能否正常请求

单独运行请求函数,确认返回状态码和文档开头内容。

python -c "from crawler import fetch_html; html = fetch_html('https://example.com/game/nip-vs-wbg/ratings'); print(html[:200])"

判断标准:

  • 返回 HTML 包含页面标题或比赛信息。
  • 没有出现验证码页面、403 页面、登录跳转。

如果出现 403,大概率是请求头没带全或频率过高。可以先在浏览器里复制完整请求头,再补充到 HEADERS。

5.2 测试二:字段解析是否正常

把解析函数跑一遍,检查返回列表长度和字段完整性。

items = parse_ratings(html) print(len(items)) for item in items[:3]: print(item)

判断标准:

  • items长度大于 0。
  • 每条记录都包含选手名和评分。
  • 评分字段没有混入多余文字。

如果items为空,优先检查选择器。页面结构更新后,之前的选择器会失效。

5.3 测试三:缺失字段和异常值处理

采集过程中可能出现评分人数缺失、选手名重复、评分为空的情况。解析时要做好空值兜底。

def parse_ratings_safe(html: str): soup = BeautifulSoup(html, "lxml") items = [] for card in soup.select(".rating-card"): player_el = card.select_one(".player-name") score_el = card.select_one(".score") if player_el is None: continue player = player_el.get_text(strip=True) score_text = score_el.get_text(strip=True) if score_el else "" try: score = float(score_text) except ValueError: score = None items.append({ "player": player, "score": score, }) return items

判断标准:程序不因为某一条数据异常而中断,缺失字段被置为None,后续清洗阶段再统一处理。

5.4 测试四:重试和限速

批量采集前,先模拟连续 5 次请求,观察是否触发验证码或 429 状态码。

for i in range(5): html = fetch_html("https://example.com/game/nip-vs-wbg/ratings") print(i, len(html) if html else "fail") time.sleep(random.uniform(3, 5))

如果连续请求出现异常,把间隔拉大到 10 秒以上,并在重试逻辑里使用指数退避。这里不做任何规避验证码的操作,遇到限制就降低频率或停手。

6. 数据清洗与存储

采集到的原始数据含有多余字符、缺失值和重复记录,需要先清洗再分析。

6.1 Pandas 清洗流程

import pandas as pd def clean_ratings(items): df = pd.DataFrame(items) if df.empty: return df # 去除选手名中的空白字符 df["player"] = df["player"].str.strip() # 评分转数值,无法转换的置为 NaN df["score"] = pd.to_numeric(df["score"], errors="coerce") # 去掉评分为空的记录 df = df.dropna(subset=["score"]) # 去掉重复选手 df = df.drop_duplicates(subset=["player"], keep="last") # 按评分降序排列 df = df.sort_values("score", ascending=False).reset_index(drop=True) return df

清洗后输出统计信息:

df = clean_ratings(items) print(df.describe())

describe()会给出评分均值、标准差、最小值和最大值,这些是写赛后总结时最常用的指标。

6.2 存储为 CSV / Excel

df.to_csv("nip_wbg_ratings.csv", index=False, encoding="utf-8-sig") df.to_excel("nip_wbg_ratings.xlsx", index=False, sheet_name="选手评分")

建议使用utf-8-sig编码,避免用 Excel 打开 CSV 时中文乱码。

6.3 存储为 SQLite

需要做历史累积分析时,SQLite 比 CSV 更合适。

import sqlite3 conn = sqlite3.connect("lol_ratings.db") df.to_sql("player_ratings", conn, if_exists="append", index=False) conn.close()

每条记录可以追加比赛 ID 和时间字段,方便后续按场次筛选。

7. 分析与可视化

7.1 分析维度

针对 NIP 2:1 WBG 这场比赛的评分数据,可以分析:

  • 两队选手平均分对比。
  • 评分人数分布:评分人数越多,参考价值越高。
  • 最高分和最低分选手,结合比赛过程做原因分析。
  • 队内离散程度:标准差大说明观众意见分化明显。

具体实现:

team_avg = df.groupby("team")["score"].mean().sort_values(ascending=False) print(team_avg)

7.2 柱状图:选手评分对比

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC"] plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(df["player"], df["score"]) ax.set_xlabel("选手") ax.set_ylabel("虎扑评分") ax.set_title("NIP vs WBG 选手虎扑评分对比") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("score_bar.png", dpi=150)

7.3 箱线图:两队评分分布

df.boxplot(column="score", by="team", figsize=(8, 6)) plt.title("NIP vs WBG 评分分布") plt.suptitle("") plt.tight_layout() plt.savefig("score_boxplot.png", dpi=150)

箱线图能直观体现两队的评分中位数、四分位数和离群点。如果某位选手的评分明显低于全队区间,这个点值得在复盘里单独讨论。

7.4 散点图:评分人数与评分关系

fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(df["rating_count"], df["score"]) ax.set_xlabel("评分人数") ax.set_ylabel("虎扑评分") ax.set_title("评分人数与评分关系")

这个维度可以判断某位选手的高分是“少数人打出来的”还是“大量观众一致认可”。

8. 批量任务与定时采集

8.1 多场比赛批量采集

维护一个比赛 ID 列表,循环请求、解析、存储。

import random import time games = [ {"game_id": "nip-vs-wbg-20250401", "team_a": "NIP", "team_b": "WBG"}, {"game_id": "blg-vs-jdg-20250402", "team_a": "BLG", "team_b": "JDG"}, ] def run_batch(games): for game in games: url = f"https://example.com/game/{game['game_id']}/ratings" html = fetch_html(url) if html: items = parse_ratings_safe(html) df = clean_ratings(pd.DataFrame(items)) df["game_id"] = game["game_id"] df["team_a"] = game["team_a"] df["team_b"] = game["team_b"] df["collected_at"] = pd.Timestamp.now().isoformat() save_to_database(df) time.sleep(random.uniform(3, 5))

批量任务的核心是给每条数据打上比赛标识和时间戳,否则后续无法区分来源。

8.2 增量采集策略

如果比赛评分在结束后一周内还在变化,可以做增量更新:每次采集前先查询库里已有的game_id和更新时间,只采集新增场次,或者直接覆盖当天数据。

def get_existing_game_ids(): conn = sqlite3.connect("lol_ratings.db") ids = pd.read_sql("SELECT DISTINCT game_id FROM player_ratings", conn) conn.close() return set(ids["game_id"].tolist()) existing = get_existing_game_ids() new_games = [g for g in games if g["game_id"] not in existing]

8.3 定时调度

Windows 可以用“任务计划程序”,macOS/Linux 用 cron。

# 每天 23:30 执行一次采集脚本 30 23 * * * cd /path/to/project && /usr/bin/python3 batch_crawl.py >> crawl.log 2>&1

日志文件必须保留,定时任务出问题后能快速定位是哪一场比赛导致的异常。

9. 资源占用与性能观察

这个项目是轻量级 CPU 任务,不需要 GPU,显存占用为零。内存开销主要集中在 Pandas 处理 DataFrame 时,单场比赛评分数据量一般只有几十到几百条,完全无压力。

观察性能时需要关注的指标:

  • 单次请求耗时:正常情况 1 到 3 秒。
  • 全场比赛采集耗时:包含解析和间隔,单场约 30 到 60 秒。
  • 内存峰值:单场数据清洗时通常低于 300MB。

耗时大头在网络请求,不是解析。想提高效率,最快的办法是确认是否存在批量评分 JSON 接口,一次请求拿到全部数据,而不是拆成多条请求。

反爬限制是最主要的性能瓶颈。连续请求超过一定频率后很可能触发访问限制。更稳妥的做法是:

  • 控制采集频率,固定 3 到 5 秒间隔。
  • 使用指数退避重试。
  • 设置超时上限,避免单个请求卡死整个批量任务。

不要为了提高速度使用大规模并发请求,这样不仅容易触发反爬,也可能给目标站点带来不必要的压力。

10. 常见问题与排查方法

问题现象可能原因排查方式解决方案
请求返回 403缺少 User-Agent 或请求头不完整打印响应状态码和响应体验证码页面补充浏览器请求头,降低频率
解析列表为空CSS 选择器过时或页面结构变化检查 HTML 中是否包含真实字段类名更新选择器
评分字段包含文字页面包含“分”等后缀打印原始文本正则提取数字或使用float()前先清洗
中文乱码编码判断错误查看响应头编码使用resp.encoding = resp.apparent_encoding
访问被限制请求频率过高查看是否出现验证码或 429拉长间隔,暂停一段时间
SQLite 数据重复重复运行采集脚本查询 game_id 重复数量增加主键或先查重再写入
定时任务未执行cron 环境变量或 Python 路径不对查看 cron 日志使用绝对路径,打印日志到文件

10.1 关键排查思路

第一,所有排查从日志开始。脚本里至少要在请求前后打印 URL、状态码、解析条数,不要静默失败。

第二,页面结构变化是采集失效的最常见原因。发现解析结果为空,第一件事就是打开浏览器手动访问页面,重新确认 DOM 结构。

第三,请求异常先检查响应文本是什么,不要只看状态码。403 页面和验证码页面的处理逻辑完全不同。

11. 最佳实践与使用建议

11.1 工程化建议

  • 把请求、解析、清洗、存储拆成四个独立函数,方便单测和维护。
  • 每场比赛的原始 HTML 可以落盘保存一份,解析失败时不用重新请求。
  • 批量任务要加分页容错,单场失败不中断整个队列。
  • 数据入库前先做查重,避免重复采集污染历史数据。

11.2 合规建议

  • 采集前确认目标网站的 robots 协议和用户协议。
  • 只采集公开数据,不采集用户名、ID、评论正文等个人信息。
  • 采集频率保持在合理范围,不构造高并发请求。
  • 涉及选手、战队、赛事名称的内容发布时,注明数据来源和采集时间。
  • 本流程仅用于个人技术学习、赛事分析和合理研究,不要用于商业出售。

11.3 场景扩展思路

这套数据采集流程稍微调整后,可以迁移到其他方向:

  • 选手历史评分趋势:跨场次累积数据后,画出某一选手近十场评分走势。
  • 战队口碑变化:把比赛结果和赛后评分关联,观察队伍成绩与社区口碑的关系。
  • 自动化赛报生成:采集评分后,结合比赛结果自动生成一段带图表的赛后总结。

12. 总结与下一步

以 NIP 2:1 WBG 这场比赛作为切入点,这篇文章完整走了一遍“网页评分数据采集 -> 清洗 -> 存储 -> 可视化”的流程。核心收获不是某个固定的爬虫脚本,而是三个可以复用的能力:确认数据位置、解析页面字段、批量任务容错。

最先应该验证的是页面结构。打开目标评分页面,确认数据是静态渲染还是 JSON 接口返回,这一步决定了后面所有代码写法。最容易踩的坑也是这里:CSS 选择器写错,脚本运行不报错,但列表始终是空。

下一步可以考虑把采集结果接到 Superset 或者 Grafana,做实时看板;也可以把评分变化历史存进时序数据库,做选手状态的趋势分析。技术本身不复杂,关键是把数据源、字段逻辑和更新策略先定义清楚,后续扩展会顺很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 2:10:55

基于SpringBoot的社区团购管理系统设计与实现

1. 项目背景与意义随着移动互联网和社区电商的快速发展,社区团购已成为连接社区居民与本地供应商的重要零售模式。传统社区团购多依赖微信群接龙、手工记账等方式,存在订单易错、对账繁琐、团长管理困难、配送信息不透明等问题。开发一套基于SpringBoot的…

作者头像 李华
网站建设 2026/8/31 2:10:47

从人才喊话到生态共建:AI协作网络的关键在连接而非回流

最近和一个做 AI 基础设施的朋友聊远程协作,他说了一句话让我印象很深:现在真正稀缺的不是模型,而是能把模型推进到业务里的人。这让我想到另一个在技术圈刷屏的话题——Cohere 创始人公开喊话加拿大人回国共建。很多人的第一反应是&#xff…

作者头像 李华
网站建设 2026/8/31 2:09:52

RGB加解密法:从像素编码到图像隐写的技术解析

第一次看到“RGB加解密法”这个开源项目标题时,我愣了一下。我们习惯把加密和密钥、算法、二进制串绑定在一起,很少会想到图像里那三个颜色通道也能承载一段加密信息。但这个项目偏偏把RGB和加解密两个字拉到一起,还正儿八经地开源了。我觉得…

作者头像 李华
网站建设 2026/8/31 2:07:56

从NIP 2-1 WBG看电竞论坛生态:赛后信息场如何影响你的判断力

当NIP 2-1 WBG的比赛结束后,虎扑电竞区的刷新速度会在几分钟内进入一种奇特状态。比分已经定格,但帖子数量反而比比赛最后时刻还多。有人第一时间甩出赛果帖,有人在评分区打出“尽力了”,有人开始翻赛前预测来逐条打脸&#xff0c…

作者头像 李华
网站建设 2026/8/31 2:05:51

深入理解 Rust Pin:从自引用到内存地址稳定的安全机制

Rust 的 Pin 是很多人学习 async 时一定会碰到的类型&#xff0c;也是我第一次看到poll方法签名时最困惑的地方。为什么不能直接传&mut Self&#xff0c;非要包一层Pin<&mut Self>&#xff1f;后来我把标准库的实现思路拆开&#xff0c;又自己动手写了一个迷你版…

作者头像 李华
网站建设 2026/8/31 2:05:20

人工势场算法动态避障演示:Python+Tkinter交互式路径规划实战

简介&#xff1a;本资源是一套基于人工势场法&#xff08;APF&#xff09;的动态路径规划教学演示系统&#xff0c;面向机器人学、智能控制与路径规划方向的本科生及入门研究者&#xff0c;解决静态/动态障碍物环境下移动机器人实时避障与目标跟踪问题。压缩包共8个文件&#x…

作者头像 李华