由于博主前两天刚入门Java爬虫,并且自学了Jsoup的爬取和解析方式以及输入输出流的相关知识,因此打算检验一下目前的学习成果。在一番深思熟虑(x)后,毅然打算爬取王者荣耀官网全英雄全皮肤的壁纸。
爬取分析
1.首先进入王者荣耀官网英雄主页面(https://pvp.qq.com/web201605/herolist.shtml),右键点击检查,对主页进行分析。
2.观察英雄主页面源代码,可发现所有英雄都包含在ul标签(ul.herolist.clearfix)下的li标签中,而其中的li标签的href便是我们需要进入的英雄页面。
3.点击href的链接,进入到英雄页面。(这里我进入的是蒙恬的页面,多尝试进入几个英雄页面可以发现基本规律就是/herodetail/XXX.shtml,其中XXX为英雄编号)
观察英雄页面,其中ul标签(ul.pic-pf-list.pic-pf-list3)下的data-imgname属性内容是英雄皮肤名称字符串;而通过select选中“秩序猎龙者”,可以发现这个小图标来自于img标签内。这里注意了!img标签内的src属性的链接是小图标,而壁纸则是data-imgname属性的链接!
4.复制data-imgname属性内的链接粘贴进入,可以看到我们的爬取目标——英雄皮肤壁纸。多点几个英雄的链接地址进行观察,可找到规律。(前缀一样,即前缀都是:https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/;总体规律为:https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/ + 英雄编号 + / + 英雄编号 + -bigskin- + 皮肤顺序 + .jpg)
5.根据上述爬取分析,我们的爬取目标已经很明确了,这里来总结一下:
① 通过英雄主页面(https://pvp.qq.com/web201605/herolist.shtml)爬取英雄名称,英雄编号,英雄页面地址链接,便于进入各个英雄的详情页面,并在爬取时标记好当前正在爬取的英雄。
② 通过英雄详情页面爬取英雄的皮肤串,去除特殊符号并拆分保存到List列表中便于为之后爬取的英雄皮肤图片命名。
③ 组装好各个英雄皮肤壁纸链接,为之后下载保存到文件夹作准备。
目前思路已经很明确了,接下来进入按照爬取目标进行代码编写的实战篇。
实战演练
1.首先按照我们前面“爬取分析”模块的第一个目标进行代码编写,也就是通过英雄主页面爬取英雄名称,英雄编号,英雄页面地址链接。
public static void main(String[] args) throws IOException {
// 请求主页面
String url = "https://pvp.qq.com/web201605/herolist.shtml"; // 主页面
Connection connect = Jsoup.connect(url); // 创建连接
Document document = connect.get(); // 请求网页
// 解析获取英雄名称、英雄编号、英雄页面
Elements elements = document.select("ul.herolist.clearfix").select("li");
for(Element ele : elements) { // 遍历每一个li节点
String hero = ele.select("a").select("img").attr("alt");
String hero_url = ele.select("a").attr("href");
String hero_id = hero_url.substring(hero_url.length()-9, hero_url.length()-6);
System.out.println("英雄:" + hero + ", 英雄编号:" + hero_id + ", 英雄页面:" + hero_url);
}
}
通过观察控制台的输出,可以看到从“云中君”到“廉颇”所有英雄爬取完毕。
这个模块没什么可说的,无非就是通过Jsoup循环遍历获取li节点集合,然后对其中的每个li节点集合进行解析输出。(之后输出可以调整为调用方法分别进入英雄详情页面进行壁纸下载)
2.编写英雄详情页面爬取方法heroPage,完成前述“爬取分析”模块中的第二个目标和第三个目标。
static void heroPage(String hero, String hero_id, String hero_url) throws IOException {
String url = "https://pvp.qq.com/web201605/" + hero_url;
Document doc = Jsoup.connect(url).get(); // 获取英雄页面源代码
Element ele = doc.select("ul.pic-pf-list.pic-pf-list3").get(0);
String pf = ele.attr("data-imgname"); // 找到皮肤串
pf = pf.replaceAll("\\d+", ""); // 通过正则表达式去除皮肤串中的数字
int pf_num = 0; // 假设皮肤数量为0
for(int i = 0; i < pf.length(); i++) {
pf_num = (pf.charAt(i) == "&".toCharArray()[0])?pf_num + 1 : pf_num; // 将String类型的字符"&"转换成char类型
}
pf = pf.replaceAll("&", ""); // 删除皮肤串中所有的“&”
String[] allpf = pf.split("\\|"); //在JAVA中需要用“\”进行转义,其中“*”、“.”、“/”、“.”都需要双斜杠
List<String> pfList = new ArrayList<String>(); // 皮肤列表内存储的是对应英雄hero_id的所有皮肤
for(int j = 0; j < allpf.length; j++) {
pfList.add(allpf[j]); // 存储到皮肤列表
}
String pfurl = "https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/";
String pic, pic_url = null;
// 创建对应文件夹用于之后存储皮肤壁纸
String pfPath = "D:\\KingGlory\\" + hero + "\\";
int z = 0; // 声明在for循环外,一个英雄hero_id对应z.size()-1个皮肤
for(int i = 1; i <= pf_num; i++) {
pic = hero_id + "/" + hero_id + "-bigskin-" + i + ".jpg"; // 构造皮肤连接的后缀
pic_url = pfurl + pic;
// 输出皮肤列表
for( ; z < pfList.size(); ) {
System.out.println("英雄【" + hero + "】的“" + pfList.get(z) + "”皮肤壁纸链接:" + pic_url);
z++; // 输出一次z+1走到下一个皮肤的列表位置
break;
}
}
}
在进行爬虫实战时,主要的知识点和问题都在这个模块内。
<1> 首先是在获取到英雄皮肤字符串后进行解析的问题。我们的解析目标就是删除皮肤字符串中的所有数字、删除皮肤串中的所有“&”符号、通过“|”符号将字符串进行拆分存储。
① 在下述代码中我使用了正则表达式通过replaceAll删除了皮肤字符串中的所有数字:
pf = pf.replaceAll("\\d+", ""); // 通过正则表达式去除皮肤串中的数字
② 在删除皮肤字符串中的所有“&”符号时,需要将String类型的“&”转换成char类型(.replaceAll()方法内的替换目标字符是char类型),并且需要统计皮肤数量(方便之后构造皮肤链接)。
因此我通过循环使用charAt()方法遍历字符串中每个字符,同时通过toCharArray()方法(必须获取首字符[0])进行字符串类型的转换;然后在这过程中通过表达式统计该英雄的皮肤数量。
int pf_num = 0; // 假设皮肤数量为0
for(int i = 0; i < pf.length(); i++) {
pf_num = (pf.charAt(i) == "&".toCharArray()[0])?pf_num + 1 : pf_num; // 将String类型的字符"&"转换成char类型
}
pf = pf.replaceAll("&", ""); // 删除皮肤串中所有的“&”
③ 在通过皮肤字符串中的字符“|”对字符进行拆分存取到数组时,因为用到了.split()方法,因此需要在字符前面加上双斜杠进行转义。除此之外,在JAVA中“*”、“.”、“/”、“.”都需要在前面加上双斜杠转义才行。
String[] allpf = pf.split("\\|");
<2> for循环算法问题。我的目标是将爬取到的英雄皮肤与链接分别进行一一对应,但刚开始编写这段代码时总是出现多个英雄皮肤的链接都一样的情况。经过调整,我将z=0提取到外面,作为遍历进入当前英雄详情页面后用于遍历英雄皮肤列表的标志;而每次遍历一次英雄列表,直接让z加1并跳出循环。这样,便能够做到英雄皮肤与链接一一对应。
String pfurl = "https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/";
String pic, pic_url = null;
// 创建对应文件夹用于之后存储皮肤壁纸
String pfPath = "D:\\KingGlory\\" + hero + "\\"; // 构造皮肤文件夹存储路径
int z = 0; // 声明在for循环外,一个英雄hero_id对应z.size()-1个皮肤
for( ; z < pfList.size(); ) {
System.out.println("英雄【" + hero + "】的“" + pfList.get(z) + "”皮肤壁纸链接:" + pic_url);
z++; // 输出一次z+1走到下一个皮肤的列表位置
break;
}
通过观察控制台的输出,可以看到从“云中君”到“廉颇”所有英雄的皮肤与链接一一对应,解析完毕。
3.我们在“爬取分析”中的三个目标目前均已完成。因此,接下来我们需要将爬取的链接作为图片壁纸存储到文件夹中。在这个模块中,我们需要用到输入流与输出流的相关知识。
static void saveImage(BufferedInputStream in, String savePath) throws IOException {
byte[] buffer = new byte[1024];
int len = 0;
FileOutputStream fileOutputStream = new FileOutputStream(new File(savePath)); // 创建字节输出流
BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutputStream); // 字节输出流转换成缓冲流
// 皮肤壁纸写入
while((len = in.read(buffer, 0, 1024)) != -1) {
bufferedOut.write(buffer, 0, len);
}
// 缓冲流释放与关闭
bufferedOut.flush();
bufferedOut.close();
}
在编写这个模块时,我的主要问题是对应文件夹的创建问题。在使用Java创建多级目录时,需要用到.mkdirs()方法。(.mkdir()方法只能创建一级目录)其中,使用File对象构建文件夹路径,然后通过.exists()方法判断文件夹是否存在,如果不存在则进行创建。
最后统一将壁纸分别写入到对应英雄名称的文件夹中,用皮肤名称命名。(遍历一个英雄,则创建一个文件夹并将壁纸写入进去)
// 创建对应文件夹用于之后存储皮肤壁纸
String pfPath = "D:\\KingGlory\\" + hero + "\\"; // 构造皮肤文件夹存储路径
File herofile = new File(pfPath); // 创建皮肤文件夹路径
if(!herofile.exists()) {
herofile.mkdirs(); // 文件夹不存在时创建文件夹
}
通过观察控制台的输出,可以看到从“云中君”到“廉颇”所有英雄的皮肤壁纸的动态爬取。
爬取完毕,打开D盘找到KingGlory文件夹,可以看到我们爬取的王者荣耀全英雄全皮肤。
整体代码
package com.test.project;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jsoup.Connection;
import org.jsoup.Connection.Method;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class KingGlory {
/**
* 英雄主页面操作
* 1.请求响应网页源代码
* 2.解析英雄名称、英雄编号、英雄页面
* 3.进入英雄页面
**/
public static void main(String[] args) throws IOException {
// 请求主页面
String url = "https://pvp.qq.com/web201605/herolist.shtml"; // 主页面
Connection connect = Jsoup.connect(url); // 创建连接
Document document = connect.get(); // 请求网页
// System.out.println(document.html()); // 输出网页源代码
// 解析获取英雄名称、英雄编号、英雄页面
Elements elements = document.select("ul.herolist.clearfix").select("li");
for(Element ele : elements) { // 遍历每一个li节点
String hero = ele.select("a").select("img").attr("alt");
String hero_url = ele.select("a").attr("href");
String hero_id = hero_url.substring(hero_url.length()-9, hero_url.length()-6);
heroPage(hero, hero_id, hero_url); // 进入英雄页面并解析出所需数据
// System.out.println("英雄:" + hero + ", 英雄编号:" + hero_id + ", 英雄页面:" + hero_url);
}
}
/**
* 英雄详情页面操作
* 1.进入对应编号英雄页面
* 2.解析英雄页面内的皮肤字符串并存储到皮肤列表
* 3.构造皮肤壁纸下载链接
* 4.保存皮肤壁纸
**/
static void heroPage(String hero, String hero_id, String hero_url) throws IOException {
String url = "https://pvp.qq.com/web201605/" + hero_url;
Document doc = Jsoup.connect(url).get(); // 获取英雄页面源代码
Element ele = doc.select("ul.pic-pf-list.pic-pf-list3").get(0);
String pf = ele.attr("data-imgname"); // 找到皮肤串
pf = pf.replaceAll("\\d+", ""); // 通过正则表达式去除皮肤串中的数字
int pf_num = 0; // 假设皮肤数量为0
for(int i = 0; i < pf.length(); i++) {
pf_num = (pf.charAt(i) == "&".toCharArray()[0])?pf_num + 1 : pf_num; // 将String类型的字符"&"转换成char类型
}
pf = pf.replaceAll("&", ""); // 删除皮肤串中所有的“&”
String[] allpf = pf.split("\\|"); // 在JAVA中需要用“\”进行转义,其中“*”、“.”、“/”、“.”都需要双斜杠
List<String> pfList = new ArrayList<String>(); // 皮肤列表内存储的是对应英雄hero_id的所有皮肤
for(int j = 0; j < allpf.length; j++) {
pfList.add(allpf[j]); // 存储到皮肤列表
}
// System.out.println("英雄【" + hero + "】有" + pf_num + "个皮肤:" + pf);
// // 输出皮肤列表
// for(String apf : pfList) {
// System.out.print(apf + " ");
// }
String pfurl = "https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/";
String pic, pic_url = null;
// 创建对应文件夹用于之后存储皮肤壁纸
String pfPath = "D:\\KingGlory\\" + hero + "\\"; // 构造皮肤文件夹存储路径
File herofile = new File(pfPath); // 创建皮肤文件夹路径
if(!herofile.exists()) {
herofile.mkdirs(); // 文件夹不存在时创建文件夹
}
int z = 0; // 声明在for循环外,一个英雄hero_id对应z.size()-1个皮肤
for(int i = 1; i <= pf_num; i++) {
pic = hero_id + "/" + hero_id + "-bigskin-" + i + ".jpg"; // 构造皮肤链接的后缀
pic_url = pfurl + pic;
// 输出皮肤列表
for( ; z < pfList.size(); ) {
// System.out.println("英雄【" + hero + "】的“" + pfList.get(z) + "”皮肤壁纸链接:" + pic_url);
System.out.println("英雄【" + hero + "】的“" + pfList.get(z) + "”皮肤正在下载中...");
Connection hero_connect = Jsoup.connect(pic_url); // 建立连接
Response hero_response = hero_connect.method(Method.GET).ignoreContentType(true).execute(); // 执行请求
BufferedInputStream bufferedInputStream = hero_response.bodyStream(); // 响应转化为缓冲流
saveImage(bufferedInputStream, pfPath + pfList.get(z) + ".jpg"); // 调用皮肤壁纸保存操作方法
System.out.println("英雄【" + hero + "】的“" + pfList.get(z) + "”下载完毕!");
z++; // 输出一次z+1走到下一个皮肤的列表位置
break;
}
}
////game.gtimg.cn/images/yxzj/img201606/skin/hero-info/513/513-bigskin-2.jpg
}
/**
* 皮肤壁纸保存操作
* @throws IOException
**/
static void saveImage(BufferedInputStream in, String savePath) throws IOException {
byte[] buffer = new byte[1024];
int len = 0;
FileOutputStream fileOutputStream = new FileOutputStream(new File(savePath)); // 创建字节输出流
BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutputStream); // 字节输出流转换成缓冲流
// 皮肤壁纸写入
while((len = in.read(buffer, 0, 1024)) != -1) {
bufferedOut.write(buffer, 0, len);
}
// 缓冲流释放与关闭
bufferedOut.flush();
bufferedOut.close();
}
}