Python爬虫实战:王者荣耀官网壁纸自动化下载与反爬策略解析
1. 项目缘起与核心需求解析最近在整理游戏素材库想找一些《王者荣耀》的高清壁纸发现官网的壁纸页面https://pvp.qq.com/web201605/wallpaper.shtml设计得挺有意思图片质量不错但一张张手动下载实在费时费力。作为一个习惯用技术解决重复劳动的程序员自然就想到了写个爬虫。这个需求看似简单但官网的页面结构有些年头了不是那种直接就能用requests抓取到图片链接的静态页面里面涉及一些动态加载和反爬机制正好可以拿来练手也把整个过程和踩过的坑记录下来。这个项目适合谁呢如果你是刚学Python爬虫想找一个有轻微挑战性的实战项目那这个很合适。它涵盖了从静态页面分析、动态内容抓取Selenium、到应对基础反爬策略如请求限制的完整流程。即使你有点基础里面关于Chromedriver版本匹配、Selenium等待策略以及429状态码处理的经验也可能对你有帮助。核心目标就一个写一个稳定、高效的脚本把官网壁纸分类下的图片都自动化下载到本地。2. 技术方案选型与思路拆解面对一个网页首先要判断用什么技术去抓取。直接打开目标页面按F12查看网络请求会发现壁纸的缩略图是直接存在于HTML源码中的但那是低分辨率的小图。我们想要的是点击缩略图后出现的高清大图。这里就出现了第一个关键点高清大图的链接是否直接暴露在初始页面中经过分析发现并非如此。点击某个壁纸分类比如“皮肤”或“英雄”页面会动态加载出该分类下的壁纸列表这个列表是异步加载的。进一步观察网络请求发现了一个形如https://apps.game.qq.com/cgi-bin/ams/module/ishow/draw.php?…的GET请求其返回的JSON数据里包含了当前分类下所有壁纸的信息其中就有高清图的链接。这意味着我们无法仅通过解析初始HTML来获得所有数据必须模拟这个接口请求。因此技术方案需要分两步走获取分类列表及接口参数首先需要从初始页面中提取出所有壁纸分类如“最新”、“皮肤”、“英雄”、“场景”等以及每个分类对应的请求参数比如col和tag。这部分信息是直接写在页面JavaScript变量或HTML元素中的可以用requestsBeautifulSoup静态解析获得。遍历分类请求接口获取图片数据根据上一步获得的参数循环构造上述接口的请求URL发送GET请求获取JSON数据从中解析出高清图片的URL和名称。这个方案避开了完全使用Selenium模拟点击操作的低效方式因为Selenium启动浏览器开销大。但为什么我们还需要Selenium呢这是因为在第一步有时页面结构复杂分类信息可能由一段JS动态生成用BeautifulSoup直接提取比较麻烦。作为备用方案或者为了更直观地演示我们可以用Selenium来获取完全渲染后的页面源码再进行分析。所以我们的最终方案是混合模式优先使用requestsBeautifulSoup进行高效抓取将Selenium作为辅助工具或降级方案。工具选型如下请求库requests。简单易用是处理HTTP请求的标准库。解析库BeautifulSoup4 (bs4)。用于解析HTML提取分类信息和图片链接。动态渲染备用库selenium。配合Chrome浏览器和chromedriver用于应对页面JS渲染复杂的情况。浏览器驱动chromedriver。必须与本地安装的Chrome浏览器版本匹配这是Selenium能控制浏览器的关键。其他os模块用于创建目录time模块用于添加延时防止请求过快json模块用于解析接口返回数据。3. 环境准备与核心工具配置3.1 Python环境与库安装首先确保你安装了Python建议3.6及以上版本。可以通过命令行输入python --version或python3 --version来检查。接下来安装必要的第三方库。打开命令行CMD、PowerShell或终端使用pip进行安装pip install requests beautifulsoup4 selenium如果下载速度慢可以使用国内镜像源例如清华源pip install requests beautifulsoup4 selenium -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 Chromedriver的下载与配置这是使用Selenium最常遇到问题的地方。Chromedriver是一个独立的可执行文件Selenium通过它来与真实的Chrome浏览器通信。第一步查看Chrome浏览器版本打开你的Chrome浏览器点击右上角三个点 - 帮助 - 关于Google Chrome记下版本号例如124.0.6367.91。第二步下载对应版本的Chromedriver访问Chromedriver的官方下载站或国内镜像站。你需要下载与你的Chrome浏览器主版本号一致的Chromedriver。例如你的Chrome是124.x.x.x就下载版本号为124.x.x.x的Chromedriver。注意如果官网没有完全匹配的版本就选择版本号最接近且不超过你浏览器版本的Chromedriver。例如浏览器是124.0.6367.91可以下载124.0.6367.x或124.0.6367。第三步配置Chromedriver将下载的chromedriver.exeWindows或chromedriverMac/Linux文件放置在一个你记得的目录下例如C:\WebDriver\或/usr/local/bin/。然后你需要将这个目录的路径添加到系统的环境变量PATH中。更简单的做法推荐在Python脚本中直接指定chromedriver的路径这样就不需要修改环境变量。我们将采用这种方法。验证安装安装完上述库并准备好chromedriver后可以写一个简单的脚本来测试环境是否正常。import requests from bs4 import BeautifulSoup from selenium import webdriver import time # 测试requests和BeautifulSoup url https://pvp.qq.com/web201605/wallpaper.shtml try: resp requests.get(url, timeout10) resp.raise_for_status() # 检查请求是否成功 print(requests 测试成功状态码:, resp.status_code) soup BeautifulSoup(resp.text, html.parser) title soup.title.string if soup.title else 无标题 print(页面标题:, title) except Exception as e: print(requests 测试失败:, e) # 测试Selenium和Chromedriver driver_path r你的chromedriver完整路径 # 例如rC:\WebDriver\chromedriver.exe try: # 创建一个Chrome浏览器选项对象可以添加一些配置 options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式不显示浏览器窗口 options.add_argument(--disable-gpu) # 禁用GPU某些环境下需要 options.add_argument(--no-sandbox) # Linux下可能需要 driver webdriver.Chrome(executable_pathdriver_path, optionsoptions) driver.get(https://www.baidu.com) time.sleep(2) print(Selenium 测试成功当前页面标题:, driver.title) driver.quit() except Exception as e: print(Selenium 测试失败请检查chromedriver路径和版本:, e)运行这个脚本如果都能成功打印信息说明基础环境配置正确。4. 页面结构与数据接口分析这是爬虫编写中最关键的一步决定了我们代码的效率和稳定性。我们需要像侦探一样仔细审查网页的构成。4.1 初始页面静态分析用浏览器打开目标网址右键“检查”或按F12打开开发者工具切换到“元素”Elements标签页。 观察页面结构我们发现壁纸分类是以li列表的形式存在的每个li标签里有一个a链接。查看其中一个a标签的href属性可能不是直接的链接而是javascript:;但它的onclick事件或>li classcurrent>{ code: 0, msg: ok, data: { sProdName: 壁纸名称, sProdImgNo: 编号, sImgSize_200_0: 小图URL, sImgSize_400_0: 中图URL, sImgSize: 原图URL } }我们需要的是sImgSize字段它指向高清原图。sProdName可以作为保存图片时的文件名。实操心得这里有一个非常重要的细节。返回的sImgSize字段的URL可能是一个相对路径或不完整的路径。你需要仔细观察它可能需要拼接一个固定的域名前缀如https://shp.qpic.cn/才能构成完整的可访问链接。一定要在浏览器中直接打开这个拼接后的URL进行验证。4.3 确定最终抓取逻辑基于以上分析我们的抓取逻辑清晰了请求初始页面用BeautifulSoup解析出所有分类的>import requests from bs4 import BeautifulSoup import json import os import time import re from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 常量定义 BASE_URL https://pvp.qq.com/web201605/wallpaper.shtml API_URL_TEMPLATE https://apps.game.qq.com/cgi-bin/ams/module/ishow/draw.php?col{col}tag{tag}page0sort0 # 图片URL可能需要的前缀根据实际接口返回调整 IMG_URL_PREFIX https://shp.qpic.cn/ SAVE_DIR ./王者荣耀壁纸/ # 请求头模拟浏览器访问 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://pvp.qq.com/ } # 创建保存目录 if not os.path.exists(SAVE_DIR): os.makedirs(SAVE_DIR)5.2 方法一使用RequestsBeautifulSoup解析分类我们首先尝试用requests直接获取页面并解析。def get_categories_by_requests(): 使用requests获取壁纸分类列表 print(正在尝试通过Requests获取分类信息...) try: response requests.get(BASE_URL, headersHEADERS, timeout15) response.raise_for_status() response.encoding gbk # 该页面编码可能是gbk根据实际情况调整 soup BeautifulSoup(response.text, html.parser) # 寻找包含分类的ul或div这里需要根据实际页面结构调整选择器 # 通过观察分类通常在 class 为 ‘piclist’ 或 ‘nav’ 的 ul 中 category_list [] # 尝试多种可能的选择器 nav_ul soup.find(ul, class_piclist) if not nav_ul: nav_ul soup.find(ul, idpiclist) if not nav_ul: # 如果找不到尝试查找所有li再根据父级或内容筛选 all_lis soup.find_all(li) for li in all_lis: if li.get(data-id) and li.find(a): category_list.append({ name: li.a.text.strip(), data_id: li.get(data-id) }) return category_list # 如果找到了ul提取其中的li for li in nav_ul.find_all(li): data_id li.get(data-id) name_tag li.find(a) if data_id and name_tag: category_list.append({ name: name_tag.text.strip(), data_id: data_id }) return category_list except Exception as e: print(fRequests方式获取分类失败: {e}) return None5.3 方法二使用Selenium作为备用方案如果上面的方法因为页面JS动态加载而无法获取到分类我们就启用Selenium方案。def get_categories_by_selenium(driver_path): 使用Selenium获取壁纸分类列表备用方案 print(正在通过Selenium获取分类信息...) options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式后台运行 options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) # 可添加其他选项如屏蔽图片加载以加速 prefs {profile.managed_default_content_settings.images: 2} options.add_experimental_option(prefs, prefs) driver None try: driver webdriver.Chrome(executable_pathdriver_path, optionsoptions) driver.get(BASE_URL) # 显式等待确保分类元素加载出来 wait WebDriverWait(driver, 10) # 等待分类列表出现这里使用CSS选择器示例需根据实际页面调整 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ul.piclist li[data-id]))) soup BeautifulSoup(driver.page_source, html.parser) driver.quit() # 解析逻辑与requests方法类似 category_list [] nav_ul soup.find(ul, class_piclist) if nav_ul: for li in nav_ul.find_all(li): data_id li.get(data-id) name_tag li.find(a) if data_id and name_tag: category_list.append({ name: name_tag.text.strip(), data_id: data_id }) return category_list except Exception as e: print(fSelenium方式获取分类失败: {e}) if driver: driver.quit() return None5.4 核心抓取与下载函数获取到分类列表后我们就可以遍历每个分类请求接口并下载图片了。def download_wallpapers(category_list, tag全部): 根据分类列表下载壁纸 if not category_list: print(未获取到任何分类信息程序退出。) return session requests.Session() session.headers.update(HEADERS) for category in category_list: cat_name category[name] cat_id category[data_id] print(f\n开始处理分类: [{cat_name}] (ID: {cat_id})) # 为每个分类创建单独的文件夹 cat_save_dir os.path.join(SAVE_DIR, cat_name) if not os.path.exists(cat_save_dir): os.makedirs(cat_save_dir) # 构造API请求URL api_url API_URL_TEMPLATE.format(colcat_id, tagtag) print(f请求接口: {api_url}) try: # 重要添加延时避免请求过快触发反爬429错误 time.sleep(2) resp session.get(api_url, timeout15) resp.raise_for_status() # 解析JSON响应 data resp.json() if data.get(code) ! 0: print(f接口返回错误: {data.get(msg)}) continue pic_list data.get(data, []) if not isinstance(pic_list, list): print(接口返回的data字段不是列表格式) continue for index, pic_info in enumerate(pic_list): img_name pic_info.get(sProdName, f未命名_{index}) # 清理文件名中的非法字符 img_name re.sub(r[\\/*?:|], _, img_name) img_url pic_info.get(sImgSize) if not img_url: print(f 第{index1}张图片未找到sImgSize字段跳过。) continue # 处理图片URL如果返回的是相对路径则拼接前缀 if img_url.startswith(//): img_url https: img_url elif not img_url.startswith(http): img_url IMG_URL_PREFIX img_url.lstrip(/) # 构建图片保存路径 file_extension os.path.splitext(img_url)[1] # 从URL获取扩展名 if not file_extension or len(file_extension) 5: # 简单判断如果没有或太长默认.jpg file_extension .jpg save_path os.path.join(cat_save_dir, f{img_name}{file_extension}) # 下载图片 print(f 正在下载: {img_name} - {save_path}) try: img_resp session.get(img_url, timeout30) img_resp.raise_for_status() with open(save_path, wb) as f: f.write(img_resp.content) print(f 下载成功) time.sleep(0.5) # 图片间也稍作延时 except Exception as img_e: print(f 下载失败: {img_e}) except requests.exceptions.RequestException as req_e: print(f请求接口时发生网络错误: {req_e}) except json.JSONDecodeError as json_e: print(f解析JSON响应失败: {json_e}) print(f原始响应文本: {resp.text[:200]}...) # 打印前200字符辅助调试 except Exception as e: print(f处理分类[{cat_name}]时发生未知错误: {e}) print(\n所有分类处理完毕)5.5 主函数与流程控制最后我们将所有功能串联起来形成主程序。def main(): 主函数 # 首先尝试用requests获取分类 categories get_categories_by_requests() # 如果requests失败则使用Selenium需要指定chromedriver路径 if not categories: print(Requests方式未获取到分类尝试使用Selenium...) # 请将这里的路径替换为你自己的chromedriver路径 CHROMEDRIVER_PATH rC:\WebDriver\chromedriver.exe # Windows示例 # CHROMEDRIVER_PATH /usr/local/bin/chromedriver # Mac/Linux示例 categories get_categories_by_selenium(CHROMEDRIVER_PATH) if categories: print(f成功获取到 {len(categories)} 个分类:) for cat in categories: print(f - {cat[name]} (ID: {cat[data_id]})) # 开始下载tag参数可能需要根据页面分析调整这里先用‘全部’ download_wallpapers(categories, tag全部) else: print(无法获取分类信息请检查网络、页面结构或chromedriver配置。) if __name__ __main__: main()6. 常见问题、反爬策略与优化技巧在实际运行中你几乎一定会遇到下面这些问题。这里是我踩过坑后的经验总结。6.1 请求频率过高导致429错误这是最经典的反爬策略之一。服务器识别出你的请求过于频繁返回429 Too Many Requests状态码。解决方案增加延时在关键请求之间使用time.sleep()。比如在遍历每个分类前sleep(2)在下载每张图片前sleep(0.5)。这个时间需要根据目标服务器的容忍度调整太短容易被封太长效率低。使用Session像我们代码中那样使用requests.Session()。Session对象可以保持一些连接参数和Cookie比每次新建请求更“友好”也稍微高效一点。设置随机延时固定延时容易被识别。可以引入随机性例如time.sleep(random.uniform(1, 3))。处理429状态码可以在代码中捕获429异常然后等待更长的时间后重试。import random def safe_request(session, url, max_retries3): for i in range(max_retries): try: resp session.get(url, timeout15) if resp.status_code 429: wait_time (2 ** i) random.random() # 指数退避随机 print(f触发429等待{wait_time:.2f}秒后重试...) time.sleep(wait_time) continue resp.raise_for_status() return resp except requests.exceptions.RequestException as e: print(f请求失败 ({i1}/{max_retries}): {e}) if i max_retries - 1: raise time.sleep(2) return None6.2 Chromedriver版本不匹配或无法启动问题表现SessionNotCreatedException提示This version of ChromeDriver only supports Chrome version ...。解决方案严格匹配版本再次确认你的Chrome浏览器版本和下载的Chromedriver版本。主版本号必须一致。使用WebDriver Manager这是一个第三方库可以自动下载和管理匹配的浏览器驱动。安装pip install webdriver-manager然后在代码中这样使用from webdriver_manager.chrome import ChromeDriverManager from selenium import webdriver driver webdriver.Chrome(ChromeDriverManager().install())这能极大简化环境配置。检查路径和权限确保指定的chromedriver路径正确并且有可执行权限Linux/Mac系统需要chmod x chromedriver。6.3 页面元素定位失败或数据提取为空问题表现BeautifulSoup或Selenium找不到预期的标签返回空列表。解决方案确认页面已加载完成对于Selenium务必使用显式等待WebDriverWait而不是time.sleep。显式等待更智能会在条件满足后立即继续而不是傻等固定时间。检查选择器页面结构可能已更新。重新使用开发者工具检查元素确认CSS选择器或XPath是否正确。优先使用id、name或稳定的class避免使用可能变化的索引位置。查看页面源码在Selenium中使用driver.page_source查看获取到的实际HTML与浏览器中看到的进行对比确认JS是否真的渲染出了我们需要的数据。尝试备用解析方案就像我们代码中做的如果通过class找不到就尝试通过id找或者通过查找所有具有特定属性如>