ECharts绘制王者荣耀周年庆皮肤返场投票热度图

介绍

王者荣耀每年都会在周年庆举行皮肤返场投票活动,但是王者官方可能是为了防止玩家乱投票,每次加载页面都会将皮肤的投票位置打乱。每个皮肤的投票数显示成一串很长的数字,很难在短时间内看懂投票的情况。这样的话玩家不知道自己喜欢的皮肤在当前的投票排名,进而影响到个人的投票选择。

于是,一个主意在我脑海中出现:使用 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>

##演示效果

ECharts绘制王者荣耀周年庆皮肤返场投票热度图_第1张图片

演示地址

声明

本项目严禁用于任何违法违规用途,仅供学习使用,其他用途产生的一切违法行为、后果或纠纷与开发者无关。

你可能感兴趣的:(php,mysql,echarts,html,javascript,前端,爬虫)