头脑王者的Python答题助手——从OCR文字识别到Fiddler抓包

2,141
原文链接: zhuanlan.zhihu.com

由于自己的专业不是学计算机的,所以只能利用课余时间自学Python。从上个暑假开始,写了大大小小的Python小程序,虽然大多数都比较简陋,但确实在每一次写代码的时候都能感受到编程的乐趣。

最近微信的小游戏占据了很多人的朋友圈,像跳一跳、头脑王者。自从上次体验了知乎大神写的跳一跳辅助程序之后,自己就有了做一个头脑王者答题助手的念头,一开始也是希望能够实现全自动答题,仿照跳一跳那个adb+Python的模式。看了网络上的一些教程,大多数教程都是比较简单的,没有完整代码,仅仅提供一个思路,那就自己动手丰衣足食吧~~

1.OCR文字识别

一开始接触到的就是OCR,经过百度谷歌之后,Python识别图片的文字需要pytesseract和PIL两个库,还需要识别引擎tesseract-ocr。前面两个库通过命令行安装就好了,然后tesseract可以在github下载,在安装的过程中记得要选择下载简体中文的语言包。安装完成之后,需要修改一下配置才能正常使用,找到你Python的安装路径,打开Python\Lib\site-packages\pytesseract\pytesseract.py,打开之后,作以下修改:

#tesseract_cmd = 'tesseract'
tesseract_cmd = 'C:/Program Files (x86)/Tesseract-OCR/tesseract.exe'

成功之后我们找一道题来试一下:

from PIL import Image
import pytesseract
question = pytesseract.image_to_string(Image.open('test.jpeg'),lang='chi_sim')
question = question.replace(' ','')  #去除空格
print(question)

OK~虽然有一些错别字,但是至少是识别出来了。看到成功识别出题目之后,我就兴奋地去写接下去的代码,可是后来用不同的题目来测试代码的时候,才发现识别率是真的低,除了一开始兵马俑那道题,其他题测试出来全是乱码。无奈只能去谷歌提高识别率的方法,网络上都是说黑白图片、高分辨率图片的识别率会高一点。后来就加了一段修改图片的代码,都是运用了PIL这个库。

修改图片模式:

from PIL import Image
img = img.convert('1')  

裁剪图片(从手机截图裁剪题目的部分):

from PIL import Image
p = Image.open(picname)
p_size = p.size   #获得图片尺寸
t = p.crop((0,int(p_size[1])*0.25,p_size[0],int(p_size[1])*0.45))  #截取题目部分的图片,后两个数字要比前两个大

但是修改之后,识别率并没有明显的变化,大多数图片识别出来还是乱码,在停滞了一段时间之后(主要还是因为学习期末很多事做- -),突然想到修改图片的背景颜色和字体的颜色,经过多次检验,发现黄底黑字的识别率最高,颜色改了之后,大多数的题目都能识别出来了。

图片修改颜色:

from PIL import Image
t2 = t1.convert('RGB')    #转rgb模式
    for i in range(0,t2.size[0]):
        for j in range(0,t2.size[1]):
            r = t2.getpixel((i,j))[0]
            g = t2.getpixel((i,j))[1]
            b = t2.getpixel((i,j))[2]
            if b>r and b>g and (r,g<100)and (b<210):
                r=255
                g=255
                b=154    #背景蓝色变黄
            elif (r,g,b>=180):
                b=0      #白色字变黑
                g=0
                r=0
            t2.putpixel((i,j), (r,g,b))

代码大概的思路是用ADB命令实时截取头脑王者的图片,然后处理图片,识别出题目和四个选项,用百度知道搜索题目,再用爬虫抓下答案,根据四个选项在答案中的出现次数,得出最佳选项。

完整代码:

from PIL import Image
import pytesseract
import requests
from bs4 import BeautifulSoup as BS
from urllib import parse
import datetime
import os

def open_pic(picname):
    p = Image.open(picname)
    p_size = p.size   #获得图片尺寸
    t = p.crop((0,int(p_size[1])*0.25,p_size[0],int(p_size[1])*0.45))  #截取题目部分的图片,后两个数字要比前两个大
    t.save('./first_change.png')
    t_size = t.size   #获得截取后的图片尺寸
    return t_size,p,t

def get_question(picsize,firstpic):
    new_x = 0
    new_y = 0
    t = firstpic
    for i in range(0,picsize[0]):
        last_pixel = t.getpixel((i,0))[2]
        for j in range(0,picsize[1]):
            now_pixel = t.getpixel((i,j))[2]
        if last_pixel < 190 and now_pixel > 200:
                new_x = i-50
                new_y = j-150
                break
        if new_x:
            break   #找到背景和文字刚刚转换的像素点
    #背景变黄色,字体变黑色
    t1 = t.crop((new_x,new_y,new_x+894,new_y+280))
    t2 = t1.convert('RGB')  #转rgb模式
    for i in range(0,t2.size[0]):
        for j in range(0,t2.size[1]):
            r = t2.getpixel((i,j))[0]
            g = t2.getpixel((i,j))[1]
            b = t2.getpixel((i,j))[2]
            if b>r and b>g and (r,g<100)and (b<210):
                r=255
                g=255
                b=154 #背景蓝色变黄
            elif (r,g,b>=180):
                b=0 #白色字变黑
                g=0
                r=0
            t2.putpixel((i,j), (r,g,b))
    t2.save("./second_change.png")
    question = pytesseract.image_to_string(Image.open('second_change.png'),lang='chi_sim')  #分析题目
    question = question.replace(' ','')   #去除空格
    question = question.replace('\n','')   #去除换行
    print(question)
    return  question

def get_choice(oldpic):
    p = oldpic
    p_size = p.size
    c = p.crop((250,int(p_size[1])*11/20,850,int(p_size[1])*8/9))  #截取选项部分的图片,后两个数字要比前两个大
    c1 = c.crop((0,0,600,691*1/6))
    c2 = c.crop((0,160,600,300))
    c3 = c.crop((0,360,600,500))
    c4 = c.crop((0,550,600,691))
    cc = [c1,c2,c3,c4]
    choices = []
    for h in cc:
        for i in range(0,h.size[0]):
            for j in range(0,h.size[1]):
                r = h.getpixel((i,j))[0]
                g = h.getpixel((i,j))[1]
                b = h.getpixel((i,j))[2]
                if b>r and b>g and (r,g<100)and (b<220):
                    r=0
                    g=0
                    b=0 #蓝色字变黑
                elif (r,g,b>=160):
                    b=154 #白色背景变黄
                    g=255
                    r=255
                h.putpixel((i,j), (r,g,b))
        h.save("./ana_choice.png")
        choice = pytesseract.image_to_string(Image.open("ana_choice.png"), lang='chi_sim')  # 分析选项
        choice = choice.replace(' ','')
        #解决选项中有英文大写字母0的识别错误
        if '0' in choice:
                choice=choice.replace('0','O')
        print (choice)
        choices.append(choice)
    return  choices

def search_answer(question,choices):
    ll = [0,10,20]
    answer = []
    for p in ll:
        b = parse.quote(question.encode('gbk')) #转gbk码
        url = 'https://zhidao.baidu.com/search?word=' + b + '&ie=gbk&site=-1&sites=0&date=0&pn=' + str(p)
        r = requests.get(url)
        r.encoding = 'gbk' #网址转gbk编码
        soup = BS(r.text, 'html.parser')
        want = soup.find('div', id='wgt-list')
        wants = want.find_all('dl', class_='dl')
        for i in wants:
            ans = i.find('dd', class_='dd answer').text
            answer.append(ans)
    choiceset = {}
    choiceset['A'] = choices[0]
    choiceset['B'] = choices[1]
    choiceset['C'] = choices[2]
    choiceset['D'] = choices[3]
    for i in choiceset:
        account = []
        for j in answer:
            if choiceset[i] in j:
                account.append(j)
        a = 0
        for k in account:
            a += 1
        print('选' + i + '的可能性是' + str('%.2f' % (a * 100 / 30)) + '%')

def main(filename):
    picsize = open_pic(filename)[0]
    oldpic = open_pic(filename)[1]
    firstpic = open_pic(filename)[2]
    question = get_question(picsize,firstpic)
    choices = get_choice(oldpic)
    search_answer(question,choices)

if __name__ == '__main__':
    start = datetime.datetime.now()
    your = input('准备好了按y:')
    if your == 'y':
        os.system('adb shell screencap -p /sdcard/auto.png')
        os.system('adb pull /sdcard/auto.png')
        img = Image.open('auto.png')
        img.convert('RGB')
        img.save('auto.png')
        main('auto.png')
    end = datetime.datetime.now()
    print ('本次一共花了'+str((end-start).seconds)+'秒')

尝试运行一下,发现运行时间太太太太长了,估计是图片识别会占用很长时间,每当我5个题目答完,第一题才刚刚分析出来,虽然过程中花了很多心思,但是这种效果肯定是没有实用性的,让人心酸。

2.Fiddler抓包

正打算放弃这个程序的时候,发现了Fiddler这个抓包工具,之前学爬虫的时候就听到过,但是那时候没认真研究。应用到这里刚刚好,通过Fiddler实时抓取头脑王者传输的数据,把数据保存下来给Python分析,接下来的事就简单得多了。

Fiddler手机抓包的教程网上有很多,重点是把传输的数据自动保存下来。使用Fiddler时最后设置成只看含有‘quiz’的url,不然会冒出很多无关的数据。

设置完之后玩一局游戏,软件中出现了五个新的数据,里面就包含了每一道题的信息。原来之前辛辛苦苦弄图片识别,现在这么容易就把题目和选项拿到手了。

接下来就是最重要的自动保存json数据,在软件中的‘FiddlerScript’--‘OnBeforeResponse’修改一下代码:

在原有的基础上加这段代码:

if(oSession.host == 'question.hortor.net'){
            oSession.utilDecodeResponse();   //Decoding HTTP request in case it's gzip
            //Saving full request object (Including HTTP headers)
            oSession.SaveResponse('C:\\Users\\XXXX\\Desktop\\data\\response.txt',true);
            //Saving just body
            oSession.SaveResponseBody('C:\\Users\\XXXX\\Desktop\\data\\responsebody.txt');
        }

有了数据文件,接下来的事就交给Python了,直接贴代码:

import json
import time
from urllib import parse
import requests
from bs4 import BeautifulSoup as BS

def get_appinf(filename):
    f = open(filename, 'r', encoding='utf-8')
    try:
        j = json.loads(f.read())
        #判断数据文件是否有题目和选项
        if 'quiz' in j['data'] and 'options' in j['data']:
            num = j['data']['num']
            quiz = j['data']['quiz']
            print(('第'+str(num)+'题:'+quiz).center(50,'*')+'\n')
            cho = j['data']['options']
        else:
            pass
        return quiz,cho
    except:
        pass
    f.close()

def search(question,choice):
    pagenum = [0,10,20]
    answer = []
    for i in pagenum:
        q = parse.quote(question.encode('gbk'))  # 转gbk码
        url = 'https://zhidao.baidu.com/search?word=' + q + '&ie=gbk&site=-1&sites=0&date=0&pn=' + str(i)
        requests.packages.urllib3.disable_warnings()  # 忽视网页安全性问题
        r = requests.get(url, verify=False)  # 不验证证书
        r.encoding = 'gbk'  # 网址转gbk编码
        soup = BS(r.text, 'html.parser')
        want = soup.find('div', id='wgt-list')
        wants = want.find_all('dl', class_='dl')
        for i in wants:
            ans = i.find('dd', class_='dd answer').text
            answer.append(ans)
    choiceset = {}
    choiceset['A'] = choice[0]
    choiceset['B'] = choice[1]
    choiceset['C'] = choice[2]
    choiceset['D'] = choice[3]
    #计算四个选项在爬取百度答案中的出现次数
    results = {}
    for i in choiceset:
        account = []
        for j in answer:
            if choiceset[i] in j:
                account.append(j)
        result = len(account)/30
        results[i] = result
        if i == 'D':
            print(('选' + i + '的可能性是:%.2f%%' % (result * 100 )).center(50)+'\n')
        else:
            print(('选' + i + '的可能性是:%.2f%%' % (result * 100 )).center(50))
        
    #选出数值最大元素的对应键
    bestchoice = max(results.items(), key=lambda x: x[1])[0]
    print (('此题最好选'+bestchoice).center(50,'-')+'\n\n\n')

def main():
    try:
        que,cho = get_appinf('C:/Users/XXXX/Desktop/data/responsebody.txt')  #修改成你自己的保存位置
        search(que,cho)
    except:
        pass

if __name__ == '__main__':
    while True:
        main()
        time.sleep(2)

这次的程序实际效果比之前的好多了,在手机上的题目出来之前,Fiddler就能抓取到数据并通过Python找到答案,但是问题也是很明显,稍微复杂一点的题目百度也搜索不出来,还有反向题目(‘不属于’、‘不包括’‘不是’)的识别率也不高,偶尔也会被答题大神吊打,但是拿来娱乐一下其实也足够了,毕竟头脑王者不同什么登顶大会,答对题没有奖金。程序出来之后,花了大半个小时上了王者。