在爬取网页数据的过程中,有时候需要操作网页中的滚动条来获取完整的数据。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 地址。主要流程如下:
- 使用 Selenium 中预定义的条件等待函数
visibility_of_element_located
,根据定位判断目标元素是否已显示,如果已显示,将其存储在chapter
变量中。 - 从
chapter
元素中获取该视频的标题名称和视频链接,并存储在file_name
和video_url
变量中。 - 使用
driver.execute_script
函数模拟点击chapter
元素。 - 将该视频的标题和链接存储在一个字典中,并将该字典作为函数的返回值。
如果处理过程中出现异常,process_chapter
函数中的的视频索引在当前页面视图中不可见时,跳转到 except
模块中的 handle_exception
函数进行处理。主要流程如下:
- 输出日志记录当前页面中不可见的视频的索引。
- 使用循环模拟下拉滚动条,直到视频可见可被
process_chapter
解析为止。
其中,内部滚动条下拉距离 drag_dist
的控制主要通过 scrollHeight
、 clientHeight
和 scrollTop
三个属性值来控制,使用 driver.execute_script()
方法执行 JavaScript 代码,获取元素的 scrollHeight
、 clientHeight
和 scrollTop
属性值。其中, 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」,分享数据科学家的自我修养,既然遇见,不如一起成长。关注【数据分析】公众号,后台回复【文章】,获得整理好的【数据分析】文章全集。