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

&&下文是代码和详介,谢谢各位老板莅临检查!有任何问题请留言!如有更好建议也请留言赐教,谢谢!完整代码github地址:https://github.com/luoshuxiao/python-spisers/tree/master/%E9%AA%8C%E8%AF%81%E7%A0%81%E7%A0%B4%E8%A7%A3/%E6%9E%81%E9%80%9F%E6%BC%AB%E7%94%BB--%E6%97%8B%E8%BD%AC%E5%9B%BE%E7%89%87%E9%AA%8C%E8%AF%81%E7%A0%81

本文简介:

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

本爬虫程序功能:

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

功能演示如下:
效果演示~1.gif

准备工作:

需要安装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

下载原始验证码图片(循环requests,300-500张就差不多了).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 = 75


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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容