王者荣耀每年都会在周年庆举行皮肤返场投票活动,但是王者官方可能是为了防止玩家乱投票,每次加载页面都会将皮肤的投票位置打乱。每个皮肤的投票数显示成一串很长的数字,很难在短时间内看懂投票的情况。这样的话玩家不知道自己喜欢的皮肤在当前的投票排名,进而影响到个人的投票选择。
于是,一个主意在我脑海中出现:使用 PHP cURL 库模拟登录皮肤返场投票页面,然后用定时任务每间隔一分钟获取投票数据。将数据排序、整理成统一格式后存储到 MySQL,然后编写 PHP 接口让前端调用。前端采用 ECharts 将数据图表化显示,这样玩家就很方便地了解投票实时情况。
进入皮肤投票页面,然后抓包获取到实时的投票数据接口。下面使用PHP的cURL库模拟登录,发送请求。
$headers[] = 'content-type: application/x-www-form-urlencoded';
$headers[] = 'origin: https://camp.qq.com';
$headers[] = 'referer: https://camp.qq.com/h5/webdist/camp-activity-anniversary-voting/index.html';
$headers[] = 'sec-fetch-dest: empty';
$headers[] = 'sec-fetch-mode: cors';
$headers[] = 'sec-fetch-site: same-site';
$headers[] = 'user-agent: Mozilla/5.0 (Linux; Android 7.1.2; M2007J17C Build/NZH54D; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Mobile Safari/537.36;GameHelper; smobagamehelper; Brand: Redmi M2007J17C$';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://comm.ams.game.qq.com/ide/?sIdeToken=REJTws&iChartId=148983');
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, '登录参数');
$re = curl_exec($ch);
curl_close($ch);
$res = json_decode($re, true);
print_r($res);
返回数据中键名与皮肤名称的映射如下
$skin = array(
'i10708' => '赵云 - 龙胆',
'i11108' => '孙尚香 - 异界灵契',
'i11207' => '鲁班七号 - 乒乒小将',
'i11303' => '庄周 - 云端筑梦师',
'i11305' => '庄周 - 玄嵩',
'i11306' => '庄周 - 高山流水',
'i11805' => '孙膑 - 天狼运算者',
'i12104' => '芈月 - 白晶晶',
'i12306' => '吕布 - 御风骁将',
'i12703' => '甄姬 - 游园惊梦',
'i12704' => '甄姬 - 幽恒',
'i12806' => '曹操 - 天狼征服者',
'i12904' => '典韦 - 岱宗',
'i14108' => '貂蝉 - 遇见胡旋',
'i15005' => '韩信 - 飞衡',
'i15304' => '兰陵王 - 默契交锋',
'i15407' => '花木兰 - 默契交锋',
'i16708' => '孙悟空 - 孙行者',
'i16804' => '牛魔 - 奔雷神使',
'i17104' => '张飞 - 虎魄',
'i17602' => '杨玉环 - 遇见飞天',
'i18002' => '哪吒 - 逐梦之翼',
'i19005' => '诸葛亮 - 时雨天司',
'i19105' => '大乔 - 白鹤梁神女',
'i19203' => '黄忠 - 烈魂',
'i19904' => '公孙离 - 祈雪灵祝',
'i50204' => '裴擒虎 - 李小龙',
'i51103' => '猪八戒 - 猪悟能',
'i51302' => '上官婉儿 - 梁祝'
);
将数据整理,得到$result,然后存入数据库。
$re = json_decode($re, true);
$data = array();
$list = $re['jData']['AllSkin'];
foreach ($list as $k => $v) {
$data[$v] = array(
'id' => $k,
'name' => $skin[$k],
'score' => $v
);
}
krsort($data);
$result = array();
foreach ($data as $v) {
$result[] = $v;
}
由于每一分钟获取一次投票数据,返场活动的时间是多天的,所以数据量很大。如果将所有数据同时展现出来的话,前端可能加载很慢,图表显示也会很密集。因此,需要将接口的返回数据缩减。接口返回数据包含抽取50条相同间隔的数据和一条最新的实时数据,这样既可以展现出整体投票趋势,也可以实时获取到投票情况。
header('Content-Type:application/json;');
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
//抽取50条数据+最新数据
case 'getAllData':
$config = require_once('./config.php');
try {
$conn = mysqli_connect($config['hostname'], $config['username'], $config['password'], $config['database'], $config['port']);
if (!$conn) throw new Exception('数据库连接失败:' . mysqli_connect_error($conn));
$re = mysqli_query($conn, "SELECT * FROM vote_2022 WHERE id%floor((select max(id) from vote_2022)/50)=0 OR id=(select max(id) from vote_2022) ORDER BY id DESC");
//将JSON转为数组
$tmp = [];
while ($res = mysqli_fetch_assoc($re)) {
$data = json_decode($res['data'], true);
$tmp[] = ['time' => $res['time'], 'data' => $data];
}
//转为符合ECharts格式
$data = [];
for ($i = count($tmp) - 1; $i >= 0; $i--) {
$data['time'][] = $tmp[$i]['time'];
foreach ($tmp[$i]['data'] as $value) {
$data['i' . $value['id']][] = $value['score'];
}
}
$result = [
'code' => 1,
'msg' => '获取成功',
'data' => $data
];
} catch (Exception $e) {
$result = [
'code' => 0,
'msg' => $e->getMessage(),
'data' => null
];
}
break;
//获取最新排名数据
case 'getLatestData':
$config = require_once('./config.php');
try {
$conn = mysqli_connect($config['hostname'], $config['username'], $config['password'], $config['database'], $config['port']);
if (!$conn) throw new Exception('数据库连接失败:' . mysqli_connect_error($conn));
$re = mysqli_query($conn, "SELECT * FROM vote_2022 ORDER BY id DESC LIMIT 1");
$res = mysqli_fetch_assoc($re);
$info = json_decode($res['data'], true);
$data = [];
$rank = 1;
foreach ($info as $value) {
$data['skinList'][] = $rank . '-' . $value['name'];
$data['scoreList'][] = $value['score'];
$rank++;
}
$result = [
'code' => 1,
'msg' => '获取成功',
'data' => $data
];
} catch (Exception $e) {
$result = [
'code' => 0,
'msg' => $e->getMessage(),
'data' => null
];
}
break;
default:
$result = [
'code' => 0,
'msg' => '操作未知',
'data' => null
];
break;
}
exit(json_encode($result, JSON_UNESCAPED_UNICODE));
在ECharts的模板网站选择了一个比较符合的模板,修改图表配置,然后通过AJAX异步加载数据渲染。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>2022周年庆皮肤返场投票趋势title>
<script src="//cdn.staticfile.org/jquery/2.2.4/jquery.min.js">script>
<script src="//cdn.staticfile.org/echarts/5.2.2/echarts.common.min.js">script>
<script
type="text/javascript"
src="//cdn.staticfile.org/layer/3.5.1/layer.min.js"
>script>
<style>
html,
body {
width: 100%;
height: 100%;
}
#chart {
width: 100%;
height: 100%;
}
style>
head>
<body>
<div id="chart">div>
<script>
let colorList = [];
for (let index = 0; index < 30; index++) {
colorList.push(rdmRgbColor());
}
getData();
function rdmRgbColor() {
//随机生成颜色
let arr = [];
for (let i = 0; i < 3; i++) {
arr.push(Math.floor(Math.random() * 256));
}
let [r, g, b] = arr;
// rgb颜色
// let color=`rgb(${r},${g},${b})`;
// 16进制颜色
let color = `#${
r.toString(16).length > 1 ? r.toString(16) : "0" + r.toString(16)
}${g.toString(16).length > 1 ? g.toString(16) : "0" + g.toString(16)}${
b.toString(16).length > 1 ? b.toString(16) : "0" + b.toString(16)
}`;
return color;
}
function getData() {
$.ajax({
type: "GET",
url: "./api.php?action=getAllData",
dataType: "json",
success: function (re) {
if (re.code == 1) {
let myChart = echarts.init(document.getElementById("chart"));
const skinList = {
i10708: "赵云 - 龙胆",
i11108: "孙尚香 - 异界灵契",
i11207: "鲁班七号 - 乒乒小将",
i11303: "庄周 - 云端筑梦师",
i11305: "庄周 - 玄嵩",
i11306: "庄周 - 高山流水",
i11805: "孙膑 - 天狼运算者",
i12104: "芈月 - 白晶晶",
i12306: "吕布 - 御风骁将",
i12703: "甄姬 - 游园惊梦",
i12704: "甄姬 - 幽恒",
i12806: "曹操 - 天狼征服者",
i12904: "典韦 - 岱宗",
i14108: "貂蝉 - 遇见胡旋",
i15005: "韩信 - 飞衡",
i15304: "兰陵王 - 默契交锋",
i15407: "花木兰 - 默契交锋",
i16708: "孙悟空 - 孙行者",
i16804: "牛魔 - 奔雷神使",
i17104: "张飞 - 虎魄",
i17602: "杨玉环 - 遇见飞天",
i18002: "哪吒 - 逐梦之翼",
i19005: "诸葛亮 - 时雨天司",
i19105: "大乔 - 白鹤梁神女",
i19203: "黄忠 - 烈魂",
i19904: "公孙离 - 祈雪灵祝",
i50204: "裴擒虎 - 李小龙",
i51103: "猪八戒 - 猪悟能",
i51302: "上官婉儿 - 梁祝",
};
let heroList = [];
let selectList = {};
let dataList = [];
let index = 0;
for (let key in re.data) {
if (key != "time") {
heroList.push(skinList[key]);
if (index < 10) {
selectList[skinList[key]] = true;
} else {
selectList[skinList[key]] = false;
}
dataList.push({
name: skinList[key],
type: "line",
data: re.data[key],
symbolSize: 1,
symbol: "circle",
smooth: true,
yAxisIndex: 0,
showSymbol: false,
emphasis: {
focus: "series",
},
lineStyle: {
width: 2,
shadowColor: "rgba(158,135,255, 0.3)",
shadowBlur: 10,
shadowOffsetY: 20,
},
itemStyle: {
normal: {
color: colorList[index],
borderColor: colorList[index],
},
},
markPoint: {
symbol: "pin",
symbolSize: 50,
itemStyle: {
borderColor: "#000",
borderWidth: 0,
borderType: "solid",
},
},
});
index++;
}
}
let option = {
backgroundColor: "#fff",
title: {
text: "投票趋势图",
textStyle: {
fontSize: 14,
fontWeight: 400,
},
left: "center",
top: "-4px",
show: true,
},
legend: {
x: "center",
y: "top",
show: true,
top: "5%",
right: "5%",
itemWidth: 6,
itemGap: 20,
textStyle: {
color: "#556677",
},
data: heroList,
selected: selectList,
},
tooltip: {
trigger: "axis",
order: "valueDesc",
axisPointer: {
label: {
show: true,
backgroundColor: "#fff",
color: "#556677",
borderColor: "rgba(0,0,0,0)",
shadowColor: "rgba(0,0,0,0)",
shadowOffsetY: 0,
},
lineStyle: {
width: 0,
},
},
backgroundColor: "#fff",
textStyle: {
color: "#5c6c7c",
},
padding: [10, 10],
extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
},
grid: {
top: "15%",
y2: 88,
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
xAxis: [
{
type: "category",
data: re.data.time,
axisLine: {
show: true,
lineStyle: {
color: "#DCE2E8",
},
},
axisTick: {
show: true,
},
axisLabel: {
textStyle: {
color: "#556677",
},
fontSize: 12,
margin: 15,
},
axisPointer: {
label: {
padding: [0, 0, 10, 0],
margin: 15,
fontSize: 12,
backgroundColor: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#fff",
},
{
offset: 0.86,
color: "#fff",
},
{
offset: 0.86,
color: "#33c0cd",
},
{
offset: 1,
color: "#33c0cd",
},
],
global: false,
},
},
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
},
},
boundaryGap: false,
},
],
yAxis: [
{
type: "value",
offset: -20,
name: "投票数量",
axisTick: {
show: false,
},
axisLine: {
show: true,
lineStyle: {
color: "#DCE2E8",
},
},
axisLabel: {
textStyle: {
color: "#556677",
},
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
},
},
},
],
series: dataList,
};
myChart.setOption(option);
window.onresize = function () {
myChart.resize();
};
} else {
layer.alert(re.msg);
}
},
error: function () {
layer.alert("加载失败");
},
});
}
script>
body>
html>
##演示效果
演示地址
本项目严禁用于任何违法违规用途,仅供学习使用,其他用途产生的一切违法行为、后果或纠纷与开发者无关。