Selenium 操作内部滚动条的方法

在爬取网页数据的过程中,有时候需要操作网页中的滚动条来获取完整的数据。Selenium 可以操作页面中没有完全显示的内容,当页面中有内容在当前可见范围之外时,可以使用滚动操作将其滚动到视图中。
在浏览器中,滚动条有窗口滚动条和内部滚动条两种,窗口滚动条是指浏览器窗口右侧或底部的滚动条,通过拖动滚动条,可以使整个窗口中的内容上下或左右移动,用于滚动整个页面的内容,可以通过鼠标滚轮、键盘上下左右箭头、滚动条本身的拖动等方式进行操作。内部滚动条,在网页内部特定区域中垂直或水平移动,用于滚动该区域内的内容。例如,一个包含很多内容的固定大小的 DIV 元素就可能需要内部滚动条来展示所有内容。

本文以一个获取 Bilibili 视频列表中的内容为例,详细介绍如何操作内部滚动条。

总体框架

通过 Selenium 模拟浏览器访问网页,获取页面中的内容,具体实现过程如下:(1)定义初始化浏览器驱动的函数 initialize_driver(),主要设置浏览器参数和创建 Chrome 浏览器驱动对象 。(2)定义爬取数据的函数 crawl_video() 。(3)定义主函数 main(),组合流程:初始化浏览器驱动、爬取数据、关闭浏览器。

# 导入必要的库
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
import logging

# 定义初始化浏览器驱动的函数
def initialize_driver():
    # 设置浏览器参数
    option = ChromeOptions()
    # 静音播放
    option.add_argument('-mute-audio')
    # 忽略证书错误
    option.add_argument('--ignore-certificate-errors')
    option.add_argument('--ignore-ssl-errors')
    option.add_argument('--ignore-ssl-error')
    # 禁用扩展
    option.add_argument('--disable-extensions')
    # 忽略证书错误,即使访问的网站使用的是不安全的 HTTPS 协议,也可以不做证书验证直接访问
    option.add_experimental_option('excludeSwitches', ['ignore-certificate-errors'])
    # 添加一个参数,该参数的键为 --ignore-certificate-errors-spki-list,值为空字符串。该参数用于忽略证书错误,其中 spki-list 是一种可选参数,表示公钥 pinning 列表,用于验证服务器证书。在这里,空字符串表示不对 pinning 列表进行任何验证,直接忽略所有证书错误。
    option.add_argument('---ignore-certificate-errors-spki-list')
    # 将 Chrome 浏览器的日志级别设置为 "info" 级别
    option.add_argument('log-level=2')
    # 创建Chrome浏览器驱动对象
    driver = webdriver.Chrome(options=option)
    # 最大化浏览器窗口
    driver.maximize_window()
    return driver

def crawl_video(driver, title):
    pass

# 定义主函数
def main(url='https://www.bilibili.com/video/BV1Z4411o7TA?p=1'):
    # 输出当前url
    logging.info("url: {}".format(url))
    # 初始化浏览器驱动
    driver = initialize_driver()
    # 打开网页
    driver.get(url)
    # 调用函数爬取数据
    data = crawl_video(driver, 'Python + Selenium Web自动化 2022更新版教程 自动化测试 软件测试 爬虫')
    # 输出数据
    logging.info('data: {}'.format(data))
    # 关闭浏览器
    driver.quit()

if __name__ == '__main__':
    main()

内部滚动条的处理思路和流程

因为这个 Bilibili 视频列表中的内容随着每个视频逐步点击,滚动条会自动下拉显示页面中不可见的视频章节,所以在这里模拟了直接点击第 31 个视频时内部滚动条循环下拉的情况。

首先用 chapters 查看页面中总共有多少个视频,这里有一个 tricky 的点,获取每个视频元素时,没有直接采用 chapters 来索引每个 chapter ,而是重新定位每个 chapter 元素,因为页面结构可能发生变化,如一直通过 chapters 来索引每个 chapter 元素,可能会发生错误。

接下来尝试通过process_chapter 解析每个视频,获取视频的标题和 url 地址。主要流程如下:

  1. 使用 Selenium 中预定义的条件等待函数 visibility_of_element_located ,根据定位判断目标元素是否已显示,如果已显示,将其存储在 chapter 变量中。
  2. chapter 元素中获取该视频的标题名称和视频链接,并存储在 file_namevideo_url 变量中。
  3. 使用 driver.execute_script 函数模拟点击 chapter 元素。
  4. 将该视频的标题和链接存储在一个字典中,并将该字典作为函数的返回值。

如果处理过程中出现异常,process_chapter 函数中的的视频索引在当前页面视图中不可见时,跳转到 except 模块中的 handle_exception 函数进行处理。主要流程如下:

  1. 输出日志记录当前页面中不可见的视频的索引。
  2. 使用循环模拟下拉滚动条,直到视频可见可被 process_chapter 解析为止。

其中,内部滚动条下拉距离 drag_dist 的控制主要通过 scrollHeightclientHeightscrollTop 三个属性值来控制,使用 driver.execute_script() 方法执行 JavaScript 代码,获取元素的 scrollHeightclientHeightscrollTop 属性值。其中, scrollHeight 表示元素内容的总高度, clientHeight 表示元素在浏览器视图中的高度, scrollTop 表示元素内容向上滚动的高度。

第 31 个视频需要滚动 3 次后才可见,滚动条滚动的信息如下所示:

2023-04-08 14:43:33,280 - INFO: before: scroll_height: 1238, client_height: 348, scroll_top: 0
2023-04-08 14:43:33,280 - INFO: drag_times: 1, drag_dist: 348
2023-04-08 14:43:33,299 - INFO: after: scroll_height: 1238, client_height: 348, scroll_top: 348

2023-04-08 14:43:50,420 - INFO: before: scroll_height: 1238, client_height: 348, scroll_top: 348
2023-04-08 14:43:50,420 - INFO: drag_times: 2, drag_dist: 348
2023-04-08 14:43:50,439 - INFO: after: scroll_height: 1238, client_height: 348, scroll_top: 696

2023-04-08 14:44:01,637 - INFO: before: scroll_height: 1238, client_height: 348, scroll_top: 696
2023-04-08 14:44:01,637 - INFO: drag_times: 3, drag_dist: 194
2023-04-08 14:44:01,656 - INFO: after: scroll_height: 1238, client_height: 348, scroll_top: 890

解析视频和发生异常后滚动条处理的核心代码如下。

def process_chapter(driver, i):
    # chapter = WebDriverWait(driver, 10, 0.5).until(
    #     EC.visibility_of_element_located((By.XPATH,
    #                                       '/html/body/div[2]/div[3]/div[2]/div/div[6]/div[2]/ul/li[' + str(
    #                                           i + 1) + ']/a')))
    # 定位第 31 个视频元素
    chapter = WebDriverWait(driver, 10, 0.5).until(
        EC.visibility_of_element_located((By.XPATH,
                                          '/html/body/div[2]/div[3]/div[2]/div/div[6]/div[2]/ul/li[' + str(
                                              30 + 1) + ']/a')))
    file_name = chapter.get_attribute('title')
    video_url = chapter.get_attribute('href')
    driver.execute_script("return arguments[0].click()", chapter)
    logging.info("{}: {}".format(file_name, video_url))
    return {file_name: video_url}


def crawl_video(driver, title):
    """
    :param driver: webdriver 对象
    :param title: 视频名称
    :return: 包含视频标题和 url 地址的字典
    """

    def handle_exception(e, chapter_index):
        """

        :param e: 异常对象
        :param chapter_index: 视频列表的索引
        :return: 包含视频标题和 url 地址的字典
        """
        logging.exception(e)
        logging.info('invisible_chapter_index in page: {}'.format(chapter_index))
        # jsp_track = WebDriverWait(driver, 10, 0.5).until(EC.presence_of_element_located((By.CLASS_NAME, 'list-box')))
        # total_height = jsp_track.size.get('height')
        # jsp_drag = WebDriverWait(driver, 10, 0.5).until(
        #     EC.presence_of_element_located((By.CLASS_NAME, 'cur-list')))
        # drag_height = jsp_drag.size.get('height')
        # logging.info("total_height: {}, drag_height: {}".format(total_height, drag_height))
        drag_times = 0
        # 循环直到获取到视频信息
        while True:
            # 拖动滚动条使得视频元素可见
            drag_times += 1
            jsp_drag = WebDriverWait(driver, 10, 0.5).until(
                EC.presence_of_element_located((By.CLASS_NAME, 'cur-list')))
            scroll_height = driver.execute_script("return arguments[0].scrollHeight", jsp_drag)
            client_height = driver.execute_script("return arguments[0].clientHeight", jsp_drag)
            scroll_top = driver.execute_script("return arguments[0].scrollTop", jsp_drag)
            logging.info(
                'before: scroll_height: {}, client_height: {}, scroll_top: {}'.format(scroll_height, client_height,
                                                                                      scroll_top))
            drag_dist = min(client_height, scroll_height - client_height - scroll_top)
            logging.info("drag_times: {}, drag_dist: {}".format(drag_times, drag_dist))
            # ActionChains(driver).drag_and_drop_by_offset(jsp_drag, 0, drag_dist).perform()
            # ActionChains(driver).click_and_hold(jsp_drag).move_by_offset(0, drag_dist).perform()
            driver.execute_script('document.querySelector(".{}").scrollTop += {};'.format('cur-list', drag_dist))
            scroll_height = driver.execute_script("return arguments[0].scrollHeight", jsp_drag)
            client_height = driver.execute_script("return arguments[0].clientHeight", jsp_drag)
            scroll_top = driver.execute_script("return arguments[0].scrollTop", jsp_drag)
            logging.info(
                'after: scroll_height: {}, client_height: {}, scroll_top: {}'.format(scroll_height, client_height,
                                                                                     scroll_top))
            time.sleep(WAIT_TIME * random.random() * 2)
            try:
                chapter_data = process_chapter(driver, chapter_index)
                break
            except Exception as e:
                logging.exception(e)
                continue
        return chapter_data

    try:
        chapters = WebDriverWait(driver, 10, 0.5).until(
            EC.presence_of_all_elements_located((By.XPATH, '//a[@class="router-link-active"]')))
        logging.info('len(chapters): {}'.format(len(chapters)))
        chapters_list = []
        for chapter_index, item in enumerate(chapters):
            try:
                chapter_data = process_chapter(driver, chapter_index)
                chapters_list.append(chapter_data)
                time.sleep(WAIT_TIME * random.random() * 2)
            except Exception as e:
                chapter_data = handle_exception(e, chapter_index)
                chapters_list.append(chapter_data)
                time.sleep(WAIT_TIME * random.random() * 2)
        return {title: chapters_list}
    except Exception as e:
        logging.exception(e)

微信公众号「padluo」,分享数据科学家的自我修养,既然遇见,不如一起成长。关注【数据分析】公众号,后台回复【文章】,获得整理好的【数据分析】文章全集。

数据分析二维码.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值