背景
做一款游戏实时数据展示的APP,用到的数据源有很多家,但要么不太实时,要么数据不准确,为了数据源多一个选择,决定将365网站的实时游戏数据抓取到,提供给app展示
需求
将365 网站的实时数据抓取到,为app端提供稳定的数据展示,如:可以查看明天的比赛列表,看现在正在进行的比赛,双方对战进度,击杀多少,死亡多少,助攻多少,哪对胜利等
主要游戏有:英雄联盟,DOTA2,CS:GO
365网站介绍:
一个全球性的实时比赛的网站,有各种比赛,我们只需要电竞中的英雄联盟,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比赛截图:
问到的问题
开发方式
直接在浏览器的脚步编辑页面,写代码不仅没提示,格式化还很难用,所有需要工程化或者半工程化,打到的效果就是:vscode写代码>提交git>浏览器油猴脚本更新>刷新浏览器即可看效果。
油猴脚本的更新
在脚本设置界面添加了更新URL后,每次提交代码就可以在脚本列表手动更新
更新的url可以利用gitlab的源文件访问
功能,github也有同样的功能
下面是优化后的伪代码:
// ==UserScript==
// @name 365-数据爬取集合
// @namespace xxx365
// @version 3.07
// @description 获取365 多种比赛数据
// @author hanshuqiang
// @match http://*/dota2*
// @match http://*/LOL*
// @match http://*/csgo*
// @match https://wcccom/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=t365.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/365/-/raw/main/tampermonkey/src/utils.js
// @require http://git仓库/hanshuqiang/365/-/raw/main/tampermonkey/src/dingding.js
// @require http://git仓库/hanshuqiang/365/-/raw/main/tampermonkey/src/CSGO.js
// @require http://GIT/hanshuqiang/365/-/raw/main/tampermonkey/src/DOTA2.JS
// @updateURL http://GIT/hanshuqiang/365/-/raw/main/tampermonkey/src/主脚本.js
// @downloadURL http://GIT/hanshuqiang/65/-/raw/main/tampermonkey/src/主脚本.js
// @connect *
// ==/UserScript==
/**
* api地址格式说明:应为从365注入的脚本发网络请求时,是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_365_report&url2=http://后端/v1/lol/lol_365_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有时候浏览器页面会卡住,因此加了定时器刷新页面,结果没过几天被365封了,不得不调低刷新频率,当然,还有牛人做了动态切换代理,即使被封也无所谓
- 问题三:获取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);