HPanpan

Bet365游戏数据爬取总结  编辑 

背景

做一款游戏实时数据展示的APP,用到的数据源有很多家,但要么不太实时,要么数据不准确,为了数据源多一个选择,决定将bet365网站的实时游戏数据抓取到,提供给app展示

需求

将bet365 网站的实时数据抓取到,为app端提供稳定的数据展示,如:可以查看明天的比赛列表,看现在正在进行的比赛,双方对战进度,击杀多少,死亡多少,助攻多少,哪对胜利等

主要游戏有:英雄联盟,DOTA2,CS:GO

bet365网站介绍:

一个全球性的实时比赛的网站,有各种比赛,我们只需要电竞中的英雄联盟,DOTA2,CS:GO

中国区(应该是亚洲)域名xxxxx 不需科学上网就能访问,还有国外才能访问的域名xxxxx,内容都是一样的

解决方案

兵分两路:一路后端同学做接口层面的破解(websocket),我是利用js从页面抓取数据(前端爬虫)

考虑到365做了很多反爬手段,那种无头浏览器肯定是不能用了,最后采用油猴脚本抓取dom,然后将数据上报给后端接口,后端在做一些业务处理,最后提供给app

下面就具体看看油猴脚本的开发以及遇到的问题

实践

油猴介绍:Tampermonkey 是第一个可以用来让 Chrome 支持更多 UserScript 的 Chrome 扩展,也就是可以向网站注入自己写的js脚本,同时还支持调用一些API,如:本地存储,GM_registerMenuCommand,GM_xmlhttpRequest跨域请求等

原型程序就是下面这样,定时获取dom元素,然后post给后端接口,网络请求可以自己用XMLHttpRequest封装一个,也可以调用第三方库,也可以使用油猴自带的GM_xmlhttpRequest,对于油猴脚本的具体使用方式这里就不写了,看看其他文章吧,

// ==UserScript==
// @name         爬虫原型程序
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        */*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    setInterval(()=>{
        let name = document.querySelector('.title')
        let res = Post('url',{name:name})
    },2000)

})();

这是DOTA2比赛截图:

image.png

问到的问题

开发方式

直接在浏览器的脚步编辑页面,写代码不仅没提示,格式化还很难用,所有需要工程化或者半工程化,打到的效果就是:vscode写代码>提交git>浏览器油猴脚本更新>刷新浏览器即可看效果。

油猴脚本的更新

在脚本设置界面添加了更新URL后,每次提交代码就可以在脚本列表手动更新

image.png

更新的url可以利用gitlab的源文件访问功能,github也有同样的功能

image.png 下面是优化后的伪代码:

// ==UserScript==
// @name         bet365-数据爬取集合
// @namespace    https://www.365-288.com/
// @version      3.07
// @description  获取bet365 多种比赛数据
// @author       hanshuqiang
// @match        http://*/dota2*
// @match        http://*/LOL*
// @match        http://*/csgo*
// @match        https://www.365-288.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bet365.com
// @grant        GM_xmlhttpRequest
// @grant          GM_setClipboard
// @grant          GM_addStyle
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_listValues
// @grant          GM_deleteValue
// @grant          GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.4/moment.min.js
// @require      http://git仓库/hanshuqiang/bet365/-/raw/main/tampermonkey/src/utils.js
// @require      http://git仓库/hanshuqiang/bet365/-/raw/main/tampermonkey/src/dingding.js
// @require      http://git仓库/hanshuqiang/bet365/-/raw/main/tampermonkey/src/CSGO.js
// @require      http://git/hanshuqiang/bet365/-/raw/main/tampermonkey/src/DOTA2.JS
// @updateURL    http://git/hanshuqiang/bet365/-/raw/main/tampermonkey/src/主脚本.js
// @downloadURL  http://git/hanshuqiang/bet365/-/raw/main/tampermonkey/src/主脚本.js
// @connect      *
// ==/UserScript== 

/** 
 * api地址格式说明:应为从bet365注入的脚本发网络请求时,是https到http ,会有限制,所以需要将数据先发到本地服务,由本地服务端请求后端的上报API
 */
(function () {
    'use strict';

    //如果不是比赛详情页,不执行
    if (location.href.indexOf('https://www.365-288.com/#/IP') == -1 && location.href.indexOf('192.168') == -1) {
        return
    }
    console.log('初始化主脚本...');
    GM_setValue("DOTA2_API", 'http://127.0.0.1:7001/add?url=http://后端/v1/dota2/report&url1=http://后端/v1/dota2/report');
    GM_setValue("LOL_API", 'http://127.0.0.1:7001/add?url=http://后端&url1=http://后端/v1/lol/lol_bet365_report&url2=http://后端/v1/lol/lol_bet365_report');
    GM_setValue("CSGO_API", 'http://127.0.0.1:7001/add?url=http://后端/v1/esport/csgo/match/data');
    let Dota2ApiConfirm = () => {
        let defauleV = GM_getValue("DOTA2_API") || 'http://127.0.0.1:7001/add?url=http://后端/dota2'
        let apiUrl = prompt("DOTA2上报API:", defauleV);
        GM_setValue("DOTA2_API", apiUrl);
    }
    let LoLApiConfirm = () => {
        let defauleV = GM_getValue("LOL_API") || 'http://127.0.0.1:7001/add?url=http://后端'
        let apiUrl = prompt("LOL上报API:", defauleV);
        GM_setValue("LOL_API", apiUrl);
    }
    let CSGOApiConfirm = () => {
        let defauleV = GM_getValue("CSGO_API") || 'http://127.0.0.1:7001/add?url=http://后端/v1/esport/csgo/match/data'
        let apiUrl = prompt("CSGO上报API:", defauleV);
        GM_setValue("CSGO_API", apiUrl);
    }
    GM_registerMenuCommand("DOTA2-API", Dota2ApiConfirm, "D");
    GM_registerMenuCommand("LOL-API", LoLApiConfirm, "L");
    GM_registerMenuCommand("CS:GO-API", CSGOApiConfirm, "C");
    let setInt = setInterval(() => {
        let sj = document.querySelector('.ipe-EventHeader_BreadcrumbText ')
        if (sj) {
            clearInterval(setInt)
            if (sj && sj.innerText && sj.innerText.indexOf('CS:GO') != -1) {
               // CSGO()
            }
            if (sj && sj.innerText && sj.innerText.indexOf('反恐精英') != -1) {
              //  CSGO()
            }
            if (sj && sj.innerText && sj.innerText.indexOf('英雄') != -1) {
                LOL()
            }
            if (sj && sj.innerText && sj.innerText.indexOf('LOL') != -1) {
                LOL()
            }
            if (sj && sj.innerText && sj.innerText.indexOf('DOTA2') != -1) {
                DOTA2()
            }
        }
    }, 1000);
})()
  • 问题一:从油猴脚本中发送post请求到后端,因为是https的页面发送到http 所有会报错, 可参考 碰到https请求下发送http请求问题 所以先post数据到本地(127.0.0.1)的一个service,本地servce在转发到后端接口
  • 问题二:定时获取dom有时候浏览器页面会卡住,因此加了定时器刷新页面,结果没过几天被bet365封了,不得不调低刷新频率,当然,还有牛人做了动态切换代理,即使被封也无所谓
  • 问题三:获取dom内容很常用,因此封装了一些常用函数,如getInnerText(selector)
  • 问题四:使用GM_registerMenuCommand,GM_setValue,GM_getValue 使上报接口可以动态更改,但有点用不到,最后程序改成了了前端将上报的地址(多个,如正式环境,测试环境)和数据先post到本地service,本地servic 异步分发。

上报方式除了定时获取dom还可以监控dom变化再上报(使用MutationObserver)

参考:

let targetNode = document.querySelector('.mld-MapView_Container ')
let config = {
    // attributeFilter:[],一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知
    attributes: true,//当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false。
    characterData: true,
    childList: true,//当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false
    subtree: true,//当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false
    // attributeOldValue: false,//当为 true 时,记录上一次被监听的节点的属性变化;可查阅 MutationObserver 中的 Monitoring attribute values 了解关于观察属性变化和属性值记录的详情。默认值为 false。
    characterData: true,//当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false
    characterDataOldValue: false
};
// 当观察到突变时执行的回调函数
let callback = function (mutationsList) {
    let isChange = false
    let mu = mutationsList.length
    console.log('变化allHero:', mu.length);
    let send = async () => {
        let domData = await getDomData()
        let formatData = await formatDomData(domData)
        await postDomData(formatData)
    }
    send()
};
// 创建一个链接到回调函数的观察者实例
observer = new MutationObserver(callback);
// 开始观察已配置突变的目标节点
observer.observe(targetNode, config);