Python爬虫教程

第一章: 初学乍练-Python快速入门

第二章: 初窥门径-从全局把握网络爬虫

第三章: 爬虫数据-网页与JSON

第四章: 爬虫核心-HTTP协议

第五章: 手到擒来-数据的抓包

第六章: 利刃出鞘-HTTP请求库

第七章: 尘埃落定-数据的解析

第八章: 逆向初探-JS逆向

第九章: 爬虫进阶-Selenium, 中间人拦截

第十章:斗转星移-常用的反爬策略及应对方法

首页 > Python爬虫教程 > 第十章:斗转星移-常用的反爬策略及应对方法 > 10.3节:使用验证码进行反爬

10.3节:使用验证码进行反爬

薯条老师 2021-06-24 08:04:28 238151 0

编辑 收藏

教程引言:

系统地讲解计算机基础知识,Python的基础知识, 高级知识。关注微信公众号[薯条编程],免费领取Python电子书以及视频课程。

10.3.1 简单图片验证码

我们在注册或登录某些站点时,会在页面中看到图片验证码,如下图所示:

 

图片.png图片.png

图形验证码可用来保护站点免受爬虫的攻击,因为爬虫程序做不到像人一样通过肉眼来快速识别验证码,这在一定程度上增加了数据爬取的难度。

许多站点限制了必须登录以后才能访问特定的页面,当遇到需要提交验证码参数的登录接口时,必须得先破解登录页面的图片验证码。对于这种简单的图片验证码,破解方法比较简单。图片验证码通常有对应的url,找到这个url就可以获取到这张图片。

获取到图片以后,再通过机器学习,深度学习等算法来识别出图片中的验证码。对于像上文中的简单图片验证码,可通过OCR(光伏字符识别)来进行处理。

现在请同学们按照以下步骤来练习破解简单的图片验证码:

(1) 安装tesseract

tesseract的windows安装包地址,根据你电脑的配置来安装对应的版本:

https://digi.bib.uni-mannheim.de/tesseract/

安装成功以后将tesseract可执行文件所在的目录添加至环境变量PATH,并添加一个系统变量TESSDATA_PREFIX来保存tesseract安装目录下的tessdata的绝对路径。配置成功以后,在命令行中执行tesseract --version, 如有见到tesseract的版本信息,则说明安装成功。

图片.png 

继续在命令行中执行pip install  pytesseract,以安装tesseract模块。

(2) 对图片进行预处理

这里的预处理主要是对图片的像素进行适当处理,以减少干扰,增加识别的准确度。在Python中可通过PIL模块以及opencv对图片进行灰度化,二值化,去噪等处理。在命令行中执行以下命令安装PIL:

pip install pillow

在命令行中执行以下命令安装opencv:

pip install opencv-python

代码实例-对图片进行灰度化以及二值化处理:

import PIL
chips_image = PIL.Image.open('chipscoco.png')
# 将图片转成灰度图
chips_gray = chips_image.covert('L')
# 将图片按指定的阈值转换成二值图
threshold = 200
chips_binary = chips_gray.point(lambda x: 255 if x >= threshold else 0, "1")

代码实例-对图片进行降噪处理:

# __author__ = 薯条老师
import cv2
chips_image = cv2.imread('chipscoco.png')
# 通过中值滤波对图片进行降噪处理。其它的降噪处理方法,同学们可查找opencv文档
chips_blur = cv2.medianBlur(chips_image, 3)

(3) 使用tesseract进行字符识别

对图片进行预处理以后就可以提高字符识别的准确率,以下是薯条老师在网上搜索到的两张比较简单的验证码图片:

图片.png图片.png 

代码实例-对第一张验证码图片进行字符识别:

# __author__ = 薯条老师
import pytesseract, PIL, re
chips_image = PIL.Image.open('chipscoco.png')
# 将图片转成灰度图
chips_gray = chips_image.convert('L')
# 根据图片的具体像素值分布情况来设置阈值
threshold = 150
chips_binary = chips_gray.point(lambda x: 255 if x >= threshold else 0,'1' )
# 执行pytesseract模块的image_to_string方法对图片验证码进行识别
text = pytesseract.image_to_string(chips_binary)
text = re.sub(r"\s+", "", text)
print(text)

程序的输出结果:

nVNA

代码实例-对第二张验证码图片进行字符识别:

# __author__ = 薯条老师
import pytesseract, PIL, re, cv2
chips_image = cv.imread('chipscoco.png')
# 使用中值滤波对图片进行降噪处理
chips_blur = cv2.medianBlur(chips_image, 3)
# 通过PIL.Image模块 的fromarray方法将array格式的图片转换成PIL图片对象
chips_image = PIL.Image.fromarray(cv.cvtColor(blur, cv.COLOR_BGR2RGB))
chips_gray = chips_image.convert('L')
chips_binary = chips_gray.point(lambda x: 255 if x >= 220 else 0, "1")
# 执行pytesseract模块的image_to_string方法对图片验证码进行识别
text = pytesseract.image_to_string(chips_binary)
text = re.sub(r"\s+", "", text)
print(text)

程序的输出结果:

7364

为进一步提高tesseract的识别准确率,同学们可以先爬取目标站点的大量验证码图片,构建一个训练集,再通过tesseract提供的训练方法得到一个模型。关于tesseract的模型训练方法,不在本节讲述范围内,同学们可在官方文档中查找相关资料作进一步学习。

10.3.2 极验滑动验证码

极验滑动验证码是武汉极意网络科技公司推出的一款验证码安全产品,用来帮助企业优化验证流程(当然也包括反爬)。以下是极验官网登录页弹出的一张滑动验证码图片,初学者对这类验证码一定不会感到陌生。

图片.png 

网上不少博主分享了破解极验滑动验证码的代码,本节仍以极验为例,详细讲解如何破解极验的滑动验证码。同学们在学习的时候,侧重于学习破解的思路,因为提供滑动验证码产品的不只是极验一家,破解的方式也不尽相同。

这类验证码有两个很典型的特征,一是滑块,一是背景图片的缺口。用户在登录站点时需要滑动滑块至缺口处并覆盖。我们在破解这类滑动验证码时,关键在于找到背景图片的缺口位置,然后用程序来模拟滑块的拖动。模拟滑块的拖动比较简单,可使用selenium进行模拟。找到背景图片的缺口位置则相对困难,需要初学者具备一定的图像处理知识。

现在进入极验的后台登录页,以下是极验登录页的地址:

https://auth.geetest.com/login/

在登录页随便输入登录账号和密码,并点击按钮进行验证。当页面中出现滑动验证码图片时,我们进入浏览器的调试模式,然后分析html源码可得到滑块图片和带缺口的背景图片:

图片.png图片.png
 

 

这两张图片是通过html的canvas生成的,以下分别是滑块和背景图的HTML代码:

<canvas class="geetest_canvas_slice geetest_absolute" width="260" height="160" style="opacity: 1; display: block;"></canvas>
<canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas>

在程序中可通过selenium来抓取到canvas图片,后面再进行讲解。现在先着重教同学们如何识别背景图片中的滑动缺口位置。既然有了滑块图片和带缺口的背景图片,那么最容易想到的方法自然是图像处理中的模板匹配算法。执行以下代码对滑块图片进行预处理,用来去掉透明背景,并进行裁剪:

import cv2
import numpy
from PIL import Image
from PIL import ImageFile


def remove_transparency_with_pil(img, bg_color=(255, 255, 255)):
    """ 去掉背景图片中的透明通道 """
    if img.mode in ('RGBA', 'LA') or \
        (img.mode == 'P' and 'transparency' in img.info):
        alpha = img.convert('RGBA').split()[-1]
        bg = Image.new("RGBA", img.size, bg_color + (255,))
        bg.paste(img, mask=alpha)
        return bg
    else:
        return img
def crop_background_with_cv2(img):
    """ 裁切背景,保留前景 """
    img = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    th, threshed = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)
    cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
    cnt = sorted(cnts, key=cv2.contourArea)[-1]
    x,y,w,h = cv2.boundingRect(cnt)
    dst = img[y:y+h, x:x+w]
    return dst
    
    
if __name__ == '__main__':
    img_slice = Image.open("slice_with_alpha.png")
    img_slice = crop_background_with_cv2(remove_transparency_with_pil(img_slice))
    cv2.imwrite("slice.png", img_slice)

程序执行成功以后,会得到下图所示的裁剪后的滑块图片:

 图片.png

再通过模板匹配,来找出背景图片中的缺口位置:

import cv2
import numpy
from PIL import Image
from PIL import ImageFile


def remove_transparency_with_pil(img, bg_color=(255, 255, 255)):
    """ 去掉背景图片中的透明通道 """
    if img.mode in ('RGBA', 'LA') or \
        (img.mode == 'P' and 'transparency' in img.info):
        alpha = img.convert('RGBA').split()[-1]
        bg = Image.new("RGBA", img.size, bg_color + (255,))
        bg.paste(img, mask=alpha)
        return bg
    else:
        return img
        
        
def crop_background_with_cv2(img):
    """ 裁切背景,保留前景 """
    img = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    th, threshed = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)
    cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
    cnt = sorted(cnts, key=cv2.contourArea)[-1]
    x,y,w,h = cv2.boundingRect(cnt)
    dst = img[y:y+h, x:x+w]
    return dst
    
    
def detect_gap(img_slice,  img_background):
    """通过模板匹配来识别出缺口位置"""
    # 执行模板匹配
    res = cv2.matchTemplate(img_slice, img_background, cv2.TM_CCOEFF_NORMED)
    # 利用模板匹配的返回值获取最小值,最大值的索引
    _, _, min_loc, max_loc = cv2.minMaxLoc(res)
    # 获取左上角的匹配坐标
    top_left = max_loc[0]
    # 获取匹配位置的x,y坐标,用来在背景图中框住缺口,方便验证
    x, y = max_loc
    # 获取滑块的宽和高
    w, h = img_slice.shape[::-1]
    # 根据获得的最佳匹配的x,y坐标以及滑块图片的宽高来框住缺口
    cv2.rectangle(img_background, (x, y), (x + w, y + h), (7, 249, 151), 2)
    return top_left,  img_background
    
    
if __name__ == '__main__':
    img_slice = Image.open("slice_with_alpha.png")
    img_slice = crop_background_with_cv2(remove_transparency_with_pil(img_slice))
    cv2.imwrite("slice.png", img_slice)
    # 将裁切后的滑块图像转换为灰度图
    img_slice = cv2.cvtColor(img_slice, cv2.COLOR_BGR2GRAY)
    # 读取带缺口的背景图,并转换为灰度图
    img_background =  cv2.imread("bg.png", 0)
    top_left, img_background_with_bounding = detect_gap(img_slice, img_background)
    print("缺口在图片中的左上角位置:{}".format(top_left))
    cv2.imwrite("bg.bounding.png", img_background_with_bounding)

程序执行成功以后会输出缺口的左上角位置126,以及一张缺口被框起来的灰度图:

图片.png 

同样地,为提高图像匹配的准确度,同学们可对图片进行降噪,增强等预处理。得到缺口的位置以后,就可以利用selenium来模拟用户的拖动。读者在模拟用户的拖动时需要注意:应尽可能地模拟出用户的正常拖动轨迹。否则很容易被极验识别为爬虫程序。

回想下,当我们自己拖动滑动验证码时,一般是先快速的拖动滑块到缺口附近,然后再慢慢地将滑块移动至缺口位置。根据物理学的匀变速直线运动即可构造这样的移动轨迹,以下为程序中使用到的匀变速直线运动公式:

(1) 匀变速直线运动的速度与时间关系的公式:V=V0+at

(2) 匀变速直线运动的位移与时间关系的公式:x=v0*t+1/2*at^2

代码实例-构造移动轨迹:

def calc_tracks(distance,  t=0.2, threshold=5/7,  a=2,  d=-3):
    """
    :param distance: 需要移动的距离
    :param t: 默认值为0.2,表示以0.2秒的间隔来计算位移
    :param threshold: 默认值为5/7,表示已拖动5/7的距离以后开始做减速运动
    :param a: 匀加速时的速度,默认为2
    :param d: 匀减速时的速度,默认为-3
    :return: 返回一个列表,列表中的每一项表示t秒内的位移
    """
    # v0表示初速度,初速度初始时置为0, v表示当前速度
    v0,  v = 0, 0
    # tracks表示移动轨迹,列表内的每一个值表示每t秒移动的距离
    tracks = []
    # 当前的位移
    current = 0
    threshold = distance * threshold
    while current < distance:
        v0 = v
        # 套用公式来计算当前的匀变速速度
        v = v0 + a * t
        # 套用公式来计算t秒时间内的位移
        x = v0*t+0.5*a*(t**2)
        # 再往前移动x表示的一段距离
        current += x
        # 将计算得到的位移作为移动距离添加至移动轨迹列表中
        tracks.append(round(x))
        # 移动的距离超出阈值时,改为匀减速运动
        if a != d and current >= threshold :
            a = d
    return tracks

构造好了移动轨迹以后就可以通过selenium来模拟用户的拖动。以下是完整的源码:

# __author__ = 薯条老师
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from PIL import Image
from io import BytesIO
import base64
import cv2
import numpy as np
import time
import random


def simulate_user_input(url,  email="test@chipscoco.com",  password="123456"):
    """ 模拟用户在登录框的输入,输入邮箱地址和密码 """
    WEB_DRIVER.get(url)
    # 输入邮箱地址前,先等待页面中加载完class为ivu-input的input标签
    DRIVER_WAIT.until(EC.presence_of_element_located((By.CLASS_NAME, 'ivu-input')))
    email_input = WEB_DRIVER.find_elements_by_class_name('ivu-input')[0]
    # 输入邮箱地址
    email_input.send_keys(email)
    password_input = WEB_DRIVER.find_elements_by_class_name('ivu-input')[1]
    # 输入密码
    password_input.send_keys(password)
    
    
def simulate_clicking():
    """ 模拟点击验证按钮 """
    # 验证按钮所对应的class为geetest_radar_tip, class名可能会被更新,读者需分析html源码
    radar_tip = DRIVER_WAIT.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
    radar_tip.click()
    # 模拟点击以后,可能验证成功。如果验证不成功,需要继续通过滑动验证码来进行验证
    try:
        success = DRIVER_WAIT.until(
            EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_radar_tip'), '验证成功'))
    except TimeoutException:
        success = False
    return success
    
    
def scratch_slider_image(save=False, img_name="slider.png"):
    """ 抓取滑块的canvas图片 """
    slider_strategy = True
    try:
        # 滑块图片所对应的html class为geetest_canvas_slice, 可能会有更新,需分析源码
        DRIVER_WAIT.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_slice')))
    except TimeoutException:
        slider_strategy = False
    try:
        DRIVER_WAIT.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_tip_img')))
        touch_strategy_ = True
    except TimeoutException:
        touch_strategy_ = False
    slider_img = None
    if slider_strategy:
        js_code = 'return document.getElementsByClassName("geetest_canvas_slice ' \
                  'geetest_absolute")[0].toDataURL("image/png");'
        # 执行以下JS代码,获取滑块的base64数据
        img_data = WEB_DRIVER.execute_script(js_code)
        img_base64 = img_data.split(',')[1]
        # 将滑块的字节流数据转换为PIL的Image对象
        slider_img = Image.open(BytesIO(base64.b64decode(img_base64)))
        if save:
            slider_img.save(img_name)
    return slider_img, touch_strategy_
    
    
def scratch_bg_image(save=False, img_name="bg.png"):
    """ 抓取带缺口的canvas背景图片,返回的图片是灰度图 """
    try:
        # 背景图片所对应的html class为geetest_canvas_bg, 可能会有更新,需分析源码
        DRIVER_WAIT.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_bg')))
        js_code = 'return document.getElementsByClassName("geetest_canvas_bg ' \
                  'geetest_absolute")[0].toDataURL("image/png");'
        # 执行以下JS代码,获取滑块的base64数据
        img_data = WEB_DRIVER.execute_script(js_code)
        img_base64 = img_data.split(',')[1]
        img_array = np.frombuffer(base64.b64decode(img_base64), np.uint8)
        img_bg = cv2.imdecode(img_array, cv2.COLOR_RGB2BGR)
        if save:
            cv2.imwrite(img_name, img_bg)
    except TimeoutException:
        img_bg = None
    return img_bg
    
    
def remove_transparency_with_pil(img, bg_color=(255, 255, 255)):
    """ 去掉背景图片中的透明通道 """
    if img.mode in ('RGBA', 'LA') or \
        (img.mode == 'P' and 'transparency' in img.info):
        alpha = img.convert('RGBA').split()[-1]
        bg = Image.new("RGBA", img.size, bg_color + (255,))
        bg.paste(img, mask=alpha)
        return bg
    else:
        return img
        
        
def crop_background_with_cv2(img):
    """ 裁切背景,保留前景 """
    img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    th, threshed = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)
    cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
    cnt = sorted(cnts, key=cv2.contourArea)[-1]
    x, y, w, h = cv2.boundingRect(cnt)
    dst = img[y:y+h, x:x+w]
    return dst
    
    
def detect_gap(img_slice,  img_background):
    """通过模板匹配来识别出缺口位置"""
    # 执行模板匹配
    res = cv2.matchTemplate(img_slice, img_background, cv2.TM_CCOEFF_NORMED)
    # 利用模板匹配的返回值获取最小值,最大值的索引
    _, _, min_loc, max_loc = cv2.minMaxLoc(res)
    # 获取左上角的匹配坐标
    top_left = max_loc[0]
    # 获取匹配位置的x,y坐标,用来在背景图中框住缺口,方便验证
    x, y = max_loc
    # 获取滑块的宽和高
    w, h = img_slice.shape[::-1]
    # 根据获得的最佳匹配的x,y坐标以及滑块图片的宽高来框住缺口
    cv2.rectangle(img_background, (x, y), (x + w, y + h), (7, 249, 151), 2)
    return top_left,  img_background
    
    
def calc_tracks(distance,  t=0.2, threshold=5/7,  a=2,  d=-3):
    """
    :param distance: 需要移动的距离
    :param t: 默认值为0.2,表示以0.2秒的间隔来计算位移
    :param threshold: 默认值为5/7,表示已拖动5/7的距离以后开始做减速运动
    :param a: 匀加速时的速度,默认为2
    :param d: 匀减速时的速度,默认为-3
    :return: 返回一个列表,列表中的每一项表示每0.2秒移动的位移
    """
    # v0表示初速度,初速度一般为0,  v表示当前速度
    v0,  v = 0, 0
    # 位移/轨迹列表,列表内的一个元素代表0.2s的位移
    tracks = []
    # 当前的位移
    current = 0
    threshold = distance * threshold
    while current < distance:
        v0 = v
        # 套用公式来计算当前的匀变速速度
        v = v0 + a * t
        # 套用公式来计算t秒时间内的位移
        x = v0*t+0.5*a*(t**2)
        # 再往前移动x表示的一段距离
        current += x
        # 将计算得到的位移作为移动距离添加至移动轨迹列表中
        tracks.append(round(x))
        # 移动的距离超出阈值时,改为匀减速运动
        if current >= threshold:
            a = d
    return tracks
    
    
def simulate_sliding():
    """ 模拟滑动验证码 """
    success = False
    img_slider, touch_strategy = scratch_slider_image(save=True)
    if touch_strategy:
        print("需通过点选验证码来进行验证")
        WEB_DRIVER.close()
    else:
        img_bg = scratch_bg_image(save=True)
        img_bg_gray = cv2.cvtColor(img_bg, cv2.COLOR_BGR2GRAY)
        img_slice = crop_background_with_cv2(remove_transparency_with_pil(img_slider))
        img_slice_gray = cv2.cvtColor(img_slice, cv2.COLOR_BGR2GRAY)
        # 通过模板匹配的算法计算缺口左上角的位置
        top_left,  _ = detect_gap(img_slice_gray, img_bg_gray)
        slider = DRIVER_WAIT.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
        # 鼠标点击滑块
        ActionChains(WEB_DRIVER).click_and_hold(on_element=slider).perform()
        # 模拟正常用户快速地滑动滑块
        quickly_move1 = 3 * top_left // 5
        quickly_move2 = 3 * (top_left - quickly_move1) // 5
        ActionChains(WEB_DRIVER).move_by_offset(xoffset=quickly_move1, yoffset=0).perform()
        time.sleep(0.5)
        ActionChains(WEB_DRIVER).move_by_offset(xoffset=quickly_move2, yoffset=0).perform()
        # 计算移动轨迹
        tracks = calc_tracks(top_left - (quickly_move1 + quickly_move2))
        for track in tracks:
            ActionChains(WEB_DRIVER).move_by_offset(xoffset=track, yoffset=0).perform()
        ActionChains(WEB_DRIVER).move_by_offset(xoffset=-random.randint(2, 3), yoffset=0).perform()
        # 停留一秒后释放鼠标
        time.sleep(1)
        ActionChains(WEB_DRIVER).release().perform()
        success = DRIVER_WAIT.until(
            EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_success_radar_tip_content'), '验证成功'))
    return success,touch_strategy
    
    
if __name__ == "__main__":
    WEB_DRIVER = webdriver.Firefox()
    DRIVER_WAIT = WebDriverWait(WEB_DRIVER, 5)
    # 第一步模拟用户在登录框的输入
    login_url = "https://gtaccount.geetest.com/login/"
    simulate_user_input(login_url)
    # 第二步模拟用户点击验证按钮,如果验证失败,则需通过滑动验证码来进行验证
    simulate_sliding() if not simulate_clicking() else print("验证成功")

以上程序只模拟了一次用户的拖动,读者可对代码做简单修改,程序在模拟登录指定的次数以后再退出程序。再者,极验对智能反爬做了升级,一旦监测到程序使用了selenium,会弹出更复杂的点选验证码,如下图所示:

图片.png图片.png 

10.3.3 点选验证码

10.3.2节末尾贴出的两张图片即为点选验证码,这类验证码从结构上可分为两块:一是需要点击的目标(图像或文字),一是包含点选目标的背景图。破解这类验证码,最容易想到的方法是将需要点选的目标逐个裁切下来,然后在背景图中做模板匹配。最考验技术能力的是将目标站点的点选验证码图片抓取下来,然后利用深度学习来训练一个高效的目标检测模型。最复杂的方法,则是通过js逆向来进行破解。

当然,最不费事的是使用第三方的打码平台(比如超级鹰),将点选验证码图片通过API发送给第三方,然后直接获取点选的坐标。以上方法,读者可以一一去尝试,在本节教程中不再进行讲解。

(1) Python后端工程师高薪就业班,月薪10K-15K,免费领取课程大纲
(2) Python爬虫工程师高薪就业班,年薪十五万,免费领取课程大纲
(3) Java后端开发工程师高薪就业班,月薪10K-20K, 免费领取课程大纲
(4) Python大数据工程师就业班,月薪12K-25K,免费领取课程大纲

扫码免费领取学习资料:


欢迎 发表评论: