这几天,一款名为“小猿口算”的App火遍全网。
小猿口算原本只是给小学生练习数学口算的软件,既可以帮助老师和家长检查一些简单的数学作业,学生也可以在软件中进行一些简单的比赛,大多数是数字比大小、或是简单的加减乘除口算。
不过,最近不知道从哪冒出来一帮“大朋友”们,以大学生为主力军,在小猿口算的竞技场PK中以降维打击的态势,给不少真正的小学生们带来了困扰。
这款App到底有多火?B站、抖音一些新奇的视频有几十万、几百万的播放暂且不提,我们单看App Store的排行榜:
软件的受众群体直接现象级的从练习数学的小学生转变成了被吸引而来的大人们。或许是上手难度极低(废话,毕竟是小学生的数学题...),再加上有一种“欺负小朋友”的恶趣味,这款软件直接成为了大学生们上课解压的新宠,打工人们消遣摸鱼的香饽饽。
因此,现在的排行榜上直接没有了XX小学XX的名字,全是带着各种恶搞头像、恶搞ID的大人们。其中不乏有“大一也是一年级”、“祖冲之”、“牛顿”、“收徒”这样另类幽默的名字,也有《火影忍者》玩家们带着强烈的好胜心和嘲讽感挂上的个人资料。
可就在软件爆火没几天后,突然有人发现,部分排行榜的前几名已经将做题速度提升到了0.几秒/题。
当大家还在拼手速、唤醒自己沉睡的口算记忆时,已经有人开辟了新赛道。他们带着各大高校的头衔,在排行榜上以反人类的速度摘金夺银。
显而易见,这些“高手”们肯定是借助了科技的力量了。随着排行榜上的数字越来越小,速度越来越快,人们开始戏称现在的口算排行榜是算法优化大赛。
那么今天,我们也来看看,这场小学生口算题的“华山论剑”中,各大门派都有些什么本领吧!
门派1:视觉识图
相信不少朋友在了解机器学习时,接触的第一个模型就是图像识别模型,识别数字等。
通过计算机视觉和OCR识别,可以很轻松的完成一个自动化答题系统。考虑到小猿口算是手机软件,我们可以用模拟器,或者投屏的方式实现PC端操作。
那么,使用到比较热门的图像处理库OpenCV、图像捕获库PyAutoGUI等,以比大小的题目为例,我们可以实现:
- 先设定好目标区域,抓取屏幕截图
- 用cv2将捕获的图像转换为灰度图,进行二值化处理,以提高OCR的准确性
- 使用一些OCR的库如pytesseract、PaddleOCR,对捕获的图像进行识别,把题目的数字找到
- 比较捕获的值,输出答案应该是大于还是小于
- 使用PyAutoGUI模拟鼠标的操作,直接在答题区域绘制大于号和小于号
这样一来,我们就实现了一个完全自动化的答题系统了!
门派2:特征识别
绘制大于号小于号的速度太慢了,怎么办?
我们应该反过来想想,系统是怎么实现绘制答案的判断的。毕竟软件的受众群体本身是小学生,他们使用手机的娴熟程度可不一定有大人那么高,画出来的符号很大一部分是歪歪扭扭的。
如果观察的仔细,那些脚本视频里,很多用户使用的答题是这种方式。
利用两个点的相对高低来表示“大于号”或“小于号”是一种简化的特征识别方法,相比于完整的符号,两个点的表示更为简洁且易于识别,特别适合小学生的手写输入。这种方法对手写的细微误差具有一定的容忍性,因为只需要关注点的相对位置,而不必完全依赖于精确的符号形状。
那么如果同样是自动化答题了,打两个点的速度也比绘制一整个符号要快啊!
那就好办了,直接预设大于号和小于号的坐标,画两个点就可以了。比如,可以这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import pyautogui import time def draw_point(x, y): """在指定坐标绘制一个点""" pyautogui.mouseDown(x, y) pyautogui.mouseUp() def draw_greater_than(origin_x, origin_y, size): """绘制大于号""" # 绘制第一个点 draw_point(origin_x, origin_y) # 绘制第二个点 draw_point(origin_x + size, origin_y + size) def draw_less_than(origin_x, origin_y, size): """绘制小于号""" # 绘制第一个点 draw_point(origin_x + size, origin_y) # 绘制第二个点 draw_point(origin_x, origin_y + size) def main(answer): """根据给定的答案绘制符号""" origin_x, origin_y = 250, 250 # 绘制区域坐标 size = 50 # 点间距 if answer == ">": draw_greater_than(origin_x, origin_y, size)# 绘制大于号 else: draw_less_than(origin_x, origin_y, size)# 绘制小于号 if __name__ == "__main__": main(answer) # 假定answer已经计算好了 |
门派3:代理抓包
有人说,识别还是不够快。
事实上更快的方法也是有的,抓包拦截。通过设置一个中间代理服务器(如 mitmproxy),所有发出的 HTTP 请求都会首先经过这个代理服务器。代理服务器会转发这些请求到目标服务器,并接收目标服务器返回的响应。
这样一来,我们就可以在比赛刚刚开始的时候,通过拦截,获取到一整场比赛的题目信息。
这样连识别都不用了,可以直接通过值进行比大小,输出答案打点即可。或者传个计算后的正确答案,效率还能更高。
最后,只需要参考前面的步骤,把下一场比赛按钮的坐标固定好,就能体会到开局秒杀的快感,直接进入下一把循环往复了。
门派4:抓包+改答案
考虑到上一步我们已经能拦截http请求了,如果再狠一点,连打点或算答案这一步都可以直接省去。通过正则表达式找到响应体中的特定字段,例如 "answer" 和 "answers",这些字段存储了服务器返回的正确答案。
那么我们可以使用正则表达式对 JSON 响应中的某些字段进行替换,比如修改 "answer" 和 "answers" 的字段值为固定的答案 "1"。比如这样:
1 2 | res = re.sub(r'"answer":"[^"]+"', '"answer":"1"', res) res = re.sub(r'"answers":\[[^\]]+\]', '"answers":["1"]', res) |
现在如果我们直接将修改后的内容重新赋值,传回服务器的数据中,所有问题的答案都变成了“1”。现在无论是比大小还是口算题,所有的答案都是最简单的数字1,全部提交就做完了,好家伙,连题目是什么都不看了,抓包后篡改答案再直接回传,试问还有比这更快的方法吗?
那么如果我们找到了版本的最终答案了,剩下的比拼大概也只剩下谁的网更快、谁离服务器更近了。
在这几天的爆火之后,小猿口算团队也是极具包容性的接住了这波热度,并发表了升级公告。呼吁大朋友们公平对决,积极推出新功能。
最后,再贴上一张耐人寻味的图:
关于自动化脚本实现的完整代码部分,网上已经有很多开源的代码,这里就不贴完整的代码了,可以参考这个项目:
https://github.com/cr4n5/XiaoYuanKouSuan
那么本期的内容就是这么多,我们下期再见!