Round 1
前些天,有个无聊的哥们,给我发了这个小游戏,要跟我比眼力,说他的最好成绩是 22 次:
http://h5.gaoshouyou.com/game/44.html
(在众多凤姐中找到唯一的蔡依林,总共时长为 1 分钟,看能找到多少次)
这只是第 4 关,到后面十几关的时候会出现丧心病狂的 80 个凤姐和 1 个蔡依林。
嗯,这种瞎狗眼的小游戏能难得倒我吗?
先正常开始玩游戏,再在游戏页面上右键点击『检查』,然后就可以发现:
很明显可以看出 2.png 只有 1 个,其他都是 1.png,这就好办了,我们用 Selenium 来写个自动化脚本。
思路很简单:循环找到 id 为 box 的 div,然后找 box 里面所有的 span;然后再对这些 span 做一个循环,找到 style 属性里有 "2.png" 的 span,直接点击,如下:
搞定。来跑一下:
额,一开始连图都没加载完就点击了...秀下最后的成绩:
看到我的 96 次,该哥们已吐血 ...
Round 2
第二天这无聊哥们又来找我比手速:
http://h5.gaoshouyou.com/game/34.html
这个游戏非常简单,就是点击一个个逐渐出现的黑色块。
打开开发者工具后,发现这是个典型的 HTML5 canvas:
这就没那么简单了,因为我们没有 webelement 来操作,只能通过计算机视觉找到黑块才能执行点击。我们分解下步骤:
将 canvas 转换成图片;
找出图片中的黑块;
执行点击。
首先我们来解决下如何将 canvas 转换成图片的问题。
稍微接触过 HTML5 canvas 的同学可能都知道,在 Javascript 里,canvas 对象有个 toDataURL() 函数可以将当前 canvas 的内容转成 base64 格式。而 Selenium webdriver 提供了 execute_script 方法可以在页面上直接执行 js 脚本。
get_base64_script = '''var canvas = document.getElementById("linkScreen"); return canvas.toDataURL().substring(22);'''
img_base64 = driver.execute_script(get_base64_script)
这样我们就拿到了 canvas 的 base64 格式图片,然后我们将 base64 解码然后用 numpy 和 cv2 来转换成 numpy array 格式的图片:
img_decode = base64.b64decode(img_base64)
img_array = np.fromstring(img_decode, np.uint8)
img = cv2.imdecode(img_array, cv2.COLOR_BGR2GRAY)
效果如下:
接着使用 cv2 进行图片处理,先使用 threshold 函数将图片转成黑白,再使用 morphologyEx 函数尽可能去掉一些噪点:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
kernel = np.ones((10,10), np.uint8)
ret, thresh = cv2.threshold(img_gray, 60, 255, 0)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
到这里本来是想用 cv2 的轮廓检测函数 (findContours 以及 boundingRect 函数) 找出长方形,比如这是我在另外一个小游戏里测试的结果,能准确地找到方块,如下图绿色框中的轮廓:
但是后来发现我想复杂了。
其实整个 canvas 分成了 4 行 4 列,在游戏的经典模式里,每点击完一次黑块,画面会依次下降然后定格,所以我们需要点击的必然在都是在第 3 行,我们只要检查第 3 行就可以了。
这里我就偷懒了,取出第 3 行每个格子中间的 40 x 40 个像素,判断是否都为黑色 (即 0) 即可,使用 numpy.where 函数非常高效:
def analy(img):
x = None
centers = [(x, 300) for x in range(40, 320, 80)]
for center in centers:
x, y = center
block = img[280:320,x-20:x+20]
rows, _ = np.where(block==0)
if rows.shape[0] == 1600:
return x
可以看到模拟点击的点 (绿色圆圈) 是正确的:
剩下的问题就是用 Selenium 如何点击 canvas 的问题了。
Selenium 里有 ActionChains 类,可以记录一系列的鼠标、键盘动作,如:
ActionChains(driver).move_to_element_with_offset(canvas, 120, 300).click().perform()
上面的代码就是移动到 canvas,然后在 canvas 的 (x=120, y=300) 位置处点击。这刚好是我们所需要的!
不过这里有个坑,使用 Chrome 的 webdriver 大多数情况下没有反应,或者是点击的位置不对,折腾了好久始终解决不了。
后来索性换了 Firefox,却能稳定点击。看了下 Selenium 源码:
发现 Firefox 和 Chrome 的 webdriver.w3c 是不同的,Firefox 是 True,而 Chrome 为 False,应该就是这个原因了。
好了,将所有代码串起来,执行,最终效果如下:
可以看到鼠标在左上方一直没有动的。
不过呢,程序这么点虽然不会出错,但是速度上来说,还是比人类手速略慢的,其实还有改进的地方,比如一次检测三行。不过我就懒得搞了。
最后无聊哥们以 76:70 扳回一城。
Round 3
http://www.4399.com/flash/197802_1.htm
这是个用 Unity 来实现的 H5 游戏:
游戏内容是控制小兔子在水面上跳到各种物品上,鼠标左键跳一格,而右键是跳两格。
首先尝试使用上文中的 canvas 截取图片的方式,但是只能得到一个全黑的图片,原因不明。
最后只好将整个浏览器进行截图:
def get_screenshot():
global driver
img_base64 = driver.get_screenshot_as_base64()
img_decode = base64.b64decode(img_base64)
img_array = np.fromstring(img_decode, np.uint8)
img = cv2.imdecode(img_array, cv2.COLOR_BGR2GRAY)
return img
接下来我们来分析下游戏画面:
画面上要跳的格子是固定的:
第一个格子:img[800:1000,1380:1580,:]
第二个格子:img[800:1000,1580:1780,:]
第三个格子:img[800:1000,1780:1980,:]
原则上我们只需要检测第一个格子是什么就行了。
接下来我们需要分辨出来游戏中出现的六种物品:
场景中随着游戏的不断进行,背景的亮度也会有所变化,比如变亮或者变暗等,所以我们采用上面的阈值函数是不靠谱的,需要用到自适应阈值函数 (Adaptive Thresholding),如下:
因为转成阈值图片后,每种物品的形状不一样,所以黑色像素点的数目肯定是不一样的,我们可以通过这点来判断物品:
def count_adaptive_pixels(img):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_blur = cv2.medianBlur(img_gray, 5)
thresh = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
rows, _ = np.where(thresh != 255)
return rows.shape[0]
接下来我们定义一个没有重叠的可信区间,只要 count_adaptive_pixels()
函数返回的值在某个区间内,就是某个物品了:
ITEMS = {
'empty': (0, 300),
'stick': (2800, 3200),
'rotten_stick': (3500, 3800),
'rock': (4100, 4400),
'turtle': (4700, 5300),
'crocodile': (5800, 6300)
}
最后是 Selenium 模拟鼠标点击的动作了,上面我们知道 ActionChains 可以实现点击 (click),而右键则是 context_click
:
def jump(ty):
global driver
global canvas
if ty == 1:
ActionChains(driver).click(canvas).perform()
else:
ActionChains(driver).context_click(canvas).perform()
def take_action(item):
jump(2) if item in ['empty', 'rotten_stick'] else jump(1)
操作函数非常简单:
k = 0
while k < 100:
try:
img = get_screenshot()
item = predict_item_in_image(img)
print(item)
take_action(item)
except:
pass
k += 1
time.sleep(0.5)
最后效果:
这里最后的 time.sleep(0.5) 是为了等小兔子跳完,画面固定之后才开始截图判断,否则比较容易误判。但是这样会有更严重的问题,比如跳到乌龟或者鳄鱼头上时,需要及时跳走,不然就像上面那样落水失败。
小兔子卒...
Final Round
最终回,彩蛋时间。
http://h5.gaoshouyou.com/game/64.html
直男的噩梦!
各位是不是准备好 OpenCV 和 Selenium 跃跃欲试了,不过可以告诉大家一个有点让人失望的消息,这小游戏把答案都写在 js 里面了:
以上就是本文的所有内容。
scripts
脚本已上传到 gist:
https://gist.github.com/jackhuntcn/e588ef2022f46f889aa5a6313a3fdec0 https://gist.github.com/jackhuntcn/fcf9d8eb2e5fa0b21b99970bbaceac1d https://gist.github.com/jackhuntcn/9f6091fc258787a488c0d2cdb25a534f
为了行文方便,我虚构了一个无聊哥们。没错,这无聊哥们就是我本人 ...
不定期更新 入门级及不靠谱的 数据抓取、数据分析、深度学习以及其他有趣脚本的原创文章,欢迎长按下面的二维码关注?