一、前言
第八题跟以往的几道混淆题有很大区别,不需要你去很费劲扣代码逻辑,找加密参数什么的,但是难就难在验证码图片太难识别了,花了一两个星期尝试看看有没比较好的解决方案,最终还是选择最稳妥的人工识别法😂,深度学习识别什么的以后再慢慢研究,不过解题过程中还是涨了不少姿势。
二、解析过程
2.1、点选逻辑分析
网站给出的目标是按顺序点击这四个字
通过点击观察请求后,发现其操作逻辑是先选择正确的文字坐标,点击完成后,携带正确的坐标信息请求数据页,最后得到正确的数值相应。
坐标信息附带在数据页api的请求参数上,就是这个answer,由四个数值构成的汉字对应坐标信息:
/api/match/8?page=1&answer=735%7C776%7C145%7C724%7C
在源代码里请求观察可以发现,四个坐标会通过“|”符号连接: 每次点击验证码中的一个位置,就会选中该汉字对应的div 索引:
整个验证码图片共计对应有900个div标签,对应九个字的话,就是每个字对应100个div,点击某个字体区域的任何一个div位置,都可以算成功选中这个字,经过测试,div是横向排列的。以下图为例,位置1234的位置索引分别是:“0|29|270|299|”。
所以实际上整个验证码实际上是一幅30*30像素的图片,最上面的三个字对应的所有div位置如下表所示,其余字的坐标也是按这个规则推算。
行数 | 左边的字 | 中间的字 | 右边的字 |
---|
第一行 | 0-9 | 10-19 | 20-29 | 第二行 | 30-39 | 40-49 | 50-59 | 第三行 | 60-69 | 70-79 | 80-89 | 第四行 | 90-99 | 100-109 | 110-119 | 第五行 | 120-129 | 130-139 | 140-149 | 第六行 | 150-159 | 160-169 | 170-179 | 第七行 | 180-189 | 190-199 | 200-209 | 第八行 | 210-219 | 220-229 | 230-239 | 第九行 | 240-249 | 250-259 | 260-269 | 第十行 | 270-279 | 280-289 | 290-299 |
了解了验证码里的字对应的div坐标转换规则以后,剩下的问题就是怎么确定每个字在图上的哪个区域,这也是这道题最大的难点。按常规直觉,通过ocr识别是我首先想到的方式。
2.2、验证码图像预处理
由于验证码图片的干扰程度太强,一开始我尝试直接用一些ocr平台(还是太天真)试试能不能识别出里面的字体来,结果果然是失败了,直接用没处理的原图来识别是不可能正常识别出字体的,所以还是要做一些预处理,把图片里面的背景、字体表面的干扰线条去掉。
由于我以前没怎么做过图像处理,所以在网上搜了半天,找到一个大佬的处理方案,看这里,并且做了一点小改动,整体上分下面几步:
2.2.1、处理图像背景
以这个验证码为例,大片的背景颜色对识别这个这些字体造成了严重干扰,所以首先要想办法把背景颜色消除。
处理代码如下:
im=cv2.imread('验证码图片')
h, w = im.shape[0:2]
colors, counts = np.unique(np.array(im).reshape(-1, 3), axis=0, return_counts=True)
ct=np.sort(counts)
top2_counts=ct[-2:].tolist()
info_dict = {counts[i]: colors[i].tolist() for i, v in enumerate(counts) if not v in top2_counts}
colors_select=np.array([v for v in info_dict.values()])
remove_background_rgbs = colors_select
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)
经过这一步处理后,图片的背景就被替换为白色了,只剩遮住字体的线条还存在一定干扰。
2.2.2、移除干扰线条
移除干扰线条的原理比较简单,由于验证码图片里,干扰线条的颜色跟待识别字体是不一样的,所以可以通过选择字体间隔间的像素颜色,将对应颜色的像素都替换为白色,这样就达到去除线条的目的。
line_list = []
for y in range(h):
for x in range(w):
tmp = mask[x, y].tolist()
if tmp != [0, 0, 0]:
if 0 < y < 20 or 110 < y < 120 or 210 < y < 220:
line_list.append(tmp)
if 0 < x < 10 or 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]
mask[np.any(mask != [255, 255, 255], axis=-1)] = [0, 0, 0]
cv2.imshow("Image with lines removed", mask)
cv2.waitKey(0)
移除线条后的图片是这样:
2.2.3、增强字体显示效果
经过上一步处理后的图片,基本上已经可以很好的辨识出字体原有的形状了,但是由于字形内还存在一些小空隙,整个字看起来会略微有些单薄,所以再做一个腐蚀处理,让字体显示更清晰。
kernel = np.ones((2, 3), 'uint8')
erode_img = cv2.erode(mask, kernel,iterations=2)
cv2.imshow('Eroded Image', erode_img)
cv2.waitKey(0)
cv2.imwrite(r"...\capchta_yuan_processed.jpg", erode_img)
经过处理后的图片,最终效果如下:
在完成以上几个步骤的处理后,我们可以看出图像相比没处理之前,辨识度已经提高很多了。但是这个代码也还是存在一些缺陷,比如有些线条不能完全消除,以及处理后由于线条被去除,连带字体一部分内容也被消除了,这就给后期的识别带来一定难度。
2.3、验证码中的文字识别
2.3.1、ocr识别
ocr识别是一种比较智能化,便捷的提取图片中文字的方法了,bat公司都有提供在线ocr调用接口,百度甚至也开源了paddle ocr 这个python库,实际上这些方式用于识别常规的图片文字是绰绰有余的,精度相当不错。但是第八题这个验证码里的字首先是生僻字,对识别库的汉字训练库要求比较高,其次即使是处理过后的图片,还是存在一些干扰,这就导致我们及时使用这些现有的ocr识别库,依然没法很高精度的识别出图片里的九个字。实际测试中对于验证码里稍微简单点的字,成功率会高一点,但生僻字比例较高的图片里几乎不可能把全部汉字都正确识别出来。所以可能比较合适的一个尝试是自己对这些图片做个深度学习模型训练,这需要对深度学习有一定了解,而且采样、预处理什么的有点耗时间,我这里就暂时放弃这种方法了,还是太菜了(〃 ̄︶ ̄)。
使用easyocr和paddleocr识别图片的代码如下,可以留作参考:
'''easyocr识别图片,设置识别中英文两种语言'''
reader = easyocr.Reader(['ch_sim'], gpu = False)
text = reader.readtext(img_path, detail = 0)
'''paddle ocr识别图片,设置识别中文'''
ocr = PaddleOCR(use_angle_cls=True, lang="ch")
pd_ocr_words = ocr.ocr(img_path, cls=True)
2.3.2、手动识别
手动识别就很简单了,虽然是笨办法,但是很有效,每次把处理好的图片弹出窗口,然后人眼判断对应汉字的位置,再手动输入坐标,就能实现百分百准确识别了…
一开始我是不想用这种方法的,本来就是搞自动化爬虫,结果最后还是手动处理,太没面子了!没办法,谁叫自己技术不到家,技术栈修炼不够啊。
代码的整体思路如下:先给每个字定义固定的索引位置,在人眼识别图片的时候,输入0-8这就个索引就行,最后自动拼接出answer字符串:
cv2.imshow("Image processed", processed_img)
cv2.waitKey(0)
std_index={0:'1',1:'11',2:'21',3:'301',4:'311',5:'321',6:'601',7:'611',8:'621'}
loc=input("手动输入坐标:")
answer_str=''
for l in loc:
answer_str += std_index[int(l)] + '|'
return answer_str
三、完整代码实现
整个代码写的有点长,中间夹杂了许多其他尝试,比如图片切割后ocr识别,或者用识别出的字体跟四个标准字做相似度比较,所以导入的库比较多
import os
import requests
import re
import base64
import io
import cv2
from PIL import Image,ImageFont,ImageDraw
import numpy as np
import pandas as pd
from fuzzychinese import FuzzyChineseMatch
import easyocr
from collections import Counter
HEADERS={
'Proxy-Connection': 'keep-alive',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'User-Agent': 'yuanrenxue.project',
'X-Requested-With': 'XMLHttpRequest',
'Referer': 'http://match.yuanrenxue.com/match/8',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
SESSION=requests.session()
SESSION.headers=HEADERS
reader = easyocr.Reader(['ch_sim'], gpu=False)
def image_process(cv_img):
'''
处理验证码图片,并使用easyocr模块识别图中中文汉字
:param cv_img: cv格式图片内容
:return: 返回九个识别出来的汉字
'''
'''步骤一:处理图像背景'''
im = cv_img
cv2.imshow("原图", im)
cv2.waitKey(0)
h, w = im.shape[0:2]
colors, counts = np.unique(np.array(im).reshape(-1, 3), axis=0, return_counts=True)
ct=np.sort(counts)
top2_counts=ct[-2:].tolist()
info_dict = {counts[i]: colors[i].tolist() for i, v in enumerate(counts) if not v in top2_counts}
colors_select=np.array([v for v in info_dict.values()])
remove_background_rgbs = colors_select
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 y in range(h):
for x in range(w):
tmp = mask[x, y].tolist()
if tmp != [0, 0, 0]:
if 0 < y < 20 or 110 < y < 120 or 210 < y < 220:
line_list.append(tmp)
if 0 < x < 10 or 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]
mask[np.any(mask != [255, 255, 255], axis=-1)] = [0, 0, 0]
cv2.imshow("Image with lines removed", mask)
cv2.waitKey(0)
'''步骤三:图像膨胀腐蚀操作'''
kernel = np.ones((2, 3), 'uint8')
erode_img = cv2.erode(mask, kernel,iterations=2)
cv2.imshow('Eroded Image', erode_img)
cv2.waitKey(0)
'''给图像最右侧加一列空白'''
return erode_img
def ocr_recognization(processed_img):
imglist=image_clip(processed_img)
ocr_words=[]
'''easyocr识别图片,设置识别中英文两种语言'''
for img in imglist:
text = reader.readtext(img, detail = 0)
ocr_words.append(text)
print('easyocr结果:', sum(ocr_words,[]))
return sum(ocr_words,[])
'''paddle ocr识别图片,设置识别中文'''
def manual_recognization(processed_img):
cv2.imshow("Image processed", processed_img)
cv2.waitKey(0)
std_index={0:'1',1:'11',2:'21',3:'301',4:'311',5:'321',6:'601',7:'611',8:'621'}
loc=input("手动输入坐标:")
answer_str=''
for l in loc:
answer_str += std_index[int(l)] + '|'
return answer_str
def word_match(words,ocr_words):
'''
将ocr识别出来的九个字与正确的四个字进行相似度匹配,选择最相似的四个字,返回其点选位置构成的字符串
:param words: 四个带点选的字
:param ocr_words: 识别出来的九个字
:return: answer参数的字符串
'''
std_index={0:'1',1:'11',2:'21',3:'301',4:'311',5:'321',6:'601',7:'611',8:'621'}
answer_str=''
for ind,ocr in enumerate(ocr_words):
for word in words:
if ocr==word:
answer_str+=std_index[ind]+'|'
print(answer_str)
return answer_str
def get_verifyInfo():
'''
解析待待点选的字符和验证码图片
:return:
'''
response = SESSION.get('http://match.yuanrenxue.com/api/match/8_verify').json()
content=response['html']
words=re.findall('<p>(.*?)</p>',content,re.S)
base64_image=re.findall('base64,(.*?)" alt=',content,re.S)[0]
image=base64.b64decode(base64_image)
img1 = np.frombuffer(image, np.uint8)
img = cv2.imdecode(img1, cv2.IMREAD_ANYCOLOR)
return words, img
def get_data():
url = 'http://match.yuanrenxue.com/api/match/8'
for i in range(1,6):
words, img = get_verifyInfo()
print('待识别字符:', words)
processed_img = image_process(img)
answer=manual_recognization(processed_img)
params = (
('page', str(i)),
('answer',answer)
)
response = SESSION.get(url, params=params)
try:
data = response.json()
except:
print('识别失败')
continue
print(data)
def text_to_image(words):
'''汉字转图片'''
for word in words:
text = word
im = Image.new("RGB", (100, 100), (255, 255, 255))
dr = ImageDraw.Draw(im)
font = ImageFont.truetype('C:\Windows\Fonts\simhei.ttf', 100)
dr.text((0, 0), text=text, font=font, fill="#000000")
im.show()
im.save(r"d:/Desktop/{0}.jpg".format(text))
def image_clip(cv_img):
clip_imgs=[]
for y in range(0,300,100):
for x in range(10,300,100):
cropped = cv_img[y:y+100, x:x+100]
clip_imgs.append(cropped)
return clip_imgs
if __name__=="__main__":
get_data()
某一轮五页的验证码识别输出结果:
待识别字符: ['涧', '帄', '围', '止']
手动输入坐标:6243
{'status': '1', 'state': 'success', 'data': [{'value': 7453}, {'value': 1457}, {'value': 5053}, {'value': 2127}, {'value': 4455}, {'value': 4290}, {'value': 9875}, {'value': 7453}, {'value': 8778}, {'value': 2571}]}
待识别字符: ['娆', '嚎', '劳', '钥']
手动输入坐标:1872
{'status': '1', 'state': 'success', 'data': [{'value': 3932}, {'value': 5963}, {'value': 3372}, {'value': 9736}, {'value': 7831}, {'value': 1706}, {'value': 887}, {'value': 9955}, {'value': 4029}, {'value': 3034}]}
待识别字符: ['螅', '恽', '謟', '埚']
手动输入坐标:4315
{'status': '1', 'state': 'success', 'data': [{'value': 9606}, {'value': 3850}, {'value': 4106}, {'value': 2381}, {'value': 8545}, {'value': 2403}, {'value': 9984}, {'value': 7453}, {'value': 3585}, {'value': 7545}]}
待识别字符: ['牝', '衙', '铐', '山']
手动输入坐标:7106
{'status': '1', 'state': 'success', 'data': [{'value': 5231}, {'value': 7453}, {'value': 6090}, {'value': 6476}, {'value': 2965}, {'value': 5510}, {'value': 3879}, {'value': 7453}, {'value': 5821}, {'value': 1356}]}
待识别字符: ['绖', '璜', '餋', '鳅']
手动输入坐标:0863
{'status': '1', 'state': 'success', 'data': [{'value': 4798}, {'value': 8040}, {'value': 3086}, {'value': 7453}, {'value': 9874}, {'value': 4251}, {'value': 2862}, {'value': 677}, {'value': 9708}, {'value': 7902}]}
四、结语
爬虫案例尝试的越多,越发现搞爬虫是一项对技术栈要求很高的任务,js逆向、深度学习什么的任何一门领域内容都是非常庞杂的,掌握皮毛不难,想要学深的话就需要花很多功夫了,总之且学且珍惜,慢慢修炼吧。
五、参考文献
|