爬虫实战:滑动验证码

爬虫实战:滑动验证码

一、目标

破解猪八戒网滑动验证码,实现登录

二、技术点

  • 1.python + selenium自动化
  • 2.python + PIL图像rgb对比
  • 3.模拟人类滑动

三、思路

  • 1.获取块图、缺口图、完整图
  • 2.计算滑块图、缺口图、完整图x坐标
  • 3.计算滑动距离
  • 4.模拟运动

四、环境

python3.6 + selenium + pillow

安装(推荐使用清华源):
   pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple/
   pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple/on

五、代码

# @author: ZLY
# @time: 2020-09-13
# @copytight: All Right Reversed


from time import sleep, time
import random

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from PIL import Image


class SliderVerification:

    def __init__(self):
        start = time()

        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(10)

        # 登录网址、点击验证
        self.driver.get("https://account.zbj.com/login?fromurl=https%3A%2F%2Fchangsha.zbj.com%2Fsem%2Findex%3Fpmcode%3D128434637%26utm_source%3Dbaidu%26utm_medium%3DSEM%26sdclkid%3DALeN152_15fG152Nxg")
        self.driver.maximize_window()
        self.driver.find_element_by_class_name("geetest_radar_tip_content").click()

        # 1. 截取图片
        self.get_img()
        # 2. 计算距离
        distance = self.caculate()

        # 如果验证成功则退出循环
        while True:

            # 3. 生成运动轨迹
            x = self.track(distance)
            # 4. 操作滑块移动
            self.move(x)

            success = self.driver.find_element_by_class_name("geetest_success_radar_tip_content").text
            if success:
                end = time()
                print("校验成功, 验证时长:{:.2f}秒".format(end - start))
                break

            else:

                sleep(3)
                fail_flag = self.driver.find_element_by_class_name("geetest_reset_tip_content")

                # 图被怪兽吃了
                if "点击重试" in fail_flag.text:

                    print("图被怪兽吃了^_^")

                    # 点击重试,重新截图
                    fail_flag.click()
                    self.get_img()

                    distance = self.caculate()

                # 图片拼合错误
                else:
                    print("图片拼合错误……")
                    distance = self.caculate()

        self.driver.close()

    def get_img(self):
        """
            @function: Capture pictures
        """

        # 优化,由于网络加载延迟原因,js加载不到元素会报错,所以允许尝试三次
        for i in range(3):

            # 具体睡几秒取决于网速
            sleep(5)

            try:
                # 截取滑块图
                gap_hide_js = 'document.getElementsByClassName("geetest_canvas_bg")[0].style.display="none";'
                self.driver.execute_script(gap_hide_js)
                self.driver.find_element_by_class_name("geetest_canvas_slice").screenshot("滑块图.png")

                # 截取缺口图
                slider_hide_js = "document.getElementsByClassName('geetest_canvas_slice')[0].style.display = 'none';"
                gap_show_js = "document.getElementsByClassName('geetest_canvas_bg')[0].style.display = 'block';"
                self.driver.execute_script(slider_hide_js + gap_show_js)
                self.driver.find_element_by_class_name("geetest_canvas_bg").screenshot("缺口图.png")

                # 截取缺口图
                full_show_js = "document.getElementsByClassName('geetest_canvas_fullbg')[0].style.display = 'block';"
                self.driver.execute_script(gap_hide_js + slider_hide_js + full_show_js)
                self.driver.find_element_by_class_name("geetest_canvas_fullbg").screenshot("完整图.png")

                # 还原js
                slider_show_js = "document.getElementsByClassName('geetest_canvas_slice')[0].style.display = 'block';"
                full_hide_js = "document.getElementsByClassName('geetest_canvas_fullbg')[0].style.display = 'none';"
                self.driver.execute_script(slider_show_js + gap_show_js + full_hide_js)

                break

            except Exception as e:
                # 格式化输出错误信息
                print(str(e))

    def get_slice_x(self):
        """
            @function: Get the coordinates of the slider graph
            @thinking: Contrast color difference
            @return: X-coordinate of the slider graph
        """

        # 获取滑块图的坐标
        img = Image.open('滑块图.png')
        w, h = img.size

        for x in range(w):

            for y in range(h):

                # 获取rgb
                rgb = img.getpixel((x, y))

                rgb_sum = rgb[0] + rgb[1] + rgb[2]
                if rgb_sum <= 570:
                    return x

    def get_gap_x(self):
        """
            @function: Get the coordinates of the notch graph
            @thinking: Comparing RGB of gap graph and complete graph
            @return: X-coordinate of the notch graph
        """

        # 获取滑块图的坐标
        gimg = Image.open('缺口图.png')
        fimg = Image.open('完整图.png')

        w, h = gimg.size

        for x in range(w):

            for y in range(h):

                grgb = gimg.getpixel((x, y))
                frgb = fimg.getpixel((x, y))

                r = abs(grgb[0] - frgb[0])
                g = abs(grgb[1] - frgb[1])
                b = abs(grgb[2] - frgb[2])

                if r + g + b > 200:
                    return x

    def caculate(self):
        """
            @function: The x-coordinate of the notch graph minus the x-coordinate of the
                       slider graph is used to calculate the sliding distance of the slider
            @return: distance
        """

        slice_x = get_slice_x()
        gap_x = get_gap_x()
        distance = abs(gap_x - slice_x)

        print("滑块图x:%s, 缺口图x:%s, 滑动距离:%s" % (slice_x, gap_x, distance))

        return distance

    def track(self, distance):
        """
            @function: Generating sliding trajectory by Newton's law
            @return: Sliding track ---> list
        """

        tracks = []

        def segmentate(s):

            # 分块操作,对于某块轨迹如果20px,则需要切分成更小的轨迹
            if 20 <= abs(s) < 35:
                tracks.extend([round(s / 2) - 3, round(s / 2) + 3])

            elif 35 <= abs(s) < 50:
                tracks.extend([round(s / 3) - 5, round(s / 3), round(s / 3) + 5])

            elif abs(s) >= 50:
                tracks.extend([
                    round(s / 5) - 5, round(s / 5) - 3,
                    round(s / 5),
                    round(s / 5) + 3, round(s / 5) + 5
                ])
            else:
                tracks.append(round(s))

        def popitem(tracks):
            """解决向左滑过头的问题,正负抵消"""

            # 最大轨迹值列表,用来抵消连续负值
            maxvalue = [0 for _ in range(6)]
            flag = 0

            for i in range(round(len(tracks) * 2 / 3)):
                if tracks[i] < 0:
                    if flag == 7:

                        # 查找连续的6个负值
                        neglist = [tracks.pop(k) for k in range(i - 6, i)]

                        # 查找6个较大的正值
                        for x in range(len(tracks) - 1):

                            if tracks[x] > min(maxvalue):
                                maxvalue[maxvalue.index(min(maxvalue))] = tracks[x]

                        [tracks.remove(maxvalue[j]) for j in range(6)]

                        # 做抵消后的补偿值
                        num = sum(maxvalue) + sum(neglist)
                        segmentate(num)

                        break

                    else:
                        flag += 1
                else:
                    flag = 0

        mid = distance * 8 / 17
        distance += 15

        current = v0 = 0

        while current < distance:
            # 一位随机小数
            t = random.randint(1, 4) / 2

            if current <= mid:
                a = random.randint(1, 3)
            else:
                a = - random.randint(2, 4) / 4

            v0 += a * t
            s = v0 * t + 0.5 * a * t ** 2
            current += s

            segmentate(round(s))

        popitem(tracks)

        # 后退大约15px左右
        for i in range(5):
            tracks.append(- random.randint(2, 4))

        temp = sum(tracks) - (distance - 15)
        # 如果temp < 0, 则滑块还没到目标距离
        if temp < 0:
            segmentate(temp)

        # 如果temp >= 0, 则滑块超过了目标距离
        else:
            segmentate(-temp)

        # 如果轨迹长度小于40或者大于120则重新生成,因为短了无法通过,长了浪费时间
        if len(tracks) >= 80 or len(tracks) <= 40:
            return track(distance - 15)

        else:
            print("轨迹总长:%s,轨迹段数:%s, 轨迹:%s" % (sum(tracks), len(tracks), tracks))
            return tracks

    def move(self, x):
        """
            @function: The slider slides according to the motion track
        """

        # 选中滑块
        element = self.driver.find_element_by_xpath('//div[@class="geetest_slider_button"]')

        # 动作链,按住鼠标不松、滑动、释放鼠标 等于0的位移没必要移动,浪费时间
        ActionChains(self.driver).click_and_hold(element).perform()
        [ActionChains(self.driver).move_by_offset(i, 0).perform() for i in x if i != 0]
        ActionChains(self.driver).release(element).perform()

        # 等待加载,需要等待一会,要不然可能会报莫名其妙的错误
        sleep(3)


SliderVerification()



猜你喜欢

转载自blog.csdn.net/zly717216/article/details/108562512
今日推荐