写在前面

Python反反爬系列

  1. JS混淆---源码乱码
  2. JS混淆---动态Cookie
  3. 访问逻辑---推心置腹
  4. CSS加密---样式干扰
  5. JS混淆---回溯
  6. 动态字体---随风漂移

解题代码

题目


题目网址,点我去刷题
视频讲解Python识别生僻字验证码,那不有手就行?

因为这道题目考的是验证码识别,所以想要看到页面的数据,必须通过验证码

通过验证码、抓取出现的数字,将5页全部数字中出现频率最高的数字(即:众数)填入答案

分析网页

老规矩,我们还是首先打开刷题网站,接着打开谷歌调试工具
查看【XHR】里面的内容

我们已经做了很多道的猿人学题目,知道他们返回数据的链接一般都是这样的URL

http://match.yuanrenxue.com/api/match/8 (URL中的数字就代表返回第几题的数据)

但是,在上面的图片中,我们可以发现这条获取数据的链接,出现了400错误(Bad Request)
出现这种情况,其实也很好理解,因为这道题目需要通过验证码呀

如果我们没猜错的话,8_verify这条链接应该就是验证码的图片

通过上面的图片,就可以验证我们的猜想,这条链接会去获取一个通过base64编码的图片


现在,我们通过验证码后,再去看看【XHR】里面的内容

不出我们所料,【XHR】出现了网页中的数据
我们再来分析一下这条链接,到底它是怎么拿到数据的呢?


通过分析请求成功的URL

http://match.yuanrenxue.com/api/match/8?page=1&answer=166%7C725%7C117%7C154%7C

我们可以知道要想请求成功,在/api/match/8,必须携带两个参数

  • page:不用多解释吧,就是页码
  • answer:单词的意思是答案,但是后面的值是怎么来的呢?

分析 answer

我们将answer的值单独取出来

166%7C725%7C117%7C154%7C

%7C,这个大家应该很熟悉才对,就是将一个字符通过url编码得到的结果,而这个字符就是【 | 】
通过,谷歌的调试工具我们也可以知道,这一结果

Query String Parameters,这个就是查询字符串参数,也就是查询url请求所携带的参数,和我们上面提到的两个参数相对应
%7C的问题解决了,但是answer的值里面的四个数字是什么东东呢?


我们现在回想一下获取到数据的整个思路

  • 要想正常获取到网页数据,必须先通过验证码
  • 而通过验证码,就要按照提示,依次点击四张图片
  • 但是猿人学的服务器,是怎么知道我们点击的哪四张图片呢
  • 我们通过上面的分析知道,请求成功的URL中是携带了四个数字的,这四个数字是不是就对应着图片的位置信息呢

带着这样疑问,我们分析一下验证码图片

通过查看网页中的HTML代码,可以发现验证码是一个300*300的图片,也是一个300*300的div盒子
而这个300*300的div盒子中,还包含了900个10*10的小号div盒子

我们现在,做个小小的测试,依次点击第一排的四个小号div盒子,看看请求的URL会发生什么

请求失败很正常,毕竟没有按照提示去点击,而请求的参数answer,告诉了我们很重要的信息
这个测试我们是依次点击的第一排的盒子,answer的值是:0|1|2|3|

通过这个测试,我想大家就明白了,猿人学的服务器是通过什么方式知道我们点击的正确图片

  • 首先将一个300*300的图片分割成900个10*10的小图片
  • 而这900张小图片,按照排列的顺序,依次对应着坐标位置
  • 300*300的图片可以看作是一个九宫格,每一个格子里面就有一个生僻字
  • 每一个生僻字,就对应着100张10*10的小图片
  • 所以,猿人学的服务器就可以通过预先设定好的图片位置信息,知道我们点击的是哪一个字,接着按照给出的提示,验证点击的文字是否正确,最后返回结果

那么,是不是只要图片位置信息对应正确就能正常获取到数据呢?
我们就拿刚才请求成功的URL试一试

http://match.yuanrenxue.com/api/match/8?page=1&answer=166%7C725%7C117%7C154%7C

大家可以点击这条URL试一试
为什么会请求失败呢?按道理来讲图片位置信息也是正确的呀
这个时候,我们就可以大胆猜测一下,会不会是session的问题


我们查看8_verify返回验证码图片的链接,可以发现确实是设置session的
其实解决这个问题也很简单,因为这道题目考察的并不是session的问题
在后面写爬虫代码的时候,只需要预先设置一下session就可以解决啦


我们已经弄清楚了网页中获取数据的整个逻辑

  1. 访问题目网站,返回一个生僻字验证码
  2. 根据提示依次点击正确的生僻字,方可获取到正确数据
  3. 整张图片包含九个生僻字,每个生僻字中又包含100张被分割的小图片
  4. 而这100张小图片可以提供位置信息,让猿人学的服务器进行判断,返回结果
  5. 验证成功,返回网页数据

现在,我们就可以开始写爬虫代码,但是会遇到一个棘手的问题,生僻字验证码如何解决

解决生僻字验证码

在解决生僻字验证码之前,我们需要先了解一下图片的基础知识,图片即三维矩阵

图片即三维矩阵

学过线性代数的同学对矩阵并不陌生。一般来说,图像是一个标准的矩形,有着宽度(width)和高度(height)。而矩阵有着行(row)和列(column),矩阵的操作在数学和计算机中的处理都很常见且成熟,于是很自然的就把图像作为一个矩阵,把对图像的操作转换成对矩阵的操作,实际上所有的图像处理工具都是这么做的

直观的来说,对于一个有m*n个像素的图片,表示为三维矩阵就是(m, n, 3),其中m表示高,n表示宽,3表示该元素的RGB色彩值。也就是下面这个矩阵:

每个蓝色的框代表的就是一个像素,该像素的值为rgb色彩值,如可以是[70 69 64],该像素的R值为70,G值为69,B值为64

Python代码验证

下面我们就用Python代码进行验证一下
我们使用第三方库cv2,打开一张这样的图片


我们可以很清楚的看见,使用cv2打开后的图片就是一个三维矩阵,每一组数据就代表着相对应的像素点的RGB值
我们还可以用图片中的第一个像素点,与cv2读出来的RGB值进行比较


处理污染验证码

那么,我们了解图片即三维矩阵这一基本知识后,有什么用呢?
我给你看张图片,你就明白了

由于题目中涉及到的验证码污染很严重,以至于用肉眼都很难观察出里面的文字
所以,我们可以通过获取验证码图片的三维矩阵数据,然后进行一系列的处理,得到一张很清晰的黑白文字图片


思路清晰,我们就可以开始写代码啦(需要掌握一定的Numpy知识)

移除背景图片

import cv2
import numpy as np

# cv2.imread读取图像
im = cv2.imread(r'image/web_img.png')
# img.shape可以获得图像的形状,返回值是一个包含行数,列数,通道数的元组 (100, 100, 3)
h, w = im.shape[0:2]
# 去掉黑椒点的图像
# np.all()函数用于判断整个数组中的元素的值是否全部满足条件,如果满足条件返回True,否则返回False
im[np.all(im == [0, 0, 0], axis=-1)] = (255, 255, 255) #将像素点为黑色的全部转换为白色的
# reshape:展平成n行3列的二维数组
# np.unique()该函数是去除数组中的重复数字,并进行排序之后输出
colors, counts = np.unique(np.array(im).reshape(-1, 3), axis=0, return_counts=True)
# 筛选出现次数在500~2200次的像素点
# 通过后面的操作就可以移除背景中的噪点
info_dict = {counts[i]: colors[i].tolist() for i, v in enumerate(counts) if 500 < int(v) < 2200}

# 移除了背景的图片
remove_background_rgbs = info_dict.values()
mask = np.zeros((h, w, 3), np.uint8) + 255 # 生成一个全是白色的图片
# 通过循环将不是噪点的像素,赋值给一个白色的图片,最后到达移除背景图片的效果
for rgb in remove_background_rgbs:
    mask[np.all(im == rgb, axis=-1)] = im[np.all(im == rgb, axis=-1)]
cv2.imshow("Image with background removed", mask)  # 移除了背景的图片
cv2.waitKey(0)

移除背景图片后,我们发现,图片中还有干扰信息
那就是图片中的线条,这些线条怎么去掉呢?

去掉线条


不知道,大家看了上面的图片有没有受到一些启发
观察上面的图片,我们可以发现,线条出现在字与字之间的间隔当中
那么,我们是不是可以通过寻找间隔当中的像素点,而去除线条呢?
理论存在,开始实践

# 去掉线条,全部像素黑白化
line_list = [] # 首先创建一个空列表,用来存放出现在间隔当中的像素点
# 两个for循环,遍历9000次
for y in range(h):
    for x in range(w): 
        tmp = mask[x, y].tolist()
        if tmp != [0, 0, 0]:
            if 110 < y < 120 or 210 < y < 220:
                line_list.append(tmp)
            if 100 < x < 110 or 200 < x < 210:
                line_list.append(tmp)
remove_line_rgbs = np.unique(np.array(line_list).reshape(-1, 3), axis=0)
for rgb in remove_line_rgbs:
    mask[np.all(mask == rgb, axis=-1)] = [255, 255, 255]
# np.any()函数用于判断整个数组中的元素至少有一个满足条件就返回True,否则返回False。
mask[np.any(mask != [255, 255, 255], axis=-1)] = [0, 0, 0]
cv2.imshow("Image with lines removed", mask)  # 移除了线条的图片
cv2.waitKey(0)

腐蚀图片

我们可以发现,处理后的图片,颜色很淡
可以通过腐蚀图片的方式将图片颜色加深
(腐蚀在图像处理中是专业术语,在这里我们可以理解成加深图片颜色)

# 腐蚀
# 卷积核涉及到python形态学处理的知识,感兴趣的可以自行百度
# 生成一个2行三列数值全为1的二维数字,作为腐蚀操作中的卷积核
kernel = np.ones((2, 3), 'uint8')
# iterations 迭代的次数,也就是进行多少次腐蚀操作
erode_img = cv2.erode(mask, kernel,iterations=2)
cv2.imshow('Eroded Image', erode_img)
# cv2.imwrite('deal.png',erode_img) 这行代码可以保存处理的图片
# cv2.waitKey()等待键盘输入,为毫秒级
# cv2.waitKey()防止图片一闪而过
cv2.waitKey(0)


经过上面的一系列操作,我们就可以得到九个清晰可见的文字

使用OCR进行文字识别

文字的问题已经解决了,那么我们就可以直接调用腾讯的OCRapi进行文字识别呀
的确,我最开始也是这么做的,但是识别效果并不是很理想
识别几次才成功一次,而且刷题这个网页,请求一个新的页码,还是需要继续点击验证码才能呈现数据

所以说,如果调用api来识别的话,效率真的不是很高,毕竟这是一些生僻字

手动识别

腾讯的api识别不太行,我就陷入了沉思中,要不要去找一个识别效果更好的api呢?
这个时候,我就非常苦恼,揉了一下眼睛

woc!我为什么要去调用api呢?我自己不是长着眼睛嘛 我完全可以自己手动识别呀!

因为我们知道,猿人学的服务器是通过文字的位置进行判断,是否验证成功
那么我们就可以创建一个字典,映射文字与位置的关系

click_dict = {
        '1':126,'2':136,'3':146,
        '4':426,'5':466,'6':477,
        '7':726,'8':737,'9':776
    }

接着,通过input()函数,输入对应的值,不就可以拿到数据了嘛

解出答案

理论存在,实践开始

# @BY     :Java_S
# @Time   :2021/1/15 18:00
# @Slogan :够坚定够努力大门自然会有人敲,别怕没人赏识就像三十岁的梵高

import re
import cv2
import requests
import base64
import numpy as np

def erode_image(img):
    # cv2.imread读取图像
    im = cv2.imread(img)
    # img.shape可以获得图像的形状,返回值是一个包含行数,列数,通道数的元组 (100, 100, 3)
    h, w = im.shape[0:2]
    # 去掉黑椒点的图像
    # np.all()函数用于判断整个数组中的元素的值是否全部满足条件,如果满足条件返回True,否则返回False
    im[np.all(im == [0, 0, 0], axis=-1)] = (255, 255, 255)
    # reshape:展平成n行3列的二维数组
    # np.unique()该函数是去除数组中的重复数字,并进行排序之后输出
    colors, counts = np.unique(np.array(im).reshape(-1, 3), axis=0, return_counts=True)
    # 筛选出现次数在500~2200次的像素点
    # 通过后面的操作就可以移除背景中的噪点
    info_dict = {counts[i]: colors[i].tolist() for i, v in enumerate(counts) if 500 < int(v) < 2200}

    # 移除了背景的图片
    remove_background_rgbs = info_dict.values()
    mask = np.zeros((h, w, 3), np.uint8) + 255# 生成一个全是白色的图片
    # 通过循环将不是噪点的像素,赋值给一个白色的图片,最后到达移除背景图片的效果
    for rgb in remove_background_rgbs:
        mask[np.all(im == rgb, axis=-1)] = im[np.all(im == rgb, axis=-1)]
    # cv2.imshow("Image with background removed", mask)  # 移除了背景的图片

    # 去掉线条,全部像素黑白化
    line_list = []# 首先创建一个空列表,用来存放出现在间隔当中的像素点
    # 两个for循环,遍历9000次
    for y in range(h):
        for x in range(w):
            tmp = mask[x, y].tolist()
            if tmp != [0, 0, 0]:
                if 110 < y < 120 or 210 < y < 220:
                    line_list.append(tmp)
                if 100 < x < 110 or 200 < x < 210:
                    line_list.append(tmp)
    remove_line_rgbs = np.unique(np.array(line_list).reshape(-1, 3), axis=0)
    for rgb in remove_line_rgbs:
        mask[np.all(mask == rgb, axis=-1)] = [255, 255, 255]
    # np.any()函数用于判断整个数组中的元素至少有一个满足条件就返回True,否则返回False。
    mask[np.any(mask != [255, 255, 255], axis=-1)] = [0, 0, 0]
    # cv2.imshow("Image with lines removed", mask)  # 移除了线条的图片

    # 腐蚀
    # 卷积核涉及到python形态学处理的知识,感兴趣的可以自行百度
    # 生成一个2行三列数值全为1的二维数字,作为腐蚀操作中的卷积核
    kernel = np.ones((2, 3), 'uint8')
    erode_img = cv2.erode(mask, kernel, cv2.BORDER_REFLECT, iterations=2)
    cv2.imshow('Eroded Image', erode_img)
    cv2.waitKey(0)
    # cv2.destroyAllWindows()可以轻易删除任何我们建立的窗口,括号内输入想删除的窗口名
    cv2.destroyAllWindows()
    cv2.imwrite('image/deal.png', erode_img)
    return 'image/deal.png'

def get_verify(session):
    url = 'http://match.yuanrenxue.com/api/match/8_verify'
    response = session.get(url)

    html_str = response.json()['html']

    words_data = re.compile(r'<p>(.*?)</p>')
    words = words_data.findall(html_str)
    image_data = re.compile(r'src="(.*?)"')
    image_base64 = image_data.findall(html_str)[0].replace('data:image/jpeg;base64,','')
    with open('image/web_img.png', 'wb') as f:
        f.write(base64.b64decode(image_base64.encode()))
    print(words)
    return words

def get_page(page_num,index_list,session):
    url = 'http://match.yuanrenxue.com/api/match/8'
    click_dict = {
        '1':126,'2':136,'3':146,
        '4':426,'5':466,'6':477,
        '7':726,'8':737,'9':776
    }
    answer = '|'.join([str(click_dict[i]) for i in index_list])+'|'
    params = {
        'page':page_num,
        'answer':answer
    }
    response = session.get(url=url,params=params)
    try:
        value_list = [i['value'] for i in response.json()['data']]
        print(f'第{page_num}页的值为:{value_list}')
        return value_list
    except:
        print(f'第{page_num}页验证失败')
        return []



if __name__ == '__main__':
    session = requests.session()
    session.headers = {'User-Agent': 'yuanrenxue.project'}
    answer_list=[]

    for i in range(1,6):
        words = get_verify(session)
        erode_image(r'image/web_img.png')
        word_dict = input('请输入对应的坐标:')
        answer_list.extend(get_page(i, list(word_dict), session))

    print(f'出现次数最多的数字是:{max(set(answer_list), key=answer_list.count)}')

世界因代码而改变 Peace Out
最后修改:2021 年 01 月 25 日 08 : 15 PM
如果觉得我的文章对你有用,请随意赞赏