Mack-Yang

一、图形验证码的识别

  • 先将验证码的图片保存到本。
    • 打开开发者工具,找到验证码元素。验证码元素是一张图片,src 属性是 CheckCode.aspx。打开链接 http://my.cnki.net/elibregister/CheckCode.aspx,保存并命名为 code.jpg。
  1. 识别测试
    • 新建一个项目,将验证码图片放到项目根目录下,用 tesserocr 库识别验证码,示例:
      import tesserocr
      from PIL import Image
      
      image = Image.open(\'code.jpg\')
      result = tesserocr.image_to_text(image)
      print(result)

      这里新建了一个 Image 对象,调用 tesserocr 的 image_to_ text()方法。传入该 Image 对象 即可完成识别。

    • tesserocr 还有一个更加简单的方法,这个方法可直接将图片文件转为字符串,代码:

      import tesserocr
      print(tesserocr.file_to_text(\'image.png\'))

       此方法的识别效果不如上一种方法好。

  2. 验证码处理

    • 换一个验证码带有多线条,命名为 code2.jpg。重新识别和实际结果有偏差。

    • 对于有线条干扰情况,还需要做额外处理,如转灰度、二值化等操作。 可以利用 Image 对象的 convert()方法参数传人 L,即可将图片转化为灰度图像,示例:

      image = image.convert(\'L\')
      image.show()

      传入1 即可将图片进行二值化处理:

      image = image.convert(\'1\')
      image.show()

      还可以指定二值化的阈值,上面的方法采用的是默认阈(yù)值。以上方法采用默认阔值 127。不能直接转化原图,要将原图先转为灰度图像,再指定二值化阔值,代码:

      import tesserocr
      from PIL import Image
      
      image = Image.open(\'code.jpg\')
      image = image.convert(\'L\')
      threshold = 80
      table = []
      for i in range(256):
          if i < threshold:
              table.append(0)
          else:
              table.append(1)
      
      image = image.point(table,\'1\')
      image.show()

      变量 threshold 代表二值化阈值,阈值设置为 80。原来验证码中的线条已经去除,整个验证码将会变得黑向分明。这时再重新识别验证码,代码:

      import tesserocr
      from PIL import Image
      
      image = Image.open(\'code2.jpg\')
      
      image = image.convert(\'L\')
      threshold = 127
      table = []
      for i in range(256):
          if i < threshold:
              table.append(0)
          else:
              table.append(1)
      image = image.point(table,\'1\')
      result = tesserocr.image_to_text(image)
      print(result)

      针对一些有干扰的图片,做一些灰度和二值化处理,会提高图片识别的正确率。

    • 阈值:继续遍历图片。如果图片灰度值大于阈值说明是背景图片,将之颜色设置为白色,否则是文字,颜色设置为黑色
    • 大概逻辑:

      • (计次循环首(图片.取宽度(),x)
        计次循环首(图片.取高度(),y)
        ‘遍历每一个像素点
        ‘取出遍历到的当前像素点颜色
        颜色值=到字节集(图片.取某点颜色值(x-1,y-1))
        ‘计算灰度值
        灰度值=(颜色值[1]+颜色值[2]+颜色值[3])/3
        ‘将灰度值放回图片
        连续赋值(灰度值,颜色值[1],颜色值[2],颜色值[3])
        图片.写某点颜色值(x-1,y-1,灰度值)
        计次循环尾()
        计次循环尾()
        图片.取图片数据()      

二、极验滑动验证码的识别

    1. 识别思路
      • 首先找到一个带有极验验证的网站,如极验官方后台,链接:https://account.geetest.com/login。
  • 可以使用Selenium来完全模拟人的行为的方式来完成验证。(一般,验证需要三步:)

    1. 模拟点击验证按钮: 可以直接用Selenium 模拟点击按钮
    2. 识别滑动缺口的位置: 
    3. 模拟拖动滑块
      1. 识别滑动缺口的位置的操作比较关键,需要用到图像相关处理方法
        • 首先观察图像:缺口的四周边缘有明显的断裂边缘,边缘和边缘周围有明显的区别。可以实现一个边缘检测算法来找出缺口的位置。对于极验验证码来说,可以利用和原图对比检测的方式来识别缺口的位置,因为在没有滑动滑块之前, 缺口并没有呈现。
        • 可以同时获取两张图片。设定一个对比阈值,然后遍历两张图片,找出相同位置像素 RGB 差距超过此阈值的像素点,那么此像素点的位置就是缺口的位置。

      2. 模拟拖动滑块中需要走注意:极验验证码增加了机器轨迹识别,匀速移动、 随机速度移动等方法都不能通过验证,只有完全模拟人的移动轨迹才可以通过验证。 人的移动轨迹一般是先加速后减速,需要模拟这个过程才能成功。
  • 初始化
    • 选定链接:https://account.geetest.com/login,极验的管理后台登录页面。首先初始化一些配置,如 Selenium 对象的初始化及一些参数的配置,示例:
      from selenium import webdriver
      from selenium.webdriver.support.wait import WebDriverWait
      
      EMAIL = \'test@test.com\'
      PASSWORD = \'123456\'
      
      class CrackGeetest():
          def __init__(self):
              self.url = \'https://account.geetest.com/login\'
              self.browser = webdriver.Chrome()
              self.wait = WebDriverWait(self.browser,20)
              self.email = EMAIL
              self.password = PASSWORD

      其中, EMAIL 和 PASSWORD 就是登录极验需要的用户名和密码,如果没有需先注册。

  • 模拟点击

    • 实现第一步的操作,也就是模拟点击初始的验证按钮

      • 定义一个方法来获取这个按钮,利用显式等待的方法来实现,示例:

        def get_geetest_button(self):
            "获取初始验证按钮:return:按钮对象"
            button = self.wait.unil(EC.element_to_be_clickable((By.CLASS_NAME,\'geetest_radar_tip\')))
            return button

        获取一个WebElement对象,调用它的click()方法即可模拟点击,示例:

        #点击验证按钮
        button = get_geetest_button()
        button.click()

        完成第一步模拟点击验证按钮

  • 识别缺口

    • 识别缺口的位置。

      • 首先获取前后两张比对图片,二者不一致的地方即为缺口 。获取不带缺口图片,利用 Selenium选取图片元素,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切出来即可,代码:

        def get_position(self):
            """获取验证码位置:return:验证码位置元组"""
            img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,\'geetest_canvas_img\')))
            time.sleep(2)
            location =img.location
            size = img.size
            top, bottom, left, right = location[\'y\'],location[\'y\']+size[\'weight\'],location[\'x\'],location[\'x\']+size[\'width\']
            return(top, bottom, left, right)
        
        def get_geetest_image(self,name=\'captcha.png\'):
            """获取验证码图片:return:图片对象"""
            top,bottom,left,right = self.get_position()
            print(\'验证码位置\', top, bottom, left, right)
            screenshot = self.get_screenshot()
            captcha = screenshot.crop((left, top, right, bottom))
            return captcha

        这里 get_position()函数首先获取图片对象,获取它的位置和宽高,随后返回其左上角和右下角的坐标 get_geetest_image()方法获取网页截图,调用 crop()方法将图片裁切出来,返回的是 Image 对象

      • 接下来需要获取第二张图片,也就是带缺口的图片。要使得图片出现缺口,只需要点击下方的滑块即可。这个动作触发之后,图片中的缺口就会显现,代码:
        def get_slider(self):
            """获取滑块:return:滑块对象"""
            slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,\'greetest_slider_button\')))
            return slider

        利用 get_slider()方法获取滑块对象,调用 click()方法即可触发点击,缺口图片即可呈现,代码:

        #点按呼出缺口
        slider = self.get_slider()
        slider.click()

        调用 get_geetest_image()方法将第二张图片获取下来即可。

      • 现在已经得到两张图片对象,分别赋值给变量 image1 和 image2。再对比图片获取缺口。这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据。如果二者 RGB 数据差距距在一定范围内,那就代表两个像素相同,继续比对下一个像素点 如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置,代码:
        def is_pixel_equal(self, image1, image2, x, y):
            """判断两个像素是否相同
             :param image1:图片1
             :param image1:图片1
             :param x:位置x
             :param y:位置y
             :return:像素是否相同"""
            #取两个图片的像素点
            pixel1 = image1.load()[x, y]
            pixel2 = image2.load()[x, y]
            threshold = 60
            if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(pixel1[2] -pixel2[2])<threshold:
                return True
            else:
                return False
        def get_gap(self, image1, image2):
            """获取缺口偏移量
            :param image1:不带缺口图片
            :param image2:带缺口图片
            :return:"""
            left = 60
            for i in  range(left,image1.size[0]):
                for j in range(image1.size[1]):
                    if not self.is_pixel_equal(image1, image2, i, j):
                        left = i
                        return left
            return left

        get_gap()方法即获取缺口位置的方法。此方法的参数是两张图片, 一张为带缺口图片,另一张为不带缺口图片。遍历两张图片的每个像素,利用 is_pixel_equal()方法判断两张图片同一位置的 像素是杏相同。比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold。如果绝对值均在阔值之 内,则代表像素点相同,继续遍历。否则代表不相同的像素点,即缺口的位置。

      • 滑块的位置会出现在左边位置,缺口会出现在与滑块同一水平线的位置,所以缺口一般会在滑块的右侧。如果要寻找缺口,直接从滑块右侧寻找即可。直接设置遍历的起始横坐标为 60,也就是从滑块的右侧开始识别,这样识别出的结果就是缺口的位置。
  • 模拟拖动
    • 完全模拟加速减速的过程也就是人拖动滑块的操作通过验证。前段滑块做匀加速运动,后段滑块做匀减速运动,利用加速度公式即可完成验证。
      • 滑块滑动的加速度用 a 表示, 当前速度用 v 表示,初速用 v0 表示,位移用 x 表示,所需时间用t 表示,满足关系:
        • x = v0*t + 0.5*a*t*t
        • v = v0 + a*t
      • 利用这两个公式可以构造轨迹移动算法,计算出先加速后减速的运动轨迹,代码:
        def get_track(self, distance):
            """根据偏移量获取移动轨迹
            :param distance:偏移量
            :return: 移动轨迹"""
            #移动轨迹
            track = []
            #当前位移
            current = 0
            #减速阈值
            mid = distance * 4 / 5
            #计算间隔
            t = 0.2
            #初速度
            v = 0
            
            while current < distance:
                if current < mid:
                    a = 2
                else:
                    a = -3
                #初速度 V0
                v0 = v
                v = v0 + a*t
                move = v0 *t +1/2 *a *t *t
                current += move
                #加入轨迹
                track.append(round(move))
            return track

        定义 get_ rack()方法,传人的参数为移动的总距离,返回的是运动轨迹。运动轨迹用 track 表示,是一个列表,列表的每个元素代表每次移动多少距离

        • 定义变量 mid,即减速的阔值,也就是加速到什么位置开始减速。这里 mid 值为 4/5,即模拟前 4/5 路程是加速过程,后 1/5 路程是减速过程。

        • 接着定义当前位移的距离变量 current,初始为 0,然后进入 while 循环,循环的条件是当前位移小于总距离。在循环里我们分段定义了加速度,其中加速过程的加速度定义为 2,减速过程的加速度定义为 -3。之后套用位移公式计算出某个时间段内的位移,将当前位移更新并记录到轨迹里即可。
        • 直到运动轨迹达到总距离时,循环终止。最后得到的 track 记录了每个时间间隔移动了多少位移, 这样滑块的运动轨迹就得到了。
        • 最后按照该运动轨迹拖动滑块即可,方法实现代码:
          def move_to_gap(self, slider,tracks):
              """拖动滑块到缺口处
              :params slider:滑块
              :params tracks:轨迹
              :return:"""
              ActionChains(self,browser).click_and_hold(slider).perform()
              for x in tracks:
                  ActionChains(self,browser).move_by_offset(xoffset=x,yoffset=0).perform()
              time.sleep(0.5)
              ActionChains(self.browser).release().perform()

          传人的参数为滑块对象和运动轨迹。首先调用 ActionChains 的 click_and_ hold()方法按住拖动底部滑块,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()方法移动此位移,最后调用 release()方法松开鼠标即可。

  • 完整代码(需适当修改参数):

    import time
    from io import BytesIO
    from PIL import Image
    from selenium import webdriver
    from selenium.webdriver import ActionChains
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    EMAIL = \'cqc@cuiqingcai.com\'
    PASSWORD = \'\'
    BORDER = 6
    INIT_LEFT = 60
    
    
    class CrackGeetest():
        def __init__(self):
            self.url = \'https://account.geetest.com/login\'
            self.browser = webdriver.Chrome()
            self.wait = WebDriverWait(self.browser, 20)
            self.email = EMAIL
            self.password = PASSWORD
        
        def __del__(self):
            self.browser.close()
        
        def get_geetest_button(self):
            """
            获取初始验证按钮
            :return:
            """
            button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, \'geetest_radar_tip\')))
            return button
        
        def get_position(self):
            """
            获取验证码位置
            :return: 验证码位置元组
            """
            img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, \'geetest_canvas_img\')))
            time.sleep(2)
            location = img.location
            size = img.size
            top, bottom, left, right = location[\'y\'], location[\'y\'] + size[\'height\'], location[\'x\'], location[\'x\'] + size[
                \'width\']
            return (top, bottom, left, right)
        
        def get_screenshot(self):
            """
            获取网页截图
            :return: 截图对象
            """
            screenshot = self.browser.get_screenshot_as_png()
            screenshot = Image.open(BytesIO(screenshot))
            return screenshot
        
        def get_slider(self):
            """
            获取滑块
            :return: 滑块对象
            """
            slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, \'geetest_slider_button\')))
            return slider
        
        def get_geetest_image(self, name=\'captcha.png\'):
            """
            获取验证码图片
            :return: 图片对象
            """
            top, bottom, left, right = self.get_position()
            print(\'验证码位置\', top, bottom, left, right)
            screenshot = self.get_screenshot()
            captcha = screenshot.crop((left, top, right, bottom))
            captcha.save(name)
            return captcha
        
        def open(self):
            """
            打开网页输入用户名密码
            :return: None
            """
            self.browser.get(self.url)
            email = self.wait.until(EC.presence_of_element_located((By.ID, \'email\')))
            password = self.wait.until(EC.presence_of_element_located((By.ID, \'password\')))
            email.send_keys(self.email)
            password.send_keys(self.password)
        
        def get_gap(self, image1, image2):
            """
            获取缺口偏移量
            :param image1: 不带缺口图片
            :param image2: 带缺口图片
            :return:
            """
            left = 60
            for i in range(left, image1.size[0]):
                for j in range(image1.size[1]):
                    if not self.is_pixel_equal(image1, image2, i, j):
                        left = i
                        return left
            return left
        
        def is_pixel_equal(self, image1, image2, x, y):
            """
            判断两个像素是否相同
            :param image1: 图片1
            :param image2: 图片2
            :param x: 位置x
            :param y: 位置y
            :return: 像素是否相同
            """
            # 取两个图片的像素点
            pixel1 = image1.load()[x, y]
            pixel2 = image2.load()[x, y]
            threshold = 60
            if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
                    pixel1[2] - pixel2[2]) < threshold:
                return True
            else:
                return False
        
        def get_track(self, distance):
            """
            根据偏移量获取移动轨迹
            :param distance: 偏移量
            :return: 移动轨迹
            """
            # 移动轨迹
            track = []
            # 当前位移
            current = 0
            # 减速阈值
            mid = distance * 4 / 5
            # 计算间隔
            t = 0.2
            # 初速度
            v = 0
            
            while current < distance:
                if current < mid:
                    # 加速度为正2
                    a = 2
                else:
                    # 加速度为负3
                    a = -3
                # 初速度v0
                v0 = v
                # 当前速度v = v0 + at
                v = v0 + a * t
                # 移动距离x = v0t + 1/2 * a * t^2
                move = v0 * t + 1 / 2 * a * t * t
                # 当前位移
                current += move
                # 加入轨迹
                track.append(round(move))
            return track
        
        def move_to_gap(self, slider, track):
            """
            拖动滑块到缺口处
            :param slider: 滑块
            :param track: 轨迹
            :return:
            """
            ActionChains(self.browser).click_and_hold(slider).perform()
            for x in track:
                ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
            time.sleep(0.5)
            ActionChains(self.browser).release().perform()
        
        def login(self):
            """
            登录
            :return: None
            """
            submit = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, \'login-btn\')))
            submit.click()
            time.sleep(10)
            print(\'登录成功\')
        
        def crack(self):
            # 输入用户名密码
            self.open()
            # 点击验证按钮
            button = self.get_geetest_button()
            button.click()
            # 获取验证码图片
            image1 = self.get_geetest_image(\'captcha1.png\')
            # 点按呼出缺口
            slider = self.get_slider()
            slider.click()
            # 获取带缺口的验证码图片
            image2 = self.get_geetest_image(\'captcha2.png\')
            # 获取缺口位置
            gap = self.get_gap(image1, image2)
            print(\'缺口位置\', gap)
            # 减去缺口位移
            gap -= BORDER
            # 获取移动轨迹
            track = self.get_track(gap)
            print(\'滑动轨迹\', track)
            # 拖动滑块
            self.move_to_gap(slider, track)
            
            success = self.wait.until(
                EC.text_to_be_present_in_element((By.CLASS_NAME, \'geetest_success_radar_tip_content\'), \'验证成功\'))
            print(success)
            
            # 失败后重试
            if not success:
                self.crack()
            else:
                self.login()
    
    if __name__ == \'__main__\':
        crack = CrackGeetest()
        crack.crack()
    View Code            
  • 三、点触验证码的识别

    • 利用在线付费平台(推荐超级鹰https://www.chaojiying.com)
      1. 获取API
        • 在官网下载对应 Python API,链接:https://www.chaojiying.com/api-14.html。使用requests库来实现。
        • 修改后的python3版本的API:
          import requests
          from hashlib import md5
          
          class Chaojiying(object):
              def __init__(self, username, password, soft_id):
                  self.username = username
                  self.password = md5(password.encode(\'utf-8\')).hexdigest()
                  self.soft_id = soft_id
                  self.base_params = {
                      \'user\':self.username,
                      \'pass2\':self.password,
                      \'softid\':self.soft_id,
                  }
                  self.headers = {
                      \'Connection\':\'Keep_Alive\',
                      \'User-Agent\':\'Mozilla/4.0(compatible;MSIE 8.0; Windows NT 5.1; Trident/4.0)\',
                  }
              def post_pic(self, im, codetype):
                  """
                  im: 图片字节
                  codetype: 题目类型参考 http://www.chaojiying.com/price.html
                  """
                  params = {
                      \'codetype\':codetype,
                  }
                  params.update(self.base_params)
                  files = {\'userfile\':(\'ccc.jpg\',im)}
                  r = requests.post(\'http://upload.chaojiying.net/Upload/Processing.php\', data=params, files=files,
                      headers=self.headers)
                  return r.json()
              def report_error(self,im_id):
                  """
                  im_id:报错题目的图片ID 
                  """
                  params = {
                      \'id\':im_id,
                  }
                  params.update(self.base_params)
                  r = requests.post(\'http://Upload.chaojiying.net/Upload/ReportError.php\',data=params, headers= self.headers)
                  return r.json()

          这里定义一个chaojiying类,构造函数接收三个参数,分别是超级鹰用户名、密码以及软件ID,保存以备使用。最重要的一个方法:post_pic(),需要传入图片对象和验证码的代号,该方法会将图片对象和相关信息发给超级鹰后台进行识别,然后将识别成功的JSON返回

      2. 初始化
        • 首先初始化一些变量,如WebDriver、chaojiying对象等,代码实现如下:
          EMAIL = \'Mack01\'
          PASSWORD = \'超级鹰密码\'
          #超级鹰用户名、密码、软件ID、验证码类型
          CHAOJIYING_USERNAME = \'Germey\'
          CHAOJIYING_PASSWORD = \'\'
          CHAOJIYING_SOFT_ID = 893590
          CHAOJIYING_KIND = 9102
          
          class CrackTouClick():
              def __init__(self):
                  self.url = \'http://admin.touclick.com/login.html\'
                  self.browser = webdriver.Chrome()
                  self.wait = WebDriverWait(self.browser,20)
                  self.email = EMAIL
                  self.password =PASSWORD
                  self.chaojiying = Chaojiying(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)
      3. 获取验证码

        • 完善表单,模拟点击呼出验证码,代码:

          def open(self):
              """
              打开网页输入用户名密码 
              return: None 
              """
              self.browser.get(self.url)
              email =self.wait.until(EC.present_of_element_located((By.ID,\'email\')))
              password = self.wait.until(EC.present_of_element_located((By.ID,\'password\')))
              email.send_keys(self.email)
              password.send_keys(self.password)
          
          def get_touclick_button(self):
              """
              获取初始验证按钮
              return: 
              """
              button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,\'touclick-hod-wrap\')))
              return button

          open()方法负责填写表单,get_touclick_button()方法获取验证码按钮,之后触发点击即可。
          接下来,类似极验验证码图像获取一样,获取验证码图片的位置和大小,从网页截图里截取相应 的验证码图片,代码实现如下所示:

          def get_touclick_element(self):
              """
              获取验证图片对象
              return: 图片对象 
              """
              element = self.wait.until(EC.present_of_element_located((By.CLASS_NAME,\'touclick-pub-content\')))
              return element
          def get_position(self):
              """
              获取验证码位置 
              return: 验证码位置元组
              """
              element =self.get_touclick_element()
              time.sleep(2)
              location = element.location
              size = element.size
              top, bottom, left, right = location[\'y\'],location[\'y\']+size[\'height\'],location[\'x\'],location[\'x\']+size[\'width\']
              return (top, bottom, left, right)
          def get_screenshot(self):
              """
              获取网页截图 
              return: 截图对象 
              """
              screenshot = self.browser.get_screenshot_as_png()
              screenshot = Image.open(BytesIO(screenshot))
              return screenshot
          def get_touclick_image(self, name=\'captcha.png\'):
              """
              获取验证码图片
              return: 图片对象
              """
              top,bottom,left,right =self.get_position()
              print(\'验证码位置\',top,bottom, left ,right)
              screenshot = self.get_screenshot()
              captcha = screenshot.crop((left,top,right,bottom))
              return captcha

          get_ touclick_image()方法即为从网页截图中截取对应的验证码图片,其中验证码图片的相对位 置坐标由 get position()方法返回得到。 最后我们得到的是 Image 对象

      4. 识别验证码

        • 调用 Chaojiying 对象的 post_pic()方法,即可把图片发送给超级鹰后台,这里发送的图像是字节 流格式,代码实现如下所示:

          image = self.get_touclick_image()
          bytes_array = BytesIO()
          image.save(bytes_array, format=\'PNG\')
          #识别验证码
          result = self.chaojiying.post_pic(bytes_array.getvalue(),CHAOJIYING_KIND)
          print(result)

           运行之后, result 变量就是超级鹰后台的识别结果。 运行需要等待几秒。

          返回的结果是一个 JSON。 如果识别成功,典型的返回结果如下所示 :
          {’ err_no’:0, \'err_str’: \'OK\', \'pic_id’: ’6002001380949200001\', \'pic_str\' :\'132,127|56, 77 \',\'md5\': \'1f8e1d4bef8b11484cb1f1f34299865b\'}
          其中,pic_str 就是识别的文字的坐标,是以字符串形式返回的,每个坐标都以|分隔。接下来只需要将其解析,然后模拟点击,代码:

          def get_points(self, captcha_result):
              """
              解析识别结果
              param captcha_result: 识别结果 
              return: 转化后的结果
              """
              groups = captcha_result.get(\'pic_str\').split(\'|\')
              locations = [[int(number) for number in group.split(\',\')] for group in groups]
              return locations
          def touch_click_words(self,locations):
              """
              点击验证图片 
              param locations:点击位置 
              return: None
              """
              for location in locations:
                  print(location)
                  ActionChains(self.browser).move_to_element_with_offset(self.get_touclick_element(),location[0],location[1]).click().perform()
                  time.sleep(1)

          这里用 get_points()方法将识别结果变成列表的形式。touch_click_words()方法则通过调用 move_to_element_with_offset()方法依次传入解析后的坐标,点击即可。
          这样就模拟完成坐标的点选,最后点击提交验证的按钮, 等待验证通过,再点击登录按钮即可成功登录。

    四、微博宫格验证码的识别

    • 一般选用全图匹配的方式来进行识别。找到匹配的模板之后,就可以得到事先为模板定义的拖动顺序,然后模拟拖动即可。

    1. 获取模板
      • 需要做一下准备工作,先保存所有的 24 张验证码全图。因为验证码是随机的,一共有 4!= 24 种。可以写一段程序来批量保存验证码图片,然后从中筛选出需要的图片,代码所示:
        import time
        from io import BytesIO
        from PIL import Image
        from selenium import webdriver
        from selenium.common.exceptions import TimeoutException
        from selenium.webdriver.common.by import By
        from selenium.webdriver.support.ui import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC
        
        USERNAME = \'\'
        PASSWORD = \'\'
        
        
        class CrackWeiboSlide():
            def __init__(self):
                self.url = \'https://passport.weibo.cn/signin/login\'
                self.browser = webdriver.Chrome()
                self.wait = WebDriverWait(self.browser,20)
                self.username = USERNAME
                self.password = PASSWORD
        
            def __del__(self):
                self.browser.close()
        
            def open(self):
                """
                打开网页输入用户名密码并点击
                return: None
                """
                self.browser.get(self.url)
                username = self.wait.until(EC.presence_of_element_located((By.ID,\'loginName\')))
                password = self.wait.until(EC.presence_of_element_located((By.ID,\'loginPassword\')))
                submit = self.wait.until(EC.element_to_be_clickable((By.ID,\'loginAction\')))
                username.send_keys(self.username)
                password.send_keys(self.password)
                submit.click()
        
            def get_position(self):
                """
                获取验证码位置
                return: 验证码位置元组
                """
                global img
                try:
                    img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,\'Patt-shadow\')))
                except TimeoutException:
                    print(\'未出现验证码\')
                    self.open()
                time.sleep(2)
                location = img.location
                size = img.size
                top,bottom, left, right = location[\'y\'],location[\'y\']+size[\'height\'],location[\'x\'],location[\'x\']+size[\'width\']
                return (top, bottom, left, right)
        
            def get_screenshot(self):
                """
                获取网页截图
                return: 截图对象
                """
                screenshot = self.browser.get_screenshot_as_png()
                screenshot = Image.open(BytesIO(screenshot))
                return screenshot
        
            def get_image(self, name=\'captcha.png\'):
                """
                获取验证码吗图片
                return: 图片对象
                """
                top, bottom, left, right = self.get_position()
                print(\'验证码位置\', top, bottom,left, right)
                screenshot = self.get_screenshot()
                captcha = screenshot.crop((left, top, right, bottom))
                captcha.save(name)
                return captcha
        
            def main(self):
                """
                批量获取验证码
                return: 图片对象
                """
                count = 0
                while True:
                    self.open()
                    self.get_image(str(count)+\'.png\')
                    count +=1
        if __name__ ==\'__main__\':
            crack = CrackWeiboSlide()
            crack.main()

        只需要挑选出不同的 24 张验证码图片并命名保存。 名称可以直接取作宫格的滑动的顺序,识别过程只需要遍历模板进行匹配。

    2. 模板匹配
      • 调用 get_image()方法,得到验证码图片对象。然后,对验证码图片对象进行模板匹配,定义如下方法:

        from os import listdir
        
        def detect_image(self,image):
            """
            匹配图片
            param image: 图片
            return: 拖动顺序
            """
            for template_name in listdir(TEMPLATES_FOLDER):
                print(\'正在匹配\', template_name)
                template = Image.open(TEMPLATES_FOLDER + template_name)
                if self.same_image(image, template):
                    #返回顺序
                    numbers = [int(number) for number in list(template_name.split(\'.\')[0])]
                    print(\'拖动顺序\',numbers)
                    return numbers

        TEMPLATES_FOLDER 就是模板所在的文件夹。 通过 listdir()方法获取所有模板的文件名称,然后对其进行遍历,通过 same_image()方法对验证码和模板进行比对。 如果匹配成功,就将匹配到的模板文件名转换为列表。例:模板文件 3124.png匹配到,返回结果为[3,1,2,4].

      • 对比的方法实现如下:

        def is_pixel_equal(self, image1, image2, x, y):
                """
                判断两个像素是否相同
                :param image1: 图片1
                :param image2: 图片2
                :param x: 位置x
                :param y: 位置y
                :return: 像素是否相同
                """
                # 获取两个图片的像素点
                pixel1 = image1.load()[x, y]
                pixel2 = image2.load()[x, y]
                threshold = 20
                if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
                        pixel1[2] - pixel2[2]) < threshold:
                    return True
                else:
                    return False
        
            def same_image(self, image, template):
                """
                识别相似验证码
                param image: 待识别验证码
                param template: 模板
                return:
                """
                # 相识度阈值
                threshold = 0.99
                count = 0
                for x in range(image.width):
                    for y in range(image.height):
                        # 判断像素是否相同
                        if self.is_pixel_equal(image, template, x, y):
                            count += 1
                result = float(count) / (image.width * image.height)
                if result > threshold:
                    print(\'功能匹配\')
                    return True
                return False

        在这里比对图片也利用了遍历像素的方法。 same_image()方法接收两个参数,image 为待检测的验证码图片对象,template 是模板对象。由于二者大小是完全一致的,所以在这里遍历了图片的所有像素点。比对二者同一位置的像素点,如果像素点相同,计数就加 1。最后计算相同的像素点占总像素的比例。如果该比例超过一定阈值,那就判定图片完全相同,则匹配成功。这里阔值设定为 0.99, 即如果二者有 0.99 以上的相似比,则代表匹配成功。 依次匹配 24 个模板。 如果验证码图片正常,我们总能找到一个匹配的模板, 这样就可以得到宫格的滑动顺序了。

    3. 模拟拖动

      • 根据滑动顺序拖动鼠标,连接各个宫格,方法实现如下所示:

        def move(self, numbers):
            """
            根据顺序拖动 
            """
            #获得四个按点
            circles = self.browser.find_elements_by_css_selector(\'.patt-wrap. patt-circ\')
            dx = dy =0
            for index in range(4):
                circle = circles[numbers[index]-1]
                #如果是第一次循环
                if index == 0:
                    # 点击第一个按点
                    ActionChains(self.browser)\.move_to_element_with_offset(circle, circle.size[\'width\']/2, circle.size[\'height\']/2 )\.click_and_hold().perform()
                else:
                    #小幅移动次数
                    times = 30
                    #拖动
                    for i in range(times):
                        ActionChains(self.browser).move_by_offset(dx/times, dy/times).perform()
                        time.sleep(1/times)
                # 如果是最后一次循环
                if index == 3:
                    #松开鼠标
                    ActionChains(slef.browser).release().perform()
                else:
                    #计算下一次偏移
                    dx = circles[numbers[index +1]-1].location[\'x\'] - circle.location[\'x\']
                    dy = circles[numbers[index +1]-1].location[\'y\'] - circle.location[\'y\']

        这里方法接收的参数就是宫格的点按顺序,如 [3,1,2,4]。首先利用 find_elements_by_css_selector()方法获取到 4 个宫格元素,它是一个列表形式,每个元素代表一个宫格。接下来遍历宫格的点按顺序,做一系列对应操作。 

        其中如果当前遍历的是第一个宫格,那就直接鼠标点击并保持动作,否则移动到下一个宫格。如果当前遍历的是最后一个宫格,那就松开鼠标,如果不是最后一个宫格,则计算移动到下一个宫格的偏移盘。 

        通过 4 次循环,便可以成功操作浏览器完成宫格验证码的拖拽填充,松开鼠标之后即可识别成功。 鼠标会慢慢从起始位置移动到终止位置。 最后一个宫格松开之后,验证码的识别便完成了。         

    分类:

    技术点:

    相关文章: