Python爬虫实战:高效获取英雄联盟战绩数据的技术解析
2026/8/25 7:38:19 网站建设 项目流程

1. 从需求到实现:为什么选择爬虫获取LOL战绩数据?

作为一名游戏数据爱好者,我经常想分析自己和朋友的《英雄联盟》对局表现。官方客户端和第三方App提供的数据虽然直观,但往往不够深入,或者无法满足批量分析、长期追踪的个性化需求。比如,我想知道自己在不同时间段、使用不同英雄时的分均经济、视野得分变化趋势,或者对比不同队友的参团率对胜率的影响,这些细粒度的、跨对局的数据聚合,在常规平台很难一站式获取。

这时候,爬虫技术就成了连接需求与数据的桥梁。简单来说,爬虫就是一个自动化的数据采集程序,它模拟浏览器访问网页,从网页的源代码中提取出我们感兴趣的结构化信息。对于英雄联盟战绩数据,我们的目标就是那些承载了KDA、伤害、经济、装备等详细信息的网页。通过编写爬虫,我们可以将这些零散分布在无数个对战记录页面中的数据,高效、批量地收集起来,存入数据库或Excel,为后续的数据分析和可视化打下坚实的基础。这不仅仅是“查战绩”,更是迈向“用数据理解游戏、提升水平”的第一步。

2. 目标分析与技术选型:我们到底要爬什么?

在动手写代码之前,明确目标至关重要。我们需要清晰地定义数据源、要爬取的具体字段以及实现的技术路径。

2.1 数据源选择与可行性分析

英雄联盟的数据主要有几个潜在来源:

  1. 英雄联盟官方客户端/官网:数据最权威,但反爬机制非常严格,接口复杂且可能随时变动,对新手不友好。
  2. 第三方战绩查询网站/App(如OP.GG、U.GG、LeagueOfGraphs等):这是更实际的选择。它们本身已经聚合和展示了非常详细的战绩数据,页面结构相对稳定,数据呈现也较为规整。我们的爬虫目标就是这类网站。

以OP.GG为例,一个典型的单场对局详情页包含了数十个数据点。我们需要爬取的核心详细数据字段通常包括:

  • 对局概要:游戏模式(排位/匹配)、对局时长、胜负结果、游戏日期。
  • 玩家表现
    • 基础数据:使用的英雄、召唤师技能、符文、K/D/A(击杀/死亡/助攻)、游戏内评分(如SVP)。
    • 经济与伤害:总金币、分均经济(CSM)、总伤害、对英雄伤害、对塔伤害、承受伤害、治疗量。
    • 视野与目标:插眼数量、排眼数量、控制得分(视野得分)、击杀大型野怪、摧毁眼位、夺取峡谷先锋/男爵等。
    • 装备与技能:出装顺序(6个装备格+饰品)、技能加点顺序(Q/W/E/R)。
    • 队伍与对手:本队所有玩家的数据、对手队伍所有玩家的数据(用于分析对位情况)。

2.2 技术栈规划:Python生态是首选

对于这样的任务,Python因其丰富的库和简洁的语法成为不二之选。核心工具链如下:

  • requests:用于发送HTTP请求,获取网页的HTML源代码。它是我们与目标网站通信的“浏览器”。
  • BeautifulSoup4(bs4)lxml:用于解析HTML文档,定位和提取我们需要的特定数据。可以将杂乱的HTML转化为一棵结构树,让我们能像使用文件路径一样找到目标标签。BeautifulSoup更易上手,lxml解析速度更快。
  • pandas:数据清洗与处理的利器。爬取到的原始数据往往是列表或字典形式,用pandasDataFrame可以方便地进行去重、缺失值处理、格式转换、合并和最终导出为CSV或Excel。
  • time/random:用于在请求间添加延时,模拟人类操作,避免请求过快导致IP被暂时封锁(反爬策略之一)。

为什么不直接用SeleniumSelenium是浏览器自动化工具,能处理JavaScript动态渲染的页面,但速度慢、资源占用高。对于OP.GG这类主要数据仍包含在初始HTML响应中的网站,requests+BeautifulSoup的组合更轻量、高效。只有当数据完全由JS加载(即查看网页源代码找不到数据)时,才需要考虑Selenium或分析其背后的API接口。

3. 实战爬虫构建:从单页到批量的完整流程

接下来,我们以爬取OP.GG上单场对局详情页为例,拆解每一步。请注意,以下代码为示例逻辑,实际网站结构可能微调,需要你动手时稍作适配。

3.1 环境准备与基础请求

首先,安装必要的库,并设置请求头以模拟真实浏览器访问。

pip install requests beautifulsoup4 pandas lxml
import requests from bs4 import BeautifulSoup import pandas as pd import time import random # 设置请求头,模拟Chrome浏览器 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9', } # 目标URL(示例:一场具体的对局详情页) match_url = 'https://www.op.gg/summoners/na/xxx/matches/1234567890' # 此处需替换为真实URL def get_page_html(url): """发送请求并返回解析后的BeautifulSoup对象""" try: # 添加随机延时,礼貌爬取 time.sleep(random.uniform(1, 3)) response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 检查请求是否成功 # 指定使用lxml解析器,速度更快 soup = BeautifulSoup(response.content, 'lxml') return soup except requests.exceptions.RequestException as e: print(f"请求失败: {url}, 错误: {e}") return None # 获取页面 soup = get_page_html(match_url) if soup is None: print("无法获取页面,请检查网络或URL")

3.2 数据定位与解析:关键的“寻宝”环节

这是爬虫的核心,需要仔细分析网页结构。使用浏览器的“开发者工具”(F12)是必备技能。通过“检查元素”,找到目标数据所在的HTML标签及其属性(如classid)。

假设我们想提取本局所有10名玩家的核心数据(KDA、英雄、伤害等)。在OP.GG页面上,这些数据通常在一个<table>或一系列具有特定class<div>中。

def parse_match_detail(soup): """解析单场对局详情页,提取数据""" match_data = [] # 1. 提取对局元信息(示例) game_info_div = soup.find('div', class_='game-info') # 实际class名需根据网站调整 if game_info_div: game_mode = game_info_div.find('span', class_='game-type').text.strip() game_duration = game_info_div.find('span', class_='game-length').text.strip() game_result = '胜利' if 'Victory' in soup.find('div', class_='result').text else '失败' # ... 提取其他元信息 # 2. 定位玩家数据区域 - 这是关键步骤 # 通常玩家数据会分在两个表格或区域,代表双方队伍 team_tables = soup.find_all('table', class_='team-table') # 假设类名如此 for team_table in team_tables: # 找到表格内的每一行,通常每个玩家对应一个<tr>标签 player_rows = team_table.find('tbody').find_all('tr') for row in player_rows: player_dict = {} # 提取单元格数据 cells = row.find_all('td') # 假设列顺序是:英雄、KDA、伤害、经济、视野等 # 需要根据实际HTML结构精确索引或通过class名查找 player_dict['hero'] = cells[0].find('img')['alt'] # 英雄名可能在img标签的alt属性 player_dict['kills'] = cells[1].find('span', class_='kill').text player_dict['deaths'] = cells[1].find('span', class_='death').text player_dict['assists'] = cells[1].find('span', class_='assist').text # 更稳健的方法是遍历查找特定class的元素 damage_elem = row.find('td', class_='damage') if damage_elem: player_dict['total_damage'] = damage_elem.text.strip() gold_elem = row.find('td', class_='gold') if gold_elem: player_dict['total_gold'] = gold_elem.text.strip() # 将本局元信息也加入 player_dict.update({ 'game_mode': game_mode, 'duration': game_duration, 'result': game_result, 'match_url': match_url }) match_data.append(player_dict) return match_data # 解析单页数据 single_match_data = parse_match_detail(soup) print(f"本场对局共爬取到 {len(single_match_data)} 条玩家数据")

实操心得:网页结构是爬虫的“地图”。这个“地图”可能会变(网站改版)。因此,代码中的选择器(如class_='team-table')不能写死,最好将其作为配置参数。更重要的是,解析逻辑要有容错性。使用find(找不到返回None)比find_all(返回空列表)后直接索引更安全,务必用if判断元素是否存在,否则一个页面结构的小变动就会导致整个爬虫崩溃。

3.3 处理翻页与批量爬取:获取历史战绩

单场数据意义有限,我们通常需要某个玩家最近20场、50场甚至更多的数据。这就需要处理翻页。

首先,找到玩家战绩列表页,例如:https://www.op.gg/summoners/服务器/玩家ID。这个页面会列出多场对局的链接。

def get_match_links_from_overview(summoner_url): """从玩家概览页获取多场对局的详情页链接""" soup = get_page_html(summoner_url) if not soup: return [] match_links = [] # 假设每场对局都有一个<a>标签,其href指向详情页 # 需要具体分析OP.GG的战绩列表结构 match_items = soup.find_all('div', class_='game-item') # 示例class for item in match_items: link_tag = item.find('a', href=True) if link_tag and '/matches/' in link_tag['href']: # 过滤出对局详情链接 full_url = 'https://www.op.gg' + link_tag['href'] if link_tag['href'].startswith('/') else link_tag['href'] match_links.append(full_url) return match_links # 获取一个玩家的最近N场对局链接 summoner_url = 'https://www.op.gg/summoners/na/YourSummonerName' all_match_links = get_match_links_from_overview(summoner_url)[:10] # 只爬前10场作为演示 all_matches_data = [] for idx, link in enumerate(all_match_links): print(f"正在爬取第 {idx+1}/{len(all_match_links)} 场: {link}") soup = get_page_html(link) if soup: match_data = parse_match_detail(soup) all_matches_data.extend(match_data) # 每爬完一场,增加一个较长的随机延时,避免被封 time.sleep(random.uniform(3, 6)) print(f"批量爬取完成,共获得 {len(all_matches_data)} 条玩家对局记录")

3.4 数据清洗、存储与初步分析

爬取到的原始数据是字典列表,可能存在缺失值、格式不一致(如“1,234”这样的数字字符串)等问题,需要用pandas进行清洗。

# 转换为DataFrame df = pd.DataFrame(all_matches_data) # 查看数据概览 print(df.head()) print(df.info()) # 数据清洗示例 # 1. 处理缺失值 df.fillna('N/A', inplace=True) # 2. 转换数据类型(例如,将K/D/A从字符串转为数值) df['kills'] = pd.to_numeric(df['kills'], errors='coerce') df['deaths'] = pd.to_numeric(df['deaths'], errors='coerce') df['assists'] = pd.to_numeric(df['assists'], errors='coerce') # 3. 计算衍生字段,如KDA比率 df['kda_ratio'] = (df['kills'] + df['assists']) / df['deaths'].replace(0, 1) # 防止除零 # 4. 处理带逗号的数字字符串(如“1,234”总伤害) def clean_number_string(x): if isinstance(x, str): return x.replace(',', '') return x numeric_columns = ['total_damage', 'total_gold'] for col in numeric_columns: if col in df.columns: df[col] = df[col].apply(clean_number_string) df[col] = pd.to_numeric(df[col], errors='coerce') # 存储数据 df.to_csv('lol_match_history.csv', index=False, encoding='utf-8-sig') # 保存为CSV,兼容中文 # df.to_excel('lol_match_history.xlsx', index=False) # 保存为Excel print(f"数据已保存,共 {df.shape[0]} 行,{df.shape[1]} 列")

现在,你拥有了一个结构化的数据集。你可以用pandas进行快速分析,例如:

  • 计算自己使用不同英雄的平均KDA、胜率。
  • 分析游戏时长与伤害量的关系。
  • 对比胜利局和失败局的平均视野得分差异。

4. 高级策略与关键避坑指南

爬虫不是简单的“请求-解析”,在实际操作中会遇到各种问题。以下是基于经验的深度解析。

4.1 应对反爬机制:礼貌、伪装与持久化

第三方网站不希望被无节制地爬取,因此设有反爬措施。

  • 请求频率限制:这是最基本的一点。像上面代码中time.sleep(random.uniform(3, 6))这样的随机延时是必须的。过于频繁的请求会导致你的IP地址被暂时甚至永久封禁。对于重要项目,可以考虑使用代理IP池来分散请求。
  • 请求头(Headers):完整的headers至关重要。除了User-Agent,有时还需要Referer(来源页)、Accept-Encoding等。缺少这些,服务器可能识别出你是爬虫而非浏览器。可以通过浏览器开发者工具的“Network”面板,查看一次真实请求所携带的所有Headers并进行模仿。
  • Cookie与Session:有些数据需要登录后才能查看(如“仅自己可见的战绩”)。这时需要处理Cookie。你可以先用浏览器登录,然后从开发者工具中复制Cookie字符串,在requests.get()时通过headers={'Cookie': '你的Cookie字符串'}传入。更规范的做法是使用requests.Session()对象,它能够自动管理会话和Cookie。
  • 动态内容与JavaScript渲染:如果所需数据在页面初始HTML中找不到,而是由JavaScript动态加载的(比如滚动页面时加载更多战绩),那么requests就无能为力了。此时有两种主流方案:
    1. 使用Selenium或Playwright:直接控制一个真正的浏览器,可以执行JS并等待内容加载。优点是简单直接,能应对几乎所有复杂页面;缺点是速度极慢,资源消耗大,不适合大规模爬取。
    2. 分析网络请求(推荐):在开发者工具的“Network”面板中,筛选XHR/Fetch请求,寻找直接返回数据的API接口。这些接口通常返回JSON格式,结构清晰,直接用requests调用即可,效率极高。这需要一些耐心去观察和猜测接口参数。

4.2 解析策略的健壮性:让爬虫更“智能”

网页结构并非一成不变,一个微小的CSS类名改动就可能导致你的find语句失效。

  • 使用多种选择器组合:不要只依赖一个class。可以结合标签名、属性、甚至文本内容来定位。BeautifulSoup支持CSS选择器(soup.select(‘.class1 .class2’)),功能强大。
  • 层级定位与相对查找:先定位到一个稳定的、大的容器元素(比如整个战绩表格),再从这个容器内部去查找具体数据。这样即使页面其他部分变动,只要这个容器结构稳定,你的解析逻辑就依然有效。
  • 异常捕获与数据验证:在解析每个字段时,都应用try...except包裹,并为缺失字段设置默认值(如‘N/A’0)。定期运行爬虫,并检查输出数据的完整性和一致性。
  • 定期更新与维护:将解析规则(如CSS选择器字符串)作为配置文件管理。当网站改版导致爬虫失效时,只需更新配置文件,而无需重写核心逻辑。

4.3 数据存储与工程化考量

当数据量变大时,CSV文件会变得笨重。

  • 数据库存储:使用SQLite(轻量,适合个人项目)、MySQLPostgreSQL来存储数据。这便于进行复杂的查询、关联和分析。Python的sqlite3SQLAlchemy库可以方便地操作数据库。
  • 增量爬取:为了避免重复爬取相同对局,可以在数据库中记录已爬取对局的唯一ID(如从URL中提取)。每次爬取前先查询,只爬取新的对局。
  • 任务调度:如果你需要定期(如每天)更新某个玩家的战绩,可以使用系统的cron(Linux)或计划任务(Windows),或者使用Python的APScheduler库来定时运行你的爬虫脚本。

4.4 法律与道德边界:做一个负责任的爬虫使用者

这是最重要的一课。爬虫在法律和道德上存在灰色地带。

  • 尊重robots.txt:在网站根目录下(如https://www.op.gg/robots.txt)的robots.txt文件指明了网站允许或禁止爬虫访问的路径。尽管这不是法律文件,但遵守它是行业惯例和尊重的表现。
  • 不要造成服务器负担:务必设置合理的请求间隔。你的爬虫不应该影响网站的正常服务。
  • 数据用途:爬取的数据应用于个人学习、分析或研究。绝对不要用于商业用途、公开大量分发、或进行任何可能干扰游戏公平性的行为(如用于外挂)。这很可能违反网站的服务条款,甚至相关法律法规。
  • 隐私考虑:你爬取的是公开数据,但依然涉及其他玩家的信息。避免公开传播可识别特定个人的详细数据集合。

我在实际构建这类爬虫时,最大的教训就是“稳定性高于一切”。一个能稳定运行数月、优雅处理各种异常、在网站小改版后能快速修复的爬虫,远比一个功能花哨但脆弱的爬虫有价值。因此,在开发初期,就要投入大量时间构建健壮的解析逻辑、完善的错误处理和日志记录系统。每次运行,都详细记录爬取了哪些URL、成功了多少、失败了多少、失败原因是什么。这些日志是后期维护和优化的唯一依据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询