### 前景提要
今天逛B站看了一个我喜欢的up主发的视屏,(音乐区up主)。然后视屏下面有人评论说要赞助抽些奖品。我就随手写了评论。要帮他写抽奖。
完了之后想了下实现逻辑,亿点都不难。只需要获取到所有的评论信息,然后写随机数就可以了。
### 踩坑日记
然后我忙完手里的工作后用浏览器抓包看了下评论信息的URL。并试着用java请求。好吧失败了。然后浏览器多抓了几次包,分析浏览器请求。发现最后一位是随机数,仔细看了好像是当前时间的毫秒值。然后我把数字转成时间。好家伙。果然是当前请求的毫秒数。然后拼接参数后继续请求。还是失败。没办法只能继续看请求了。发现中间有一段参数每次请求都会递增,然后继续组装参数递增。还是请求失败
好吧我在想是不是判断了我当前请求的客户端是不是为浏览器。然后我设置了请求头,结果还是失败。
最后我想,肯定不止我一个人闲的没事扒B站评论呐,看看别人是怎么扒的。然后我百度了一下。果然还是有很多人扒的,然后我发现,他们的请求居然没有两个会变得参数。然后我删掉两个参数后。what ? 请求到数据了。。。。
后面的工作就简单多了,只需要将json格式的数据转成我熟悉的java对象来操作就好了。
然后,B站返回的数据实在是太多了,一个身份信息就用了好多个字段,判断是不是大会员呀之类的。好了我们用不到,那就先忽略吧,随意拿了一些属性出来构建实体。
### 代码实现
import lombok.Data;
/**
* b 站视频评论数据格式对象
*/
@Data
public class BiliBili {
private Integer code;
private String message;
private Integer ttl;
private BiliBiliData data;
}
import lombok.Data;
import java.util.List;
/**
* b 站视频评论数据格式Data对象
*/
@Data
public class BiliBiliData {
/**
* 分页信息
*/
private BiliBiliPage page;
/**
* 评论信息
*/
private List<BiliBiliReplies> replies;
}
import lombok.Data;
/**
* 分页数据
*/
@Data
public class BiliBiliPage {
private Integer count;
private Integer num;
private Integer size;
}
import lombok.Data;
import java.util.List;
@Data
public class BiliBiliReplies {
private Integer rpid;
private Integer oid;
private Integer type;
private Integer mid;
private Integer root;
private Integer parent;
private Integer dialog;
private Integer count;
private Integer rcount;
private Integer state;
private Integer fansgrade;
private Integer attr;
private Integer ctime;
private String rpid_str;
private String root_str;
private String parent_str;
private Integer like;
private Integer action;
/**
* 用户信息
*/
private BiliBiliUser member;
/**
* 评论内容
*/
private BiliBiliComment content;
/**
* 下级评论
*/
private List<BiliBiliReplies> replies;
}
import lombok.Data;
@Data
public class BiliBiliUser {
private String mid;
private String uname;
private String sex;
private String sign;
private String avatar;
private String rank;
}
import lombok.Data;
/**
* b 站视频评论内容爬取转JSON
*/
@Data
public class BiliBiliComment {
private String message;
}
构建完这些实体,下面该实现抽奖的功能了
我用了hutool工具来做了json的转换合http请求和集合的判断
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.1</version>
</dependency>
昨天写的时候评论比较少,评论没有分页今天发现默认的评论展示是每页十条,所以添加了分页的处理。原有基础上添加了分页的处理
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONUtil;
import java.util.*;
import java.util.stream.Collectors;
public class BiliBiliCrawler {
/**
* 抽奖次数
*/
private static final Integer COUNT = 3;
/**
* 是否取消发布者的中奖资格 true 取消 false 不取消 .默认取消 哈哈
*/
private static final boolean IS_CANCEL = true;
public static void main(String[] args) {
String url = "https://api.bilibili.com/x/v2/reply/reply?&jsonp=jsonp&pn=1&type=1&oid=418654756&ps=10&root=4747243408";
String str = HttpRequest.get(url).execute().body();
// 转java对象
BiliBili bilibili = JSONUtil.toBean(str, BiliBili.class);
// 获取分页信息
BiliBiliPage page = bilibili.getData().getPage();
// 计算页数
int total = (page.getCount() + page.getSize() - 1) / page.getSize();
if (total == 0) {
System.out.println("未获取到评论信息");
return;
}
// 用户名集合
List<String> list = new ArrayList<>(page.getCount());
// 用户评论集合,一个用户可能存在多条评论信息
Map<String, List<String>> con = new HashMap<>((int) Math.ceil(page.getCount() / 0.75) + 1);
// 填充集合,准备抽奖
for (int i = 1; i <= total; i++) {
// 赋值页码,追加到评论之后,方便找到中奖者
int a = i;
// 第一次请求的数据在获取页数的时候就已经得到了所以第一次是不需要重新请求的
BiliBili bili;
if (i != 1) {
String sf = StrFormatter.format("https://api.bilibili.com/x/v2/reply/reply?&jsonp=jsonp&pn={}&type=1&oid=418654756&ps=10&root=4747243408", i);
String json = HttpRequest.get(sf).execute().body();
// 转java对象
bili = JSONUtil.toBean(json, BiliBili.class);
} else {
bili = bilibili;
}
// 具体评论信息
List<BiliBiliReplies> replies = bili.getData().getReplies();
replies.forEach(e -> {
// 是否取消发布者的中奖资格,并不了解B站各个字段是否具有对应关系,这里手动写死
if (IS_CANCEL && e.getMid().equals(447485021)) {
return;// 跳出本次循环
}
String uname = e.getMember().getUname();
list.add(uname);
List<String> list1 = con.get(uname);
if (IterUtil.isEmpty(list1)) {
List<String> li = new ArrayList<>();
li.add(e.getContent().getMessage() + " (评论所在页码:" + a + " )");
con.put(uname, li);
} else {
list1.add(e.getContent().getMessage());
}
}
);
}
// 去除重复,保证每个人只有一次机会中奖,保证中奖几率对等
List<String> collect = list.stream().distinct().collect(Collectors.toList());
// 抽奖次数大于等于评论人数,全部中奖,不需要抽取,人手一个
if (COUNT >= collect.size()) {
System.out.println("中奖人数列表: ");
for (String userName : collect) {
StringBuilder sb = new StringBuilder("中奖用户名 : ");
sb.append(userName);
sb.append(" , 评论内容为 : [ ");
List<String> list1 = con.get(userName);
for (int i = 0; i < list1.size(); i++) {
sb.append(list1.get(i));
// 存在多条评论,用逗号分开
if (i != list1.size() - 1) {
sb.append(" || ");
}
}
sb.append(" ]");
System.out.println(sb.toString());
}
return;
}
// 打乱名单,使得中奖相对公平
Collections.shuffle(list);
// 循环抽取幸运用户 ~
System.out.println("中奖人数列表: ");
for (int i = 0; i < COUNT; i++) {
String userName = collect.remove(new Random().nextInt(collect.size()));
StringBuilder sb = new StringBuilder("中奖用户名 : ");
sb.append(userName);
sb.append(" , 评论内容为 : [ ");
List<String> list1 = con.get(userName);
for (int j = 0; j < list1.size(); j++) {
sb.append(list1.get(j));
// 存在多条评论,用 || 分开
if (j != list1.size() - 1) {
sb.append(" || ");
}
}
sb.append(" ]");
System.out.println(sb.toString());
// 重新打乱集合
Collections.shuffle(collect);
}
}
}
### 实现效果(抽取三名)
第一次抽取 :
第二次抽取: