JS写小游戏「跳一跳」外挂之自己动

上篇文章写道怎么通过Canvas识别跳转位置,本篇文章重点介绍怎样使用adb命令操作「小人」自己挑动。

adb是什么

adb是Andorid Debug Bridge,可以将安卓手机打开USB调试模式,然后连接USB线到电脑,就可以通过adb执行调试命令。

adb安装

mac下面使用brew安装

brew cask install android-platform-tools

windows下面去搜索下载,然后放到环境变量里面去,保证命令行执行adb可以成功。

测试连接成功

使用adb devices查看是否连接成功,如果连接成功会出现设备的编号。

小人跳转需要的命令

整个自动跳动的流程是这样的:

  1. 调用adb命令获取手机当前屏幕截图
  2. 拉取截图到本地路径
  3. 通过js读取图片分析中心点位置计算跳转需要按压时长
  4. 同adb命令发送长按命令

通过node的child_process核心代码执行adb的命令如下:

//截屏,放到sdcard的根目录下
adb shell screencap -p /sdcard/screencap.png
//拉取截图图片到本地电脑
//   将remote路径的图片拉取到本地的路径
adb pull ${SCREENCAP_REMOTE_PATH} ${SCREENCAP_PATH}/screencap.png
//发起长按,swipe后面是开始和结束的手指位置坐标,timeout是时长
adb shell input swipe ${r + 10} ${r + 20} ${r - 10} ${r - 2} ${timeout}

特殊说明

2.04是跳转系数,这个是从其他代码里面直接拿过来的,对应的是720宽度的手机会比较准确,可能不同的手机dpi和屏幕宽度会有不同的值,具体实践中需要根据自己手机情况调整一下。

我在红米4手机上面可以轻松几乎每次都是中心点,轻松过千。

可以看下面的红米机器的视频:

https://v.qq.com/x/page/t1331wmep7e.html

JS写小游戏「跳一跳」外挂之Canvas图像识别

17年结尾的时候微信发布新版重点推出了「小游戏」概念,H5的游戏再次火了起来,新版微信开屏的游戏就是「跳一跳」游戏可玩度很高,网上也出现了各种语言版本的外挂,前几天看到一篇用nodejs搭建的外挂,需要手动点击截屏图片来判断当前和下一步的位置然后跳转,于是就起了用Canvas来实现图像的想法,后面有实现了自动跳转,算是齐活了。今天来完整说下图像识别。

代码都放到了:https://github.com/ksky521/wechat-jump-game-hack 欢迎自己去尝试

先来看最终效果视频:https://v.qq.com/x/page/o1331igmskh.html

Canvas图像处理的原理

Canvas可以通过drawImage在上面添加图片,然后通过getImageData方法获取一个imageData对象,此对象包括了datawidthheight,其中data为图片widthheight4长度的数组,每个像素点表现在数组内为:RGBA四个0~255的值,即Red、Green、Blue和Alpha值。

通过对这个imageData.data进行遍历操作,可以利用图像差值比较找出图片内物体的边缘、物体的中心点,也可以根据图像中某个固定颜色范围的物体,进行匹配,从而找到「小人」的位置。

颜色值差值比较函数

先介绍一个函数tolerenceHelper,用来比较颜色差值,即传入需要比较的rgb,然后跟对比的rtgtbt和差值范围的t进行对比的函数,在范围内则返回true

function tolerenceHelper(r, g, b, rt, gt, bt, t) {
    return r > rt - t && r < rt + t 
            && g > gt - t && g < gt + t 
            && b > bt - t && b < bt + t;
}

获取小人当前位置

小人获取位置用的方式是差值比较,首先通过截屏中的紫色小人颜色范围,可以大致拿到小人的颜色值为:

// 小人的颜色值
const playerR = 40;
const playerG = 43;
const playerB = 86;

用Node抓站(三):防止被封

抓取如果抓取的太快太频繁会被源站封IP,本文会介绍下通过限流、限速和使用代理的方式来防止被封

上篇文章,抓取「电影天堂」最新的170部电影,在抓取首页电影list之后,会同时发出170个请求抓取电影的详情页,这样在固定时间点集中爆发式的访问页面,很容易在日志中被找出来,而且并发请求大了,很可能会中网站的防火墙之类的策略,IP被加到黑名单就悲剧了

限流&限速

先说下限流的方法,将批量的并发请求,分成多次固定请求个数,等上一次抓取结束后,再开始下一次抓取,直到全部抓取结束。

这里我使用async模块限制并发次数,async主要有:集合、流程和工具三大类方法,这里我使用eachLimit(arr, limit, iterator, [callback]),所有修改是上篇文章的fetchContents方法,该方法接受抓取到的170个文章的url list,这次通过eachLimit将170个url按照3个一组并发,依次执行,具体代码如下:

function fetchContents (urls) {
  return new Promise((resolve, reject) => {
    var results = []
    async.eachLimit(urls, 3, (url, callback) => {
      spider({url: url, decoding: 'gb2312'}, {
        url: {
          selector: '#Zoom table td a!text'
        },
        title: {
          selector: '.title_all h1!text'
        }
      }).then((d) => {
        results.push(d)
        callback()
      }, () => {
        callback()
      })
    }, () => {
      resolve(results)
    })
  })
}

限流只是控制了一次并发的请求数,并没有让抓取程序慢下来,所以还需要限速,在限流的基础上限速就变得很简单,只需要在执行eachLimitcallback的时候,加上个Timer就好了,为了方便查看限速的效果,每次抓取成功之后,都console.log显示时间,所以改完的代码如下:

function fetchContents (urls) {
  return new Promise((resolve, reject) => {
    var results = []
    async.eachLimit(urls, 3, (url, callback) => {
      spider({url: url, decoding: 'gb2312'}, {
        url: {
          selector: '#Zoom table td a!text'
        },
        title: {
          selector: '.title_all h1!text'
        }
      }).then((d) => {
        var time = moment().format(‘HH:MM:ss')
        console.log(`${url}===>success, ${time}`)
        results.push(d)
        setTimeout(callback, 2e3)
      }, () => {
        callback()
      })
    }, () => {
      resolve(results)
    })
  })
}

效果如下:

避免重复抓取

因为一些网站更新比较慢,我们写的抓取程序在定时脚本任务(crontab)跑的时候,可能网站还没有更新,如果不做处理会造成资源的浪费,尤其国内不少VPS都是有流量限制的,不做控制,真金白银就打水漂了。。

用Node抓站(二):Promise使代码更优雅

本文主要目的是通过抓取「电影天堂」的最新电影名称和下载地址,展现如何抓取列表之后,继续抓取正文内容

使用《用Node抓站(一)》(没看过的可以翻看下本公众号的历史文章)当中写的spider.js 代码可以直接用下面的代码把列表抓出来:

var spider = require('../lib/spider')

spider({
  url: 'http://www.dytt8.net/index.htm',
  decoding: 'gb2312'
}, (err, data, body, req) => {
  if (!err) {
    console.log(data)
  }
}, {
  items: {
    selector: '.co_area2 .co_content2 ul a!attr:href'
  }
})

这里不同的是涉及到一个编码问题,「电影天堂」用的是gb2312编码,需要转成utf8,不然抓的内容会乱码。我扩展了request模块的参数增加了decoding:因为encoding被占用了,而且为了转码方便,我将encoding设为null,这样出来的数据就是Buffer,可以直接用iconv-lite之类的进行转码,涉及到编码问题不是本文讨论内容,就不多说了。

抓取列表后,发现title是被截断的,也要在正文页面抓取一下;继续写抓取下载地址和电影title的代码:

spider({
  url: 'http://www.dytt8.net/index.htm',
  decoding: 'gb2312'
}, (err, data, body, req) => {
  if (!err) {
    if (data && data.items) {
      var urls = data.items
      urls.forEach(function (url) {
        url = 'http://www.dytt8.net' + url
        spider({url: url, decoding: 'gb2312'}, (e, d) => {
          if (!e) {
            console.log(d)
          }
        }, {
          url: {
            selector: '#Zoom table td a!text'
          },
          title: {
            selector: '.title_all h1!text'
          }
        })
      })
    }
  }
}, {
  items: {
    selector: '.co_area2 .co_content2 ul a!attr:href'
  }
})

看上去挺简单的,但是回调好多啊。。。

处理这种异步回调可以使用Promise!

Web前端页面劫持和反劫持

前几天看到一篇写js文件反劫持的文章,想起15年主导做百度搜索结果页面反劫持项目做得一些研究,整理成文章,跟大家分享。

常见劫持手段

按照劫持的方法不同,我将劫持分为下面两类:

  • 跳转型劫持:用户输入地址A,但是跳转到地址B
  • 注入型劫持:有别于跳转型型劫持,指通过在正常的网页中注入广告代码(js、iframe等),实现页面弹窗提醒或者底部广告等,又分为下面三个小类:
    • 注入js类劫持:在正常页面注入劫持的js代码实现的劫持
    • iframe类劫持:将正常页面嵌入iframe或者页面增加iframe页面
    • 篡改页面类劫持:正常页面出现多余的劫持网页标签,导致页面整体大小发生变化

跳转型劫持

为了获取流量,一些电商或者类似百度这样需要流量合作的网站都会有自己的联盟系统,通过给予一些奖励来获取导流,比如:百度或者电商会有渠道分成。

为了区分哪些是第三方给予导流过来的,通常会在url地址增加类似source、from之类的参数,或者进入页面之前通过「中间页」种cookie。

这样,当用户输入一个正常网址的时候,劫持方会在网络层让其跳转到带分成或者渠道号的「中间页」或者带渠道号的页面。这样用户进行下单或者搜索等行为,劫持方会得到「佣金」。

上面说的这类case还算友好,至少用户一般体验不到页面变化,还有类似跳转到钓鱼网站的case,也有不正当竞争的case:用户输入baidu.com跳转到so.com或者sm.cn,而对方网站有故意做成和百度搜索差不多的样子,那时候也帮助法务做了很多案例收集。

题外话:前些年,用户使用百度搜索某些医疗类query,立即用户就会收到电话推广医院,很多用户投诉,不明真相的群众也指责百度,实际这类是运营商把url的关键词卖给了医疗机构,百度只不过是躺枪。。。那时候还做了个项目是加密query。。。

注入型劫持

页面在传输的过程中,被网络层进行内容「再加工」,常见有:注入js、iframe、篡改页面。

注入js

注入js的方式可以通过document.write或者直接改html代码片段等方式,给页面增加外链js,为了做到更难检测,有些运营商会捏造一个不存在的url地址,从而不被过滤或者检测。

案例1:运营商会用自己识别的ip或者域名做js网址,wap.zjtoolbar.10086.cn这类只有在浙江移动网络下才会被解析出来,同理ip也是

案例2:运营商很聪明,知道页面可以检测所有外链js的域名,比如:m.baidu.com我只允许m.baidu.com/static的外链js,其他js都会被记录反馈;为了不被检测出来,我遇见个case电信会访问一个不存在的地址,比如:m.baidu.com/static/abc.js,这个地址在运营商直接返回劫持的js代码,请求不会发到百度的服务器。

被放入iframe或者iframe其他页面

这类case比较少见,但是一些擦边球的网站或者没有内容的垃圾站会用这种方式,他们一般是通过热门关键词之类做SEO,打开网站实际去了广告之类没有任何实际内容,而页面却是内嵌了一个其他网站,我们要是识别出来不被内嵌就需要检测。

篡改页面内容

这类case很少见,一般是在页面底部增加js之外的div,然后展现一些非网站内容。

劫持检测方法

讲了常见的劫持手段有哪些,我们再来看看怎么识别上面提到的这些劫持。

上图是15年8月11日这天百度某页面的劫持情况,那天数据还算不错,之前浙江移动网络劫持率高达40%+,多数劫持来自zjtoolbar.10086.cn这个域名,就是移动的流量提示(还专门启用个域名zjtoolbar,浙江toolbar)。。。

为什么我们的业务适合用Node?

本文从业务场景来谈谈为什么选择Node,以及前端写后端代码需要补足的短板。

这些日子一直在做Node方面的尝试,或多或少会收到周围的异样的目光甚至背后的质疑,于是促使我好好思考为什么我在做Node。网上搜下「为什么要用Node」,找到的文章多数是介绍Node多么多么牛逼,无非是从Node本身特性来说,比如:并发、事件驱动、非阻塞I/O、单线程、流、社区生态……诸如此类,很少谈业务场景。
我是「实用主义」者,说过:脱离业务场景谈架构都是耍流氓。因为个人是从一线业务做起的,经过几年对业务的思考,我觉得可以从业务场景来说说为什么我们的业务更适合用Node。

从业务场景说起

现在我们的业务模块化越来越普遍,很少有业务比较纯粹只有链接一个数据库就可以搞定,往往前台业务后面会有N多的API服务做支撑。比如:下面两种情况在我们实际开发中经常遇见:

  1. 某个页面需要的数据来自两个以上接口,而两个接口来自不同的团队/部门,比如:用户信息来自账号部门,而UGC数据来自业务部门
  2. 某个页面存在接口依赖,需要先调用接口A,然后根据接口A数据调取接口B,比如:个性化推荐,往往需要根据某些维度请求推荐系统拿到推荐数据的ID,至于内容,需要拿ID根据页面需要去获取具体元数据

上面两种情况,站在后台开发的角度来看,我们业务模块要分开要独立,而站在前端的角度来看,这些数据都是一个页面需要的,前端希望是一个接口给我返回。这是一个开始。。

当然后台开发,比如PHP也有并发请求的解决方案,好(上)心的后台工程师,会帮助在后台统一合并请求处理成一份数据或者接口,然后扔给页面使用。比如在实际开发中,我们的前端会写(并且维护)一个Template.class.php(我敢说我们80%的后台工程师都没看过这个代码。。),在View层使用,然后在Action当中将数据传给View层做渲染,下面的代码:

$this->render('xxx/xx.tpl', $tplData);

这样增加的沟通成本,降低了开发效率。为了一个页面,需要前端根据页面想要的数据,和后台沟通页面的数据格式,然后后台工程师找他们后面的API模块要数据、处理数据。这个过程中会有一些「灰色地带」,不好明确谁做更合适,完全靠自觉。

往往开发的时候会想各种方法来解耦,比如:引入后台模板(smarty之类),然后约定数据格式,前端根据数据格式来写Mock接口,写后台模板的前端就叫「大前端」;再Low一点的团队,会采取前端做好页面扔给后台工程师「套页面」,比如:PHP代码写HTML,各种<?php echo xxx;?>,代码很不友好,后台工程师幸福感也急剧下降。

还有一种做法是,干脆后台沦为「代理服务器」,收到请求我转给后面的API,拿到数据我返回给前端页面,做成可以「跨域」的接口,所以就成了好多webapp。

另外,站在后台工程师的个人发展来看,可能他们觉得:这些「包接口」的重复性工作,跟自己的晋升和技术发展又有毛线关系呢?

说道这里,肯定有人心里在嘀咕:这是你们大公司才有的问题,我们小公司不会有这样的问题!那我下面再从技术方面来说。

用Node抓站(一):怎么写出自己满意的代码

如果只写怎么抓取网页,肯定会被吐槽太水,满足不了读者的逼格要求,所以本文会通过不断的审视代码,做到令自己满意(撸码也要不断迸发新想法!

本文目标:抓取什么值得买网站国内优惠的最新商品,并且作为对象输出出来,方便后续入库等操作

抓取常用到的npm模块

本文就介绍两个:requestcheerio,另外lodash是个工具库,不做介绍,后面篇幅会继续介绍其他用到的npm库。

  • request:是一个http请求库,封装了很多常用的配置,而且也有promise版本(还有next版本。
  • cheerio:是一个类似jQuery的库,可以将html String转成类似jQ的对象,增加jQ的操作方法(实际是htmlparser2

request 示例

var request = require('request');
request('http://www.smzdm.com/youhui/', (err, req)=>{
  if(!err){
    console.log(Object.keys(req))
  }
})

通过上面的代码就看到req实际是个response对象,包括headersstatusCodebody 等,我们用body就是网站的html内容

Hybrid APP开发:JSSDK

拖稿了好久的「Hybrid APP开发系列」又更新了~
今天继续写JSSDK

为什么会有JSSDK

我之前文章介绍了通过 JSBridge 实现页面和NA的相互调用,并且介绍了模板本地包的开发和后台维护系统。今天介绍的是JSSDK,通过 JSSDK 可以实现:

  1. 抹平JSBridge的平台实现差异
  2. 对齐端能力,内部消化版本差异
  3. sdk封装后的代码更加符合前端习惯
  4. 权限控制、鉴权、对外开放,实现生态建设

关于sdk的代码级别的设计,可以参考文章:《JSSDK设计指南》

如果做过微信页面开发的,应该都知道wx.js,这就是微信的JSSDK,在微信内需要调用微信的端能力就需要引入这个js。

JSSDK的设计

JSSDK的设计包括两部分:

  1. 随着每个NA客户端版本内置的js,称为:inject.js,他的主要作用是封装JSBridge逻辑,通过随版更新实现减少端能力的版本分裂,降低整个sdk的代码复杂性。inject.js是一段js代码,当客户端加载一个页面的时候,由客户端在适当的时机注入到webview内执行,执行后的代码就会有给webview增加js方法,例如微信的_WeixinJSBridge,类比chrome开发插件当中的content_scripts,可以在document_startdocument_end等时机进行执行。
  2. 云端JS,即实际暴漏给开发者使用的js,称为:jssdk.js,这个是真正开发者使用的sdk文件,通过script外链引入,例如wx.js,这个js文件通过和inject.js进行交换,完成端能力的调用、鉴权和客户端事件监听等操作

记一次Vue项目的重构

上周没有更新原创技术文章,原因是忙着重构一个新接手的项目,此项目因为项目技术负责人离职,虽然投入人力持续增多,前端达到4人,后端3人,但因为新参与的童鞋对代码结构和业务的理解,导致项目开发了一个多月,还有一堆问题,达不到上线要求,接手项目之后,对项目业务场景和代码进行简单的了梳理,决定重构。重构不是个人冲动,而是的的确确存在各种大大小小的问题:

  1. 接口太碎。项目本身按照vue组件化开发,但是页面每个组件独自请求自己的数据,比如:首页由轮播图、各种列表和用户信息展现组成,导致首页从上到下7~8个模块,每个模块都各自发自己的请求,访问首页需要同时发出8个ajax请求
  2. vuex的store太乱,大家按照组件去开发,各自跟后端兑接口、联调,但是没有人来统筹安排,导致大量重复工作,而且存在接口重复开发问题;接口多了store的命名就太乱,action和mutation中的业务逻辑代码太多,而不同页面需要不同的数据格式,则导致:1. 或者在mutation当中对数据进行重新整理,2. 或者新开个接口,这样就造成接口越来越多,mutation部分代码越来越重。
  3. 一开始设计或者沟通有误。比如:用户信息相关的接口,需要传入用户id(uid),而不是通过登录cookie从passport获取;第三方接口需要用户信息,竟然请求的时候将cookie发给对方(幸好cookie是http only的,没有调通被我及时发现)
  4. 重复代码太多,抽象能力太差。一份代码在多个地方复制,导致代码改来改去最后都不知道哪里改了哪里没改
  5. 命名太乱,包括url、方法名之类,还有错别字,getAdcontent(用户地址信息),getmaildetail(用户地址信息)
  6. 研发人员缺乏全局意识,只管自己的代码,而不关心整个流程。由于前后投入人较多,没人对整个项目有把控,只能面向自己的需求编程。比如:积分获取页面,获取成功之后,联调成功,但是实际在积分获取列表页面却没有相关的记录信息;在比如:任何用户都可以领走别人的奖品,原因后端没做奖品是否是当前登录用户获取的校验
  7. 问题定位能力不够,遇见问题一调就是半天,找不到根本问题

介绍下项目背景:

此项目是一个积分项目,一些页面需要用户登录,页面主要包括:首页、任务+列表、商品+列表、个人信息和记录以及其他类(说明和规则等)
项目用Vue+yog2编写,ajax请求部分使用vue resource