之前一直通过游创工坊来进行祈愿抽卡数据分析,但是广告太多,而且担心
auth_key
泄露,于是自己花了一天时间动手实现了个数据分析工具,数据永久保存在本地,没有信息泄露风险,话不多说,先放源码链接和运行截图
HuTool
、Java
、Maven
Api
API
请求到服务器,查询具体数据,通过抓包或者直接断网刷新,很容易拿到这个API
接口,如下(此处我删去了auth_key
的部分数据,以免信息泄露):https://webstatic.mihoyo.com/hk4e/event/e20190909gacha/index.html?authkey_ver=1&sign_type=2&auth_appid=webview_gacha&init_type=200&gacha_id=ebbfa80fdbc30f7cdfed84670d87c018950878×tamp=1653954735&lang=zh-cn&device_type=mobile&ext=%7b%22loc%22%3a%7b%22x%22%3a463.9453430175781%2c%22y%22%3a330.5148620605469%2c%22z%22%3a1488.346435546875%7d%2c%22platform%22%3a%22Android%22%7d&game_version=CNRELAndroid2.7.0_R8029328_S8227893_D8227893&plat_type=android®ion=cn_gf01&authkey=YJHBE%2bKkY%2fuKzXQ63s05Z2L%2f%2fn%2bEsN0XCx5dzsotpulgXnUovr6wOnbzJMrboBnD9KIhzOTxNkdOHtEZe9ZwZDDXvV70rTLydeHcDBBnQe6likCy0iiXkuEDfKtgBLb8ghbir%2bCDIy%2fsY0fQ4DYP4Ohht38ld%2fWudZR6Xp%2bbOxuQ249u%2fDCwDS4FudukFnKx7peYbhO1FtpFUn7zM%2fVgCum7vxTbk8vzO7wV53BtDmjEkRdyQz2%2bozzyNc4s9l6mNonFrK9rDGtHb2nLiM%2flRoKNYWDIazj7Fs8zaJSI%2bzo5Da%2fdNJwEpHaCAvVpbeDZ5YMqu8rdOZ3A1%2bpWxECTi9RfnaQXuSh1oi6nz3%2bbL9i4KC%2b%2b254wwbJQxLTWmL272gMtJtm7EZfaF21eXmfNhVbY5E2n9lq7P%2bcGZy%2bE4Wzrj7UEp%2fuQ9322Z7t%2b332kgjvwjAj7BGXt%2fZ2RU7jXtg4yLx4OGo0nIqE%2ftLJf2ZnzKZ75LR01WwUP4yKyOToe0un4LxeE3rVBHU4StrFrfN8C39gsE4lTr%2bDoU%2fP82UFIkeQ9lHXDK4H1%2bzvxldyytNr6eCdA5T5iaREMZ0cmq8VtYMr0zVbpyEXEIfjwTIcXoX9nCrh8KH8IKwb9v7OqaWV1Rr5dmjP%2bqlAyn9j1v7xebWLUJw4on%2bO6MIHlqE2t7mjGD%2b%2fsbNgHvLuMDuCGwXQrOFXNTev%2fgK2K9HgEA5gyaeXaotcYhfv2%2by9hi5On8oBaEtxeKiMIZphmb%2fJsEIiDK%2bQDnK4BujqJyekuX%2bz5JpT8tRbW1k%2bIqcRsDKIEO%2bje4iPM9kmOnS8IEngu4kQphc2Kz8CUjytd3VteQBipB%2bz%2f2494UAdkYHlgmUSdYnfR%2bnIzz9aGOb3OA9TkRKwl3R%2bJrIniF3HQdtOew72%2fVvAQ3yNYGMdYV8mfMvvK8GeSFNmAltD64YwQ%2fl%2foU9HzsTzQWB2DWNBExF5zX%2bNt%2fwfOTunHODQmTrMUTgerCuypev44CQZvISRc9CLNPZqRkcgueJOb2e2k5iGgi1BTe%2fjM6TmKQA6TDnANAqyurWKqBP%2b0TcHelBGCaeTbncpA4YLpbjnvdPHzzn2UjP%2bVg%2bWu1rmyTXscm1CI3w94I7AnAfgaHOWAotusROx%2bR%2b4f9WrVnUDpfTeztLiWzK7O%2fvabOT9y6cmgKFZ%2bN79iKiRM9Fm%2b3EOQ4tXF1MnJ7WqSkjUWRtfSwWBH5eg37cx0065aph4Bh4ZYKMLmwa0sjS6gC1F34Q%3d%3d&game_biz=hk4e_cn
authkey_ver
:用户身份类型
authkey
:用户身份标识(一个authkey
对应一个账户)
lang
:当前系统语言
timestamp
:请求时间戳(超过一定时间会使链接失效)
API
是这个:https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog
gacha_type
:祈愿类型(新手祈愿、常驻池祈愿、活动祈愿、武器祈愿)
page
:分页参数,当前页码
size
:分页参数,每一页的大小
end_id
:查询起始数据编码,此次分页请求会从这个ID
开始查询,默认为0时,查询所有数据
API
请求链接转换以及解析数据通过第一步可以看出,本地日志文件(
{user.home}/AppData/LocalLow/miHoYo/原神/output_log.txt
)和断网刷新的请求API
都不是真正获取祈愿数据的链接,需要将这些链接中的四个关键参数和额外拼接的四个参数组合在一起,才是真正获取数据的API
接口
定义请求体(与API
接口返回的字段相对应)
public class GenshinDataResponse {
private int retcode;
private String message;
private GenshinDataPage data;
}
public class GenshinDataPage {
private int page;
private int size;
private int total;
private List<GenshinData> list;
}
API
会返回一个Response
,在Response
中的data
就是祈愿数据,其中每一个祈愿数据的属性与GenshinData
实体想对应,存储在GenshinDataPage
(分页数据)的list
中
构建API
并解析数据
/**
* 通过祈愿接口获取祈愿数据
*
* @param api 祈愿接口
* @return 祈愿数据
*/
public static List<GenshinData> getData(String api) {
String content = HttpRequest.get(api).execute().body();
return Optional.ofNullable(JSON.parseObject(content, GenshinDataResponse.class))
.map(GenshinDataResponse::getData)
.map(GenshinDataPage::getList)
.orElse(null);
}
/**
* 获取祈愿的Api接口
*
* @param endId End数据ID
* @param type 祈愿类型
* @return Api接口
*/
public static String getApi(long endId, GenshinDataType type) {
return BASE_URL
+ DATA_API.substring(DATA_API.indexOf("?"))
+ "&gacha_type=" + type.getCode()
+ "&page=1&size=20&end_id="
+ endId;
}
此处的
DATA_API
就是断网刷新获得的URL
或者是本地日志文件中的URL
,给这个URL
拼接关键的四个参数即可,多余的参数也不需要删除,服务器不会处理
这里既可以选择保存在内存中(
Map
),也可以保存在Redis
、MySQL
数据库中,也可以序列化成JSON
文件保存在磁盘中,各有优劣,本文不做展开,此处选择的是保存在Sqlite
数据库中,防止数据过多造成内存溢出并支持永久保存
/**
* 插入数据
*
* @param genshinData 祈愿记录
*/
public void insert(GenshinData genshinData) {
Entity entity = Entity.create(TABLE_NAME);
entity.set("id", genshinData.getId());
entity.set("uid", genshinData.getUid());
entity.set("gacha_type", genshinData.getGacha_type());
entity.set("item_id", genshinData.getItem_id());
entity.set("count", genshinData.getCount());
entity.set("time", genshinData.getTime());
entity.set("name", genshinData.getName());
entity.set("lang", genshinData.getLang());
entity.set("item_type", genshinData.getItem_type());
entity.set("rank_type", genshinData.getRank_type());
try {
Db.use(DatabaseSource.getDataSource()).insert(entity);
Log.get().info("保存数据成功[{}][{}][{}]",genshinData.getId(), genshinData.getTime(), genshinData.getName());
} catch (SQLException e) {
Log.get().error("保存数据出现错误[{}][{}]", genshinData, e.getMessage());
throw new RuntimeException(e);
}
}
public enum GenshinDataType {
TYPE1(100, "新手祈愿"),
TYPE2(200, "常驻祈愿"),
TYPE3(301, "活动祈愿"),
TYPE4(302, "武器祈愿");
private final int code;
private final String name;
GenshinDataType(int code, String name) {
this.code = code;
this.name = name;
}
public int getCode() {
return this.code;
}
public String getName() {
return this.name;
}
}
for (GenshinDataType value : GenshinDataType.values()) {
Log.get().info("开始同步[{}]祈愿类型数据", value.getName());
List<GenshinData> data = getData(getApi(0, value));
while (!CollUtil.isEmpty(data)) {
for (GenshinData genshinData : data) {
// 避免重复保存数据
if (CONNECTION.exists(genshinData.getId())) {
continue;
}
CONNECTION.insert(genshinData);
}
data = getData(getApi(data.get(data.size() - 1).getId(), value));
Log.get("等待数据同步[1000ms]");
ThreadUtil.safeSleep(1000);
}
Log.get().info("[{}]祈愿类型数据同步完成", value.getName());
}
这一步每一个人的需求不一样,属于动态的业务需求,本人仅需要查看历史五星、四星出货量,平均出五星的抽卡次数,以及各池子历史出货次数。如果想二次开发的,还可以增加指定物品名称,查看出货时间以及命座数
/**
* 通过祈愿物品级别查询数量
*
* @param rankType 祈愿物品级别
* @return 此级别下的物品数量
*/
public int queryRankTypeCount(GenshinRankType rankType) {
String sql = "SELECT COUNT(*) FROM " + DatabaseConnection.TABLE_NAME + " WHERE rank_type = ?";
return CONNECTION.count(sql, rankType.getCode());
}
/**
* 查询祈愿数据总数
*
* @return 总数量
*/
public int queryTotal() {
String sql = "SELECT COUNT(*) FROM " + DatabaseConnection.TABLE_NAME;
return CONNECTION.count(sql);
}
/**
* 通过祈愿类型查询数量
*
* @param type 祈愿类型
* @return 此祈愿类型的总数量
*/
public int queryGenshinDataTypeCount(GenshinDataType type) {
String sql = "SELECT COUNT(*) FROM " + DatabaseConnection.TABLE_NAME + " WHERE gacha_type = ?";
return CONNECTION.count(sql, type.getCode());
}
/**
* 通过祈愿类型和祈愿物品级别查询数量
*
* @param type 祈愿类型
* @param rankType 祈愿物品级别
* @return 当前祈愿类型下的,此祈愿物品级别的总数量
*/
public int queryGenshinDataTypeAndRankTypeCount(GenshinDataType type, GenshinRankType rankType) {
String sql = "SELECT COUNT(*) FROM " + DatabaseConnection.TABLE_NAME + " WHERE gacha_type = ? AND rank_type = ?";
return CONNECTION.count(sql, type.getCode(), rankType.getCode());
}
/**
* 通过祈愿类型查询最近一次抽出五星物品后的抽卡数量
*
* @param dataType 祈愿类型
* @return 最近一次抽出五星物品后的抽卡数量
*/
public int queryGenshinDataTypeByOrderCount(GenshinDataType dataType) {
String sql = "select count(*) from genshin_data where gacha_type = ? " +
"and id > ifnull((select id from genshin_data where rank_type = 5 and gacha_type = ? order by id desc limit 1), 0)";
return CONNECTION.count(sql, dataType.getCode(), dataType.getCode());
}
/**
* 通过祈愿类型和祈愿物品级别查询祈愿物品名称
*
* @param type 祈愿类型
* @return 祈愿物品的名称集合
*/
public List<String> queryData(GenshinDataType type) {
String sql = "SELECT id, name, time FROM " + DatabaseConnection.TABLE_NAME + " WHERE gacha_type = ? AND rank_type = 5 ORDER BY id";
List<Entity> result = CONNECTION.find(sql, type.getCode());
if (CollUtil.isEmpty(result)) {
return new ArrayList<>();
}
List<Integer> countList = new ArrayList<>(result.size());
List<String> names = new ArrayList<>(result.size());
for (int i = 0; i < result.size(); i++) {
// 当前五星的名称
String name = (String) result.get(i).get("name");
// 当前抽取时间
String time = (String) result.get(i).get("time");
// 当前五星的ID
long id = (long) result.get(i).get("id");
// 上一个五星的ID
long lastId = (i > 0) ? (long) result.get(i - 1).get("id") : 0L;
String countSql = "SELECT COUNT(*) FROM " + DatabaseConnection.TABLE_NAME + " WHERE ID > ? and ID <= ? and gacha_type = ?";
// 抽取五星的次数
int count = CONNECTION.count(countSql, lastId, id, type.getCode());
countList.add(count);
// 保存每次抽取五星的次数
names.add(name + "[第" + count + "次祈愿][" + time.substring(0, 10) + "]");
}
RANK_TYPE5.put(type, countList);
return names;
}
整体架构没什么好说的,属于普通的增删查改操作,此处没有选择SpringBoot
+Mybatis
,改用Hutool
工具库提供的数据库操作工具,也没有编写前端展示页面,而是控制台输出结果并将结果保存在文件中,便于随时查看。
可优化地方:
API
到配置文件中,其实可以利用Hutool
的提供的FileWatcher
自动监听用户目录下的日志文件,并过滤出请求参数,手动拼接时间戳,这样即使API
时间过期也可以自动生成新的请求了。如果后续别的小伙伴使用的话,可以考虑做一个。