python爬虫--破解旋转图片验证码(selenium+Chr

作者: 222AblackA | 来源:发表于2019-02-24 17:56 被阅读9次

    &&下文是代码和详介,谢谢各位老板莅临检查!有任何问题请留言!如有更好建议也请留言赐教,谢谢!&&

    本文简介:

    主要用selenium模块模拟用户登陆网站,编写判断两张图片相匹配的算法以及破解旋转图片验证码的总体思路,各个局部功能介绍,比如下载图片、切图,图片去重,起浏览器模拟用户登陆等,都有附上相应的详细代码,组装起来就是实现登录的完整代码

    本爬虫程序功能:

    功能 - ->> 用Chrome浏览器自动登录某漫画网站,破解旋转的图片验证码。难点有两个:一是编写图片匹配算法,判断两张图片是否相同,二是要想到破解旋转图片验证码的解决思路;
    成功破解的效果 - - >> 网站会自动打开,自动点击头像,弹出登录框,自动填写账号密码,自动点击图片验证码旋转,正确后自动点击登录。

    准备工作:

    需要安装Chrome浏览器,以及与浏览器版本相对应的chromedriver(https://npm.taobao.org/mirrors/chromedriver),配置相对应的环境变量;当然,肯定得有python,以及相应的虚拟环境,安装相应的第三方库(selenium,requests等)
    在创建python项目配好虚拟环境后,需要创建的文件有(具体如何存放可以自行定义):
    1.静态文件夹static(放图片)
    2.保存下载的原始验证码图片的文件夹jisu_images(在static文件夹下)
    3.保存原始图片剪切下来的小图文件夹ringht_img(在static文件夹下)
    4.保存去掉重复后的正确状态最终的图片验证码资源文件夹imgs(在static文件夹下)
    5.创建py文件jisumanhua_rotate_picture.py,编写主要程序
    6.创建py文件compare_picture.py,编写图片匹配算法
    如下截图(蓝色部分):

    创建文件夹.png

    代码说明:

    本程序是基于python3,用pycharm开发的爬虫程序,包括图片匹配算法和模拟用户登陆两个py文件,模拟用户登陆文件中的代码并没有拆分出获取图片、裁剪图片以及下载等等功能进行模块化,不完全符合高内聚低耦合的编程思想,但已进行函数式编码处理;

    功能实现的整体思路:

    第一步:从网站requests图片验证码资源
    用开发者工具查看原始验证码的url地址(蓝色部分div标签中).png
    原始验证码图片.png
    第二步:crop处理原始验证码图片,处理后保存到本地
    从原始验证码图片上切下来的图片.png

    注意:截图中切下来的图片还没有去掉重复的图片,可以先让程序去重,再进行手动旋正,再次去重得到最后的图片(哎,我是没去重,先手动旋正1200张图片,再去重,花了我20来分钟)

    第三步:用selenium模拟用户打开chrome浏览器进入该漫画网站
    第四步:用selenium模块实现自动填写账户密码
    第五步:用selenium模块切下网页截图(包含验证码)

    程序自动截图,类似下图:

    网页截图(包含验证码).png
    第六步:用PIL模块切出网页中的验证码

    效果如下截图:

    网页截图中切出来的验证码.png
    第七步:用自己写的图片匹配算法将网页上切下来的验证码图片与本地图片验证码资源相匹配
    第八步:匹配成功自动点击登录
    第九步: 匹配不成功,自动点击换一组继续匹配

    整体代码布局(没按严格顺序排版,请见谅):

    布局截图(便于截图,尽量紧凑的放在了一起,请忽略PEP8),下面一张是剩下部分(屏幕太小,一屏截不完整,尴尬):

    代码布局1.png 代码布局2.png

    具体编码实现:

    一:从网站获取原始验证码资源:

    def get_picture():
        """下载300张该漫画网站的图片验证码原始图片"""
        url = 'http://www.1kkk.com//image3.ashx?t=1550843570000'
        headers = {
            'Referer': 'http://www.1kkk.com/',
            'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Mobile Safari/537.36'
        }
        for i in range(300):
            response = requests.get(url, headers=headers)
            with open('./static/jisu_images/img' + str(i), 'wb') as f:
                f.write(response.content)
                print(i)
    

    二:编写图片匹配算法:

    # 声明一个图片类,对象属性是图片的PIL库的Image对象
    # compare对象方法可以判断两张图片是否一样,但是并不能百分百判断正确,正确率在百分之九十左右
    
    # 设置判断两张图片相匹配的最低匹配度
    compatible = 70
    
    
    class PictureHash(object):
        """图片类"""
        def __init__(self, obj):
            """param obj: 图片的Image对象"""
            self.obj = obj
    
        def compare(self, other_picture):
            """
            比较两个图片是否一样
            :param other_picture: 另一张图片的PictureHash对象
            :return: 布尔值(1表示相同,0表示不同)
            """
            dist = 0
            hash_s = self.get_hash()
            hash_o = other_picture.get_hash()
            for i in range(0, len(hash_s)):
                if hash_s[i] == hash_o[i]:
                    dist = dist + 1
            if dist >= compatible:
                return 1
            else:
                return 0
    
        def get_gray(self):
            """
            获取图片灰度集合
            :return: 灰度列表
            """
            a = self.change_size_con()
            tmpls = []
            for h in range(0, a.size[1]):
                for w in range(0, a.size[0]):
                    tmpls.append(a.getpixel((w, h)))
            return tmpls
    
        def change_size_con(self):
            """
            改变图片大小,转255灰度图(0到255的值)
            :return: 新图片Image对象
            """
            return self.obj.resize((12, 12)).convert("L")
    
        def get_hash(self):
            """生成图片哈希表"""
            bitls = ''
            a = self.change_size_con()
            b = self.get_gray()
            avg = sum(b) / len(b)
            for h in range(1, a.size[1] - 1):
                for w in range(1, a.size[0] - 1):
                    if a.getpixel((w, h)) >= avg:
                        bitls = bitls + '1'
                    else:
                        bitls = bitls + '0'
            return bitls
    

    三:处理原始验证码图片(切下所有图片后,需要手动将所有图片旋转到正确状态,然后再去除重复的图片,因为程序无法识别那张图是正确状态)

    def split_picture(img_path):
        """
        从原始验证码中切出有效图片验证码
        :param img_path: 原始验证码图片地址
        :return: 有效验证码Image对象
        """
        img_file = Image.open(img_path)
        width = int(img_file.size[0])
        height = int(img_file.size[1])
        img1 = img_file.crop((0, 0, width/4, height/4))
        img2 = img_file.crop((width/4, 0, width/2, height/4))
        img3 = img_file.crop((width/2, 0, 3*width/4, height/4))
        img4 = img_file.crop((3*width/4, 0, width, height/4))
        return [img1, img2, img3, img4]
    
    def save_img(img_obj_list, file_name):
        """
        保存图片
        :param img_obj_list: 图片Image对象
        :param file_name: 目标文件夹名
        """
        num = 1
        for img_obj in img_obj_list:
            img_obj.save(f'./static/{file_name}/img{num}.png')
            num += 1
    
    def split_all_picture(file_name):
        """
        剪切文件中所有原始验证码图片,并保存
        :param file_name: 图片存放地址
        """
        img_list = os.listdir('./static/jisu_images/')
        for img in img_list:
            imgs = split_picture('./static/jisu_images/' + img)
            save_img(imgs, file_name)
    
    def compare_picture(picture1, picture2):
        """
        判断图片是否相同
        :param picture1: 第一张图片Image对象
        :param picture2: 第二张图片Image对象
        :return: 布尔值(True表示相同,False表示不同)
        """
        img1 = PictureHash(picture1)
        img2 = PictureHash(picture2)
        if img1.compare(img2):
            return 1
        else:
            return 0
    
    def del_repeat():
        """
        去除重复图片
        :return: 图片Image对象列表
        """
        right_list = os.listdir('./static/right_img/')
        img1 = Image.open('./static/right_img/img1.png')
        result_right_img = [img1]
        for img in right_list[1:]:
            img = Image.open(f'./static/right_img/{img}')
            for right_img in result_right_img[:]:
                return_img = compare_picture(img, right_img)
                if return_img:
                    break
            else:
                result_right_img.append(img)
        return result_right_img
    

    四:selenium+chrome模拟用户打开网站并登陆

    # 声明浏览器对象
    browser = webdriver.Chrome()
    # 设置网页大小(可修改)
    browser.set_window_size(1400, 700)
    # 设置网页最大等待时长(可修改)
    wait = WebDriverWait(browser, 5)
    # 登录的账号、密码(请自行修改)
    username = '#############'
    password = '#############'
    # 设置点击换一组验证码的最大点击次数(可修改)
    max_change_times = 3
    # 设置点击登录却登录不上的最大点击次数(可修改)
    max_login_times = 3
    
    
    def simulate_user():
        """模拟用户登陆极速漫画网站"""
        url = 'http://www.1kkk.com'
        browser.get(url)
        # 点击头像登录,进入图片验证码界面
        img_login = wait.until(EC.element_to_be_clickable((By.XPATH, '//img[@class="header-avatar"]')))
        img_login.click()
        time.sleep(1)
        #  填入账户和密码
        input_user = wait.until(EC.presence_of_element_located((By.XPATH, '//input[@name="txt_name"]')))
        input_password = wait.until(EC.presence_of_element_located((By.XPATH, '//input[@name="txt_password"]')))
        input_user.send_keys(username)
        time.sleep(1)
        input_password.send_keys(password)
        global login_num  # 点击登录却登录不上的次数
        login_num = 0
        global change_num  # 点击换一组的次数
        change_num = 0
        while change_num < max_change_times and login_num < max_login_times:
            time.sleep(1)
            # 获取网站页面截图(有验证码)
            screen_img = browser.get_screenshot_as_png()
            # 将byte类型数据转换成Image对象
            screen = Image.open(BytesIO(screen_img))
            # 定位并获取4个图片验证码节点元素
            pictures = [wait.until(
                EC.element_to_be_clickable(
                    (By.XPATH, f'//div[@class="form-wrap"]/div//div[{i+2}]'))) for i in range(4)]
            picture_num = 1
            for picture in pictures:
                img = get_cut_img(screen, picture)
                times = confirm_click_times(img, picture_num)
                picture_num += 1
                if click_picture(picture, times):
                    #  图片正确,结束本次循环,换下一张图片
                    continue
                else:
                    # 每点一次换一组,统计次数加一
                    change_num += 1
                    # 结束for循环,回到while循环开始地方开始执行
                    break
            else:
                # for循环正常结束,说明验证码已经完全正确,点击登录
                login = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#btnLogin')))
                login.click()
                login_num += 1
                time.sleep(2)
                if check_login_status():
                    break
    
    其中,实现功能的函数封装:

    1.从需要点击的验证码的网页截图中切出验证码图片

    def get_cut_img(screen_img, img):
        """
        剪切网页上的验证码图片
        :param screen_img: 整个网页大图(需包含验证码)
        :param img: 图片验证码在网页中的节点对象
        :return: 切下的图片数据
        """
        location = img.location
        size = img.size
        cut_image = screen_img.crop(
            (location['x'], location['y'], location['x'] + size['width'], location['y'] + size['height']))
        return cut_image
    

    2.匹配验证码图片,计算要得到正确验证码应该点击的次数

    def confirm_click_times(browser_img, picture_num):
        """
        确认图片验证码点击次数
        :param browser_img: 图片验证码Image对象
        :return: 点击次数(-1除外,-1表示无法在本地资源匹配验证码)
        """
        i = 0
        browser_p = PictureHash(browser_img)
        right_imgs = os.listdir('./static/imgs/')
        for img_path in right_imgs:
            i += 1
            img = Image.open('./static/imgs/' + img_path)
            if PictureHash(img).compare(browser_p):
                print(f'第{picture_num}张验证码与imgs文件中的第{i}张图片匹配,并且不用点击')
                return 0
            if PictureHash(img.rotate(90)).compare(browser_p):
                # 本地资源图片逆时针旋转90度(相当于验证码点击一次)
                print(f'第{picture_num}张验证码与imgs文件中的第{i}张图片匹配,模拟点击一次')
                return 1
            if PictureHash(img.rotate(180)).compare(browser_p):
                # 本地资源图片逆时针旋转180度(相当于验证码点击二次)
                print(f'第{picture_num}张验证码与imgs文件中的第{i}张图片匹配,模拟点击两次')
                return 2
            if PictureHash(img.rotate(270)).compare(browser_p):
                # 本地资源图片逆时针旋转270度(相当于验证码点击三次)
                print(f'第{picture_num}张验证码与imgs文件中的第{i}张图片匹配,模拟点击三次')
                return 3
        else:
            print(f'第{picture_num}张验证码在imgs中没有相匹配的资源,'
                  f'也可能是图片匹配算法对这张图片无效,模拟点击换一组')
            if change_num < max_change_times:
                print('正在尝试换一组验证码进行匹配,请稍等!...')
            return -1
    

    3.点击图片验证码,让其变正确

    def click_picture(picture, times):
        """
        点击图片验证码
        :param picture: 图片验证码节点元素
        :param times: 点击次数
        :return: 布尔值(1代表图片已点击至正确状态,0代表无法匹配图片)
        """
        if times == 0:
            return 1
        elif times == -1:
            # 点击换一组验证码
            change_img = wait.until(EC.element_to_be_clickable((By.XPATH, '//a[@class="rotate-refresh"]')))
            change_img.click()
            return 0
        else:
            # 点击相应的次数,让验证码变正确
            for _ in range(times):
                picture.click()
            return 1
    

    4.检查登录的状态,看账号是否已经登录成功

    def check_login_status():
        """
        查看是否登录成功
        :return: 布尔值(1代表已登录,0代表未登录)
        """
        try:
            # 如果网页右上方头像标签有onmouseover="getuserinfo(this);"属性,说明已登陆
            wait.until(EC.presence_of_element_located((By.XPATH, '//img[@onmouseover="getuserinfo(this);"]')))
            print('恭喜,成功登录!')
            return 1
        except Exception as e:
            print('图片匹配成功,但登录不了')
            print('可能原因一:图片匹配算法不能百分之百确认两张图片是否匹配,存在算法漏洞;')
            print('可能原因二:账号或者密码错误。')
            if login_num < max_login_times:
                print('正在重新尝试!请稍后...')
            return 0
    

    五:主函数如下(主函数中实现多个功能,并没有分开,将不需要执行的功能注释掉就可以了)

    def main():
        start_time = time.time()
        # # 从网站下载验证码到本地保存到jisu_images
        # get_picture()
    
        # # 切图,并存入right_img
        # split_all_picture('right_img')
    
        # # 去掉旋转到正确位置后,重复的图片,并保存到imgs
        # result_list = del_repeat()
        # save_img(result_list, 'imgs')
    
        # 模拟用户访问网站,自动破解验证码并登陆
        simulate_user()
        end_time = time.time()
        print(f'''
        判断图片相匹配的最低匹配度设置为:{compatible}%
        模拟点击换一组的最大次数设置为:{max_change_times}次
        模拟点击登录的最大次数设置为:{max_login_times}次
        模拟点击换一组次数:{change_num}次
        模拟点击登录次数:{login_num}次
        程序主动休眠时间:{5+change_num+3*login_num}s
        程序实现该功能的运行时间:{end_time-start_time}s
        ''')
        if change_num == max_change_times:
            print(f'已点击{max_change_times}次换一组(程序设置的极限),依然无法破解,'
                  f'请重启爬虫,或者降低图片匹配算法的匹配度,或者下载适量该网站的原始验证码图片,处理后,更新到本地图片资源库imgs文件夹中')
        if login_num == max_login_times:
            print(f'已点击{max_login_times}次登录((程序设置的极限)),依然不能登录,'
                  f'请检查账号、密码是否正确,或者重启爬虫程序, 或者优化图片匹配算法')
    
    
    if __name__ == '__main__':
        main()
    

    需要导入的库如下(compare_picture是自己编写的图片匹配算法,单独放在一个py文件中,所以需要导入):

    import time
    import os
    import requests
    
    from io import BytesIO
    from PIL import Image
    
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    from compare_picture import *
    

    网站登录成功后,Terminal黑屏终端最后的输出结果如下: 第一次模拟登录就成功后Terminal输出结果.png

    点击换一组后成功登录Terminal的输出情况.png

    相关文章

      网友评论

        本文标题:python爬虫--破解旋转图片验证码(selenium+Chr

        本文链接:https://www.haomeiwen.com/subject/lnqbyqtx.html