头脑王者辅助

-这是 小明同学 2018年第 1 篇文章-

#废话慎读

随着微信小游戏的出现,最近各种外挂又开始盛行了,听到的最多的外挂就是《跳一跳》的外挂了,看似很简单的游戏,但是玩儿起来却一点都不简单,我也像很多人一样,当分数越高,就会越紧张,至今为止自己玩儿还从未突破过200分,是不是很菜,看到朋友圈那么多好几千分的,自己也不甘心,于是就各种倒腾外挂,开始刷分,刷排名。一个同学问我,很多人都玩儿到了好几千分,这些人也太无聊了,听到这句话,真是笑出了声,她居然不知道有外挂这种东西。外挂盛行之后,腾讯也各种打击外挂,后来就发现朋友圈的分数没有以前那么离谱的高了,在微信公开课上,张小龙,居然说自己最高分数是6000多分,而现场也是当众玩儿到了900多分,这该是一种什么心态,他说这款游戏本来是让大家放松玩儿的,但是大多数人却分数越高越紧张,很容易就死掉了。也有人说,用个外挂,刷那么高的分数,有什么意思,实际上的确没啥意思,其实有意思的并不是刷那么高的分,而是外挂本身,他是怎么实现自己玩那么高分儿的,原理是什么,该如何实现,作为一名程序员,这才是我们要玩儿的东西,分数多少,排名高低,都不重要,重要的是图一乐呗。

其实原理很简单,我并没有仔细研究过github上的外挂的源代码,也是因为懒的原因吧,我只是在我自己机器上跑了起来,当然也入了很多坑,因为当时用的是python版的,而自己并不是用python做开发的,所以很多东西都是现查现使用。最后也只是把安卓的搞定了,IOS更是一窍不通,各种安装,最后还是报错了,也没再去管他。

最近《冲顶大会》,让很多人拿钱拿到手软,我也一样,到现在一分钱也没得到,实在是太笨了,于是乎我就想,这要是有外挂就好了,那真是太爽了,但是这种题目有时间限制的,外挂其实还是比较困难的,因为响应的时间可能会超出题目规定的答题时间,但也只是起到一个辅助的作用,而且有的题目也并不是能直接找到答案的,比如:下面4个选项中哪个答案是错误的(),这种怎么去搜索,所以外挂这种东西啊玩玩儿就好了,还是要靠真材实料。

同样的在小程序里面也有款类似的游戏《头脑王者》,拿它当作实验,自己做一个外挂,但是对我来说并未那么简单,其实学习python也有一段时间了(不应该这么说,是从开始看python已经很久了,但至于学了多少了,就不多说了,实在是太懒了。。。),但还是想用它来实现这个小小的外挂,下面就开始切入正题。

我们先来分析下流程,其实跟《跳一跳》外挂很相似,首先我们还是要去截图,题目界面如下图所示:

截完图后,将题目的题干和答案取出,这里也就会用到图像识别了,然后去百度或其他搜索引擎去搜索题目,得到正确答案和选项比较,最终返回正确答案,通过坐标我们也可以知道每个选项的大体位置,然后再去模拟点击答案,完成操作。其实步骤就是这么简单。

开始->截图->识别文字->搜索题目并返回答案->模拟点击答案->结束

开发工具:PyCharm 2017.3

开发语言:Python 3.6

开发环境:MacOS

图像识别:腾讯优图AI 通用ORC识别模块 地址:http://open.youtu.qq.com/#/develop/api-ocr-general

调试工具:adb  关于adb在mac上如何使用请查看我简书上一片杂乱的文章,地址:https://www.jianshu.com/p/2a0cb004792d

测试手机:锤子坚果Pro

搜索引擎:百度 or 必应

OK,下面我们进入正题,我们创建一个名为MindKing(大概是头脑王者的英文名吧,请忽略)的项目,创建一个Python脚本MindKingExt.py,然后将优图的OCR识别模块引入到项目中,大概就是酱紫:

创建完成后,下面我们完成第一个任务,手机截屏

关于手机截屏并保存,熟悉安卓开发的朋友应该都很清楚,使用使用adb的相关命令即可,我们可以直接在控制台来测试,首先我们查看手机是否连接成功(手机需要打开开发者模式,并开启USB调试,这些应该都不用说了),使用 adb devices 如果连接成功是介样的,刚开始连接时会先运行daemon,然后显示此时的设备ID,这样也就代表连接成功了,下面我们看下直接在控制台中使用截屏命令时,是怎样的,命令:

adb shell screencap -p 


没错会出现一堆乱码,并且显示了截图的宽高等信息,下一步我们将此命令在我们的程序中执行,首先需要导入一个模块,它的名字叫做创建附加进程模块,subprocess,在这里我们使用的是它的直接处理管道的方法,叫做subprocess.Popen(),如何使用它执行截屏呢,代码如下:

 

[python] view plain copy
 
  1. # 第一个参数为命令行,安卓手机截屏命令;  
  2. # 第二个参数shell=True,在Windows下表示cmd.exe /c即在这里执行的是cmd命令;  
  3. # 第三个参数建立管道,这里通过将stdout重定向到subprocess.PIPE上来取得adb命令的输出  
  4. process = subprocess.Popen('adb shell screencap -p', shell = True, stdout = subprocess.PIPE)  

然后我们我们可以从process这个变量中取到截图的二进制数据,读取二进制数据:

[python] view plain copy
 
  1. # 读取二进制数据  
  2. screenshot = process.sdtout.read()  

这时我们可以直接将screenshot保存图片了,关于文件读写的操作就是IO编程的部分了,这里不过多解释了,

[python] view plain copy
 
  1. # 可直接保存至文件  
  2.     with open('screenshot.png','wb') as f:  
  3.         f.write(screenshot)  

我们是推荐这种写法的,旧方法中代码是比较长的,关于读写文件操作,你需要加try finally 以及要将对象进行Close操作,而使用with这种语法就不用那么麻烦了,这跟C# 中using 的语法是相似的。下面是我们手机的截图,其实在这里呢直接这样保存图片是不合适的。

因为这样势必会浪费时间,我们本来答题是有时间限制的,而直接保存图片会耗费不必要的时间,所以在这里我们可以把图片加载到内存中操作,保存至内存需要引入另一个模块BytesIO ,它支持的是二进制数据,如果要将字符串写入内存的话要使用StringIO,如何使用这个模块呢,首先创建一个变量指向这个对象,再把刚才读取到的二进制数据写入这个变量中,

 

[python] view plain copy
 
  1. # 将二进制读进内存中  
  2. imgbyte = BytesIO()  
  3. imgbyte.write(screenshot)  

到这一步,我们算是取到了直接截取的图片了,下面我们开始处理这张图片,通过上图,我们可以看出,在这张图中有诸多的干扰信息,那么这会在我们后面图像识别中造成麻烦,我们要的只是题干和选项,所以我们需要通过截图以及拼接将我们的图片重新整合,只保留题干和答案选项部门,提高识别度,那怎么去截取这张图片的有效信息呢,这一步操作与你手机的实际分辨率会有关系,我们需要定位题干和答案的位置,使用像素去进行定位,下面我们将这张图用画图工具打开,


上下两个红色框标注的内容就是我们想要的了,关于这个坐标的定位,在画图工具中可以直接显示,大家请根据自己的手机的分辨率自行调节,这里给出我使用的手机的大体位置坐标(实际上这个也不是我自己截取的,从别处看到的正好也是我手机的分辨率ganga),为了方便我们将截取图片相应的参数单独放到配置信息里面,

 

[python] view plain copy
 
  1. # 配置坐标信息,根据手机分辨率的不同调整,此坐标可适用于1080 X 1920  
  2. config = {  
  3.     '头脑王者':{  
  4.         'title': (80, 500, 1000, 880),  
  5.         'answer': (80, 960, 1000, 1720),  
  6.         'point': [  
  7.             (316, 993, 723, 1078),  
  8.             (316, 1174, 723, 1292),  
  9.             (316, 1366, 723, 1469),  
  10.             (316, 1570, 723, 1657)  
  11.         ]  
  12.     }  
  13. }  

title就是我们要的题干部分,answer就是答案的部分了,而下面的point是我们要点击的4个答案选项的大体坐标。切割以及拼接图片的代码如下,不详细说明了,很简单,但是有6步操作:

 

[python] view plain copy
 
  1. # 图片处理  
  2. img = Image.open(imgbyte)  
  3. # 切出题目,左上角,右下角的点  
  4. img_Prob = img.crop((config['头脑王者']['title']))  
  5. # 切出答案,左上角,右下角  
  6. img_Ans = img.crop((config['头脑王者']['answer']))  
  7. # 拼接  
  8. new_img = Image.new('RGBA', (920, 1140)) #创建一个新的画布,宽920,高1140  
  9. new_img.paste(img_Prob, (0, 0, 920, 380))  
  10. new_img.paste(img_Ans, (0, 380, 920, 1140))  

从内存中打开图片,切割题干部分,切割答案部分,创建一个新的Image对象,粘贴题干,粘贴答案,注意下这个坐标的含义,前两个为左上角的点的左边,后两个为右下角的点的坐标。另外操作图片我们还需要引入一个模块,就是Image模块,完成后我们再将新的对象保存至内存中:

 

[python] view plain copy
 
  1. # 内存对象  
  2. new_img_byte = BytesIO()  
  3. #保存为png格式至内存中  
  4. new_img.save(new_img_byte, 'png')  

下面我们将它保存成图片看效果:

看样子应该是达到了我们预期的效果,最后我们将这个对象返回 return new_img_byte 。继续下一步操作,识别图像。关于识别图像,我用的是腾讯的优图AI开放平台的通用OCR识别,关于这个的使用呢,大家可以直接去官网看官方文档,用法也都很简单,返回的数据也都是标准的json对象,处理起来也很方便,下面直接贴代码了:

[python] view plain copy
 
  1. # 这里使用的是腾讯的优图开放平台的图像识别SDK,该appid应该会有上弦,具体不知,如果不能使用了,请自行申请更换  
  2.     """ 以下为开放平台返回的识别文本,题目和答案根据此内容解析 
  3.     { 
  4.         "errorcode":0, 
  5.         "errormsg":"OK", 
  6.         "items": 
  7.                 [ 
  8.                     { 
  9.                         "itemstring":"手机", 
  10.                         "itemcoord":{"x" : 0, "y" : 1, "width" : 2, "height" : 3}, 
  11.                         "words": [{"character": "手", "confidence": 98.99}, {"character": "机", "confidence": 87.99}] 
  12.                     }, 
  13.                     { 
  14.                         "itemstring":"姓名", 
  15.                         "itemcoord":{"x" : 0, "y" : 1, "width" : 2, "height" : 3}, 
  16.                         "words": [{"character": "姓", "confidence": 98.99}, {"character": "名", "confidence": 87.99}] 
  17.                     } 
  18.                 ], 
  19.         "session_id":"xxxxxx" 
  20.     """  
  21.     appid = '10115709'  
  22.     secret_id = 'AKIDY0HNJ482FJI8mJcqpdIpCPQFwTs6d2kM'  
  23.     secret_key = 'fAVfdP1Rlur03vfifR0U5Y1Qwv2yiWHs'  
  24.     userid = 'myApp1' # 自行命名,以上三个参数均在开放平台申请  
  25.   
  26.     end_point = TencentYoutuyun.conf.API_YOUTU_END_POINT  # 优图开放平台  
  27.   
  28.     youtu = TencentYoutuyun.YouTu(appid, secret_id, secret_key, userid, end_point)  
  29.   
  30.     with open('screenshot.png', 'wb') as fileReader:  
  31.         fileReader.write(img.getvalue())  
  32.     # 在执行generalocr()方法时,由于request对象的问题,返回中文时乱码,需要手动再指定返回对象的编码格式:r.encoding='utf-8' 详见youtu,py脚本文件  
  33.     ocrinfo = youtu.generalocr('screenshot.png', 0)  
  34.     # print(ocrinfo['items'])  
  35.     return ocrinfo['items']  

优图的SDK中有个bug,但应该是requests模块的问题,当你直接用下载的SDK的时候,识别的文字还是无法正常显示中文,因为这个requests对象无法直接识别你的编码类型,所以要自己指定,修改下它的SDK,在youtu.py脚本中,generalocr方法中,添加一句代码:r.encoding='utf-8',指定为utf-8。关于优图的这个通用识别generalocr ,里面的参数说明是这样说的,如果第二个参数为0,则代表传入的是图像文件,如果是1,则是图片的url,所以在这里我最终还是将内存中图片保存成了实体文件,具体能不能直接读取内存中的文件,我并未深入研究,如果大家知道的话,请留言,谢谢!最终返回的只有我们想要的文字内容,即['items']的内容,在使用OCR识别的时候,注意它是一行一行的识别的,所以识别后的结果你会发现最后就是一个['itemstring']的列表,只分析这一部分就可以了。OK了,这一部分也so easy了。接下来,我们再分析返回后的结果,还是以这张图片为例,识别后的内容为:

 

[html] view plain copy
 
  1. {  
  2.     'errorcode': 0,  
  3.     'errormsg': 'OK',  
  4.     'items': [{  
  5.         'itemcoord': {  
  6.             'x': 109,  
  7.             'y': 219,  
  8.             'width': 705,  
  9.             'height': 53  
  10.         },  
  11.         'itemstring': '「中国工商银行」的英文缩写是?',  
  12.         'coords': [],  
  13.         'words': [{  
  14.             'character': '「',  
  15.             'confidence': 0.9919068813323975  
  16.         }, {  
  17.             'character': '中',  
  18.             'confidence': 0.9999942779541016  
  19.         }, {  
  20.             'character': '国',  
  21.             'confidence': 0.9999985694885254  
  22.         }, {  
  23.             'character': '工',  
  24.             'confidence': 0.9998493194580078  
  25.         }, {  
  26.             'character': '商',  
  27.             'confidence': 0.9999986886978149  
  28.         }, {  
  29.             'character': '银',  
  30.             'confidence': 0.999992847442627  
  31.         }, {  
  32.             'character': '行',  
  33.             'confidence': 0.9999927282333374  
  34.         }, {  
  35.             'character': '」',  
  36.             'confidence': 0.993008017539978  
  37.         }, {  
  38.             'character': '的',  
  39.             'confidence': 0.9999935626983643  
  40.         }, {  
  41.             'character': '英',  
  42.             'confidence': 0.9999959468841553  
  43.         }, {  
  44.             'character': '文',  
  45.             'confidence': 0.9999784231185913  
  46.         }, {  
  47.             'character': '缩',  
  48.             'confidence': 0.9995788931846619  
  49.         }, {  
  50.             'character': '写',  
  51.             'confidence': 0.9999926090240479  
  52.         }, {  
  53.             'character': '是',  
  54.             'confidence': 0.9999960660934448  
  55.         }, {  
  56.             'character': '?',  
  57.             'confidence': 0.9996994733810425  
  58.         }],  
  59.         'candword': []  
  60.     }, {  
  61.         'itemcoord': {  
  62.             'x': 397,  
  63.             'y': 436,  
  64.             'width': 126,  
  65.             'height': 47  
  66.         },  
  67.         'itemstring': 'ICCB',  
  68.         'coords': [],  
  69.         'words': [{  
  70.             'character': 'I',  
  71.             'confidence': 0.732905924320221  
  72.         }, {  
  73.             'character': 'C',  
  74.             'confidence': 0.9993401169776917  
  75.         }, {  
  76.             'character': 'C',  
  77.             'confidence': 0.9990763664245605  
  78.         }, {  
  79.             'character': 'B',  
  80.             'confidence': 0.9994719624519348  
  81.         }],  
  82.         'candword': []  
  83.     }, {  
  84.         'itemcoord': {  
  85.             'x': 398,  
  86.             'y': 627,  
  87.             'width': 125,  
  88.             'height': 45  
  89.         },  
  90.         'itemstring': 'ICBB',  
  91.         'coords': [],  
  92.         'words': [{  
  93.             'character': 'I',  
  94.             'confidence': 0.8364823460578918  
  95.         }, {  
  96.             'character': 'C',  
  97.             'confidence': 0.9991937279701233  
  98.         }, {  
  99.             'character': 'B',  
  100.             'confidence': 0.9999769926071167  
  101.         }, {  
  102.             'character': 'B',  
  103.             'confidence': 0.9999263286590576  
  104.         }],  
  105.         'candword': []  
  106.     }, {  
  107.         'itemcoord': {  
  108.             'x': 399,  
  109.             'y': 819,  
  110.             'width': 125,  
  111.             'height': 45  
  112.         },  
  113.         'itemstring': 'ICBC',  
  114.         'coords': [],  
  115.         'words': [{  
  116.             'character': 'I',  
  117.             'confidence': 0.8958392143249512  
  118.         }, {  
  119.             'character': 'C',  
  120.             'confidence': 0.9992052912712097  
  121.         }, {  
  122.             'character': 'B',  
  123.             'confidence': 0.9998654127120972  
  124.         }, {  
  125.             'character': 'C',  
  126.             'confidence': 0.9920558333396912  
  127.         }],  
  128.         'candword': []  
  129.     }, {  
  130.         'itemcoord': {  
  131.             'x': 397,  
  132.             'y': 1010,  
  133.             'width': 126,  
  134.             'height': 46  
  135.         },  
  136.         'itemstring': 'IBCB',  
  137.         'coords': [],  
  138.         'words': [{  
  139.             'character': 'I',  
  140.             'confidence': 0.6056796312332153  
  141.         }, {  
  142.             'character': 'B',  
  143.             'confidence': 0.9954431056976318  
  144.         }, {  
  145.             'character': 'C',  
  146.             'confidence': 0.9996951818466187  
  147.         }, {  
  148.             'character': 'B',  
  149.             'confidence': 0.9998865127563477  
  150.         }],  
  151.         'candword': []  
  152.     }],  
  153.     'session_id': '',  
  154.     'angle': 0.0  
  155. }  

在返回的结果中,它给出了每一个字母每一个字的可信任度(confidence) ,我们会发现在头脑王者的答题中,好像所有的题目都是4个选项,而且每一个选项都只占一行,而题目可能会有多行,所以我们就按照这个规律,将题干和答案去分割组合,可能并不准确,但这不重要,重要的是我们要实现这个东西。我们通过返回的数据分析:

 

[python] view plain copy
 
  1. # 分割题目和答案  
  2. answers = [x['itemstring'] for x in infos[-4:]] # 后4项为答案  
  3. question = ''.join([x['itemstring'] for x in infos[:-4]]) # 前面为题目  

在这里infos就是刚刚返回的ocrinfo['items']了,然后每一行都是一个itemstring,按照上面的规则,后面4个为选项,前面的为题干,这样得出结果:

[plain] view plain copy
 
  1. 「中国工商银行」的英文缩写是?  
  2. ['ICCB', 'ICBB', 'ICBC', 'IBCB']  

走到这一步后,下面就是要去搜索题目的答案了,可能第一反应就是,将题目放到百度或其他搜索引擎中直接查找答案,然后再去跟选项对比,但是我们知道搜索引擎返回的答案是千变万化的,比如一个日期可能是纯数字表示,也可能是汉字表示,这样我们就不知道该怎么对比了,所以在这里的处理方式是这样的,去搜索题目答案,然后从返回的结果中,查找每一个选项出现的次数,那么很容易想到的就是出现次数最多的就是正确答案咯。还是简单粗暴的贴代码吧:

 

[python] view plain copy
 
  1. # url = 'https://www.baidu.com/s' # 百度搜索  
  2.     url = 'https://www.bing.com/search' # 必应搜索  
  3.   
  4.     # 请求头文件  
  5.     headers = {  
  6.         'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7'  
  7.     }  
  8.     data = {  
  9.         # 'wq':question 百度搜索  
  10.         'q': question  # 必应搜索  
  11.     }  
  12.     response = requests.get(url,params=data,headers=headers)  
  13.     response.encoding = 'utf-8'  
  14.     # 返回请求的文本  
  15.     html = response.text  
  16.     # print(html)  
  17.     # 查找答案并按照答案出现的次数排序  
  18.     for i in range(len(answers)):  
  19.         answers[i] = (html.count(answers[i]),answers[i],i)  
  20.     answers.sort(reverse=True)  
  21.     # 打印输出题目和答案  
  22.     print(question)  
  23.     print(answers)  
  24.     # 返回正确答案,即第一个答案  
  25.     return answers[0]  

这里需要引入一个requests模块,去请求搜索引擎,查找我们要的信息。在该题中返回的结果为:

 

[html] view plain copy
 
  1. 「中国工商银行」的英文缩写是?  
  2. [(2, 'ICBC', 2), (0, 'ICCB', 0), (0, 'ICBB', 1), (0, 'IBCB', 3)]  

在返回的结果中,第一个参数为答案出现的次数,第二个为答案选项,第三个参数为选项的索引,这样我们就可以通过正确答案的索引,去实现最后一步,模拟点击选项了。根据配置信息config中,当我们知道选项的索引后,就可以知道选项所在的位置,然后去自动点击选项答案,点击手机使用的命令为:adb shell input swipe 坐标 延迟时间,注意需要引入os系统模块,以执行手机自己模拟点击,代码如下:

 

[python] view plain copy
 
  1. cmd = 'adb shell input swipe %s %s %s %s %s' %(  
  2.     point[0],  
  3.     point[1],  
  4.     point[0]+random.randint(0,3), # 右下角的坐标随机点击  
  5.     point[1]+random.randint(0,3), # 右下角的坐标随机点击  
  6.     200 # 延迟200ms  
  7. )  
  8. # 执行cmd命令,根据指定的坐标在手机上模拟点击  
  9. os.system(cmd)  

最后一步我们也完成了,这样一个简单的外挂就实现了,当然它也只是一个辅助性的工具,因为实测,耗费时间太长,总是让别人抢先了,但这并不重要,重要的是实现它的乐趣。

太晚了,睡觉了,晚安。

源码地址:https://github.com/Allen0910/MindKing   有问题请提Issue,谢谢!

最后再说一句,哎呀,CSDN的UI换了啊,看来是招到前端了!!!

原文地址:https://www.cnblogs.com/Allen0910/p/8345799.html