Selenium-豆瓣自动机器人(三)

其实Selenium 操作浏览器,提交表单,点击事件,切换Iframe,这些都是老生常谈的东西了,在这边我就不赘述了,具体的代码可以参考我github里。

本文主要是说一下多线程与验证码滑块的问题。

知识点有:

  • opencv
  • Qthread 和pyqtSignal


上代码:

import selenium.webdriver as webdriver
from lxml import etree
import random
import time
import requests
import cv2
import numpy as np
from selenium.webdriver.common.action_chains import ActionChains
import pandas as pd
from loguru import logger
from PyQt5.QtCore import QThread, pyqtSignal
import re


class WorkThread(QThread):
    # 使用信号和UI主线程通讯,参数是发送信号时附带参数的数据类型,可以是str、int、list等
    finishSignal = pyqtSignal(str)
    displaySignal = pyqtSignal(str)

    # 带参数示例
    def __init__(self, username, password, url, comment, header, random_target, star, time, excel, blacklist,
                 parent=None):
        super(WorkThread, self).__init__(parent)
        self.username = username
        self.password = password
        self.url = url
        self.comment = comment
        self.header = header
        self.random_target = random_target
        self.star = star
        self.wait_time = time
        self.excel = excel
        self.blacklist = blacklist

    def open_browser(self):
        options = webdriver.FirefoxOptions()
        if self.header == '无头浏览':
            self.displaySignal.emit('不显示浏览器')
            options.add_argument('-headless')
        else:
            self.displaySignal.emit('显示浏览器')
        options.add_argument("--disable-gpu")
        self.displaySignal.emit('正在打开浏览器...')
        self.browser = webdriver.Firefox(options=options, executable_path='geckodriver.exe')
        self.browser.implicitly_wait(10)


这里其实可以看到我已经实例化了两个对象,用作信号传递

finishSignal = pyqtSignal(str) displaySignal = pyqtSignal(str)

而在下面,用了self.displaySignal.emit('正在打开浏览器'),这里后台就会将字符串传给前端窗口,然后就可以在ui界面实时看到这个状态了。



聊一下验证码的问题。很多时候在登陆豆瓣,如果cookie失效或者session过期的情况下,用账户密码登陆需要重新过一次滑块


    def switch_to_iframe(self):
        self.displaySignal.emit('切换至验证码图层...')
        time.sleep(2)
        auth_frame = self.browser.find_element_by_id('tcaptcha_iframe')
        self.browser.switch_to.frame(auth_frame)

    def get_img(self):
        self.displaySignal.emit('获取背景图和滑块图的url...')
        background_image_url = self.browser.find_element_by_id('slideBg').get_attribute('src')
        slider_image_url = self.browser.find_element_by_id('slideBlock').get_attribute('src')
        return background_image_url, slider_image_url

    def download_image(self, img_url, imgname):
        self.displaySignal.emit('以流的形式下载文件...')
        image = requests.get(img_url, stream=True)
        imgName = ''.join(["./", imgname])
        with open(imgName, 'wb') as f:
            for chunk in image.iter_content(chunk_size=1024):  # 循环写入  chunk_size:每次下载的数据大小
                if chunk:
                    f.write(chunk)
                    f.flush()
            f.close()
        self.displaySignal.emit('验证码下载完成')

    def get_image_offset(self):
        back_image = 'back_image.png'  # 背景图像命名
        slider_image = 'slider_image.png'  # 滑块图像命名
        background_image_url, slider_image_url = self.get_img()
        self.download_image(background_image_url, back_image)
        self.download_image(slider_image_url, slider_image)
        # 获取图片并灰度化
        block = cv2.imread(slider_image, 0)
        template = cv2.imread(back_image, 0)
        w, h = block.shape[::-1]
        # print(w, h)
        # 二值化后图片名称
        block_name = 'block.jpg'
        template_name = 'template.jpg'
        # 保存二值化后的图片
        cv2.imwrite(block_name, block)
        cv2.imwrite(template_name, template)
        block = cv2.imread(block_name)
        block = cv2.cvtColor(block, cv2.COLOR_RGB2GRAY)
        block = abs(255 - block)
        cv2.imwrite(block_name, block)
        block = cv2.imread(block_name)
        template = cv2.imread(template_name)
        # 获取偏移量
        # 模板匹配,查找block在template中的位置,返回result是一个矩阵,是每个点的匹配结果
        result = cv2.matchTemplate(block, template, cv2.TM_CCOEFF_NORMED)
        x, y = np.unravel_index(result.argmax(), result.shape)
        #     print(x,y)
        # 由于获取到的验证码图片像素与实际的像素有差(实际:280*158 原图:680*390),故对获取到的坐标进行处理
        offset = y * (280 / 680)
        # 画矩形圈出匹配的区域
        # 参数解释:1.原图 2.矩阵的左上点坐标 3.矩阵的右下点坐标 4.画线对应的rgb颜色 5.线的宽度
        cv2.rectangle(template, (y, x), (y   w, x   h), (7, 249, 151), 2)
        #     show(template)
        # print(offset - 23)
        return offset - 23


这里是过滑块的代码,主要思路就是切换iframe后,通过selenium操作浏览器,将验证码图片下载下来。

之后将图片做二值化处理,并通过cv2模糊匹配,查找缺口的坐标,并且返回矩形右下角的坐标点


    def check_login(self):
        try:
            self.account_info = self.browser.find_element_by_xpath('//a[@class="bn-more"]/span[1]').text
            self.displaySignal.emit(self.account_info)
        except Exception as e:
            self.displaySignal.emit('报错内容为:'   str(e))
            if 'Message: Browsing context has been discarded' in str(e):
                self.displaySignal.emit('浏览器异常,页面断联...')
                self.displaySignal.emit('刷新页面中...')
                self.browser.refresh()
            if 'Message: Unable to locate element:' in str(e):
                self.displaySignal.emit('尚未登录成功...遭遇验证码')
                self.switch_to_iframe()
                self.get_img()
                button = self.browser.find_element_by_id('tcaptcha_drag_thumb')
                # 拖动操作用到ActionChains类,实例化
                action = ActionChains(self.browser)
                action.click_and_hold(button).perform()
                distance = self.get_image_offset()
                # 滑动
                self.displaySignal.emit('正在滑动验证码滑块...')
                action.move_by_offset(xoffset=distance, yoffset=0).perform()
                action.reset_actions()
                time.sleep(5)


然后在每次登陆时判断是否出现滑块,并且通过actionchain拖动滑块多少个x值,这里还是相对简单的:

豆瓣没有对滑块速度进行风控,也就是说我这边拖动是采取的恒定的速度,且是一步到位的。如果做的好一点的网站比如说大众点评或者淘宝,对于滑块拖动,反爬虫都写了一套相关算法去做风控检测,这里就需要在拖动的时候加入一些加速度或者减速,或者说是抖动,尽量模拟一个人的行为。

欢迎技术探讨