Selenium 简介

[TOC]

Selenium 脑图

简介

Selenium 是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能——创建回归测试检验软件功能和用户需求。支持自动录制动作和自动生成 .Net、Java、Perl等不同语言的测试脚本。

上述内容是百度百科上对 Selenium 的介绍。

简单来说,Selenium 是一个 Web 应用自动化测试工具,它可以驱动浏览器执行特定动作,完全模拟真实用户对网页进行的操作。

使用 Selenium 甚至可以抓取浏览器 实时 的页面源码,这对于网页中存在异步渲染的页面获取是非常有用的...

环境搭建

Selenium 支持多种主流浏览器的自动化操作,但不同的浏览器必须由其对应的 WebDriver 进行驱动。

Selenium WebDriver 其实代表两层含义:

  • WebDriver 提供了多种语言的编程接口

    其支持的语言有:C#、JavaScript、Java、Python、Ruby...

    :本篇文章主要介绍使用 Selenium 提供的 Python 版本的 API 来操作浏览器。

  • WebDriver 实现了对应浏览器的自动化操作代码

    WebDriver 可以驱动本地浏览器进行自动化操作 ,它使用浏览器厂商提供的自动化操作 API 来操控浏览器和进行自动化测试,这其实就相当于一个真实的用户在操作浏览器。

    更确切地说,WebDriver 会和各个浏览器的驱动进程进行交互,传递控制行为给到本地浏览器进行执行,并将结果返回给我们。

下面介绍下 Selenium 具体的环境搭建流程:

:这里我们使用 Chrome 浏览器进行测试

  1. 下载 Chrome 浏览器

  2. 下载 Chrome 浏览器对应的 WebDriver,并将其设置到系统环境变量上,具体设置方法如下:

    • Windows 平台:以管理者权限打开一个控制台,输入以下内容永久设置环境变量:
    #  表示下载 WebDriver 的目录
    $ setx /m path "%path%;"
    
    • Mac、Linux 平台:打开控制台,输入以下内容:
    #  表示下载 WebDriver 的目录
    $ export PATH=$PATH: >> ~/.profile
    

    最后在控制台中输入chromedriver测试下是否设置成功。

    :下载的 WebDriver 要和 Chrome 浏览器的版本一致。
    Chrome 浏览器的版本可通过在 URL 输入框中输入如下内容进查看:chrome://settings/help

  3. 安装 Selenium WebDriver 的 Python 库(提供通用编程接口):

    # 创建虚拟环境
    $ python -m venv venv
    # 开启虚拟环境(Unix 平台使用:source venv/bin/activate)
    $ venv\Scripts\activate.bat
    # 安装 Seleninum
    $ pip install selenium
    
  4. 输入以下代码进行测试:

    from selenium import webdriver
    
    driver = webdriver.Chrome()
    driver.get('https://www.baidu.com')
    

    执行该文件,可以看到 Chrome 浏览器弹出来并显示百度页面。

以上,Selenium 的环境搭建就完成了。

下面具体介绍下 Selenium 的自动操作接口功能。

创建浏览器驱动对象

因为 Selenium 支持多种主流浏览器的自动化操作,因此其可创建多种不同的浏览器驱动对象,如下所示:

# 创建 Chrome 浏览器驱动对象
from selenium.webdriver import Chrome
# WebDriver 配置在系统环境变量中
driver = Chrome()
# 或者直接指定 WebDriver 路径
driver = Chrome(executable_path='/path/to/chromedriver')

# 创建 Firefox 浏览器驱动对象
from selenium.webdriver import Firefox
with Firefox() as driver:
    #your code inside this indent
# ...

访问网址

Selenium 中模拟浏览器访问网址的接口为:webdriver.get(),如下所示:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get('https://www.baidu.com')
# 查看当前网址
print(f'current_url: {driver.current_url}')
driver.quit()

WebDriver.get(url)方法可以加载访问目标url,比如上述代码,就可以对百度页面进行访问。

:使用WebDriver.get(url)加载网页时,Selenium WebDriver 默认使用的加载策略为normal模式,即WebDriver.get(url)会阻塞直到页面全部加载完毕后,才会执行下去。
更具体来讲,WebDriver.get(url)会等到页面设置document.readyState='complete'且触发load事件后,才会执行下去。
更多详细内容请查看后文:页面加载策略

延时等待

前面我们已经讲过,默认情况下,Selenium WebDriver 使用的页面加载策略为normal模式,也即会阻塞等待直到页面完全加载,这种情况下,页面中的任何元素我们都是可以获取得到的。

但是,如果页面存在元素异步生成的情况,那么我们是存在可能无法直接获取得到生成的元素的。比如:



  
    
  
  

对于上述页面,如果我们直接获取p元素,如下所示:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

driver = webdriver.Chrome()
driver.get('http://127.0.0.1:5500/index.html')

try:
    el = driver.find_element(By.TAG_NAME, "p")
    print(f'locate tag p: {el.text}')
except NoSuchElementException:
    print('failed to locate p element!!')
finally:
    driver.quit()

实际上,我们是无法直接定位到p元素的,因为p元素是使用 JavaScript 在页面完全加载 2 秒后才动态生成的,在我们查找的时间点时还未存在,因此会抛出NoSuchElementException异常。

另一方面,当前主流的 Web 应用架构是 前后端分离,这种架构下,前端涉及到的数据渲染基本上都是采用 Ajax 请求后端数据,然后手动渲染到页面上,因此,我们是无法在页面刚加载完成时,就获取到这些数据的。

针对上述问题,Selenium 采用的解决方案为:延时等待

在 Selenium 中,延时等待 机制具体方案有如下三种:

  • Implicit wait:在隐式等待模式下,WebDriver 在进行元素定位时,如果找不到该定位元素,就会每隔 500ms 轮询该元素,直至找到或者超出隐式等待时间。
    默认情况下,隐式等待时间为0,表示失能隐式等待模式。

    针对上文示例页面,隐式等待解决方案的代码如下所示:

    from selenium import webdriver
    from selenium.common.exceptions import NoSuchElementException
    from selenium.webdriver.common.by import By
    
    driver = webdriver.Chrome()
    # 设置隐式等待 10 秒
    driver.implicitly_wait(10)
    driver.get('http://127.0.0.1:5500/index.html')
    
    try:
        el = driver.find_element(By.TAG_NAME, "p")
        print(f'locate tag p: {el.text}')
    except NoSuchElementException:
        print('failed to locate p element!!')
    finally:
        driver.quit()
    
  • Explicit wait:显示等待模式是定时(每隔 500ms)轮询给定条件,直到条件为真时才进行元素查找,或者等到WebDriverWait设置的超时时间后结束轮询。

    :不要同时使用 隐式等待显示等待,否则可能会造成无法预测的延时等待时间。

    针对上文示例页面,显式等待解决方案的代码如下所示:

    from selenium import webdriver
    from selenium.common.exceptions import NoSuchElementException, TimeoutException
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    
    driver = webdriver.Chrome()
    driver.get('http://127.0.0.1:5500/index.html')
    
    try:
        # 设置 WebDriverWait 的超时时间为 10 秒
        wait = WebDriverWait(driver, timeout=10)
        # 条件为 lambda 表达式
        el = wait.until(lambda d: d.find_element_by_tag_name("p"))
        print(f'locate tag p: {el.text}')
    except TimeoutException:
        print('failed to locate p element!!')
    finally:
        driver.quit()
    

    WebDriver.until(method, message='')会轮询method执行返回值,在规定时间内,method返回真就结束轮询,并把method返回值返回给WebDriver.until(method,message='')方法,所以上述代码中,我们直接让method定位我们需要的元素,成功找到时,until(..)方法就可以直接返回该元素。
    如果超出规定时间,method仍返回false,那么until(..)方法会抛出TimeoutException异常。

    由于显示等待是针对 条件 的判断,在日常使用中,我们的很多操作都需要同步 DOM,因此 Selenium 针对这些常用的操作,预定义了其对应的 预期条件(Expected conditions),对 Python 接口来说,具体包含如下内容:

    预期条件 释义
    alert_is_present 预期出现提示框
    element_located_selection_state_to_be 预期节点元素选择状态
    element_selection_state_to_be 预期节点元素选择状态
    element_located_to_be_selected 预期节点元素为选择状态
    element_to_be_clickable 预期元素可点击(可见+使能)
    element_to_be_selected 预期元素处于选中状态
    frame_to_be_available_and_switch_to_it 预期 frame 可用,同时切换到该 frame 中
    visibility_of 预期节点可见(节点必须已经加载到当前 DOM 上)
    visibility_of_element_located 预期节点可见
    visibility_of_all_elements_located 预期指定的所有节点可见
    visibility_of_any_elements_located 预期至少一个指定的节点可见
    invisibility_of_element 预期节点元素不可见或不存在
    invisibility_of_element_located 预期节点元素不可见或不存在
    new_window_is_opened 预期新开窗口,同时窗口数量增加
    number_of_windows_to_be 预期窗口数量
    presence_of_all_elements_located 预期所有节点加载完成
    presence_of_element_located 预期节点元素加载完成(无需为可见状态)
    staleness_of 等待直到预期元素脱离 DOM
    text_to_be_present_in_element 预期节点文本
    text_to_be_present_in_element_value 预期节点元素 value 值
    title_contains 预期标题包含相关子串(区分大小写)
    title_is 预期标题(完全匹配)
    url_changes 预期 URL 更改
    url_contains 预期当前 URL 包含相关子串(区分大小写)
    url_matches 预期当前 URL 匹配指定模式
    url_to_be 预期当前 URL 内容(完全匹配)

    更多详细内容,请参考:expected_conditions

    采用 预期条件 改写我们上述示例,如下所示:

    from selenium import webdriver
    from selenium.common.exceptions import NoSuchElementException, TimeoutException
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    
    driver = webdriver.Chrome()
    driver.get('http://127.0.0.1:5500/index.html')
    
    try:
        # 设置 WebDriverWait 的超时时间为 10 秒
        wait = WebDriverWait(driver, timeout=10)
        # 条件为 lambda 表达式
        el = wait.until(
            # 预期条件:元素加载完成
            EC.presence_of_element_located(
                (By.TAG_NAME, 'p')
            )
        )
        print(f'locate tag p: {el.text}')
    except TimeoutException:
        print('failed to locate p element!!')
    finally:
        driver.quit()
    
  • FluentWait:使用 FluentWait,我们可以自定义条件等待超时时间,以及条件轮询间隔时间。具体如下:

    driver = Firefox()
    driver.get("http://somedomain/url_that_delays_loading")
    wait = WebDriverWait(driver, 10, poll_frequency=1, ignored_exceptions=[ElementNotVisibleException, ElementNotSelectableException])
    element = wait.until(EC.element_to_be_clickable((By.XPATH, "//div")))
    

    其实从上述代码中,可以看到,Python 中的 FluentWait显示等待 没有区别,这是因为不同语言的接口有些许不同,像FluentWait在 Python 接口中并没有提供,因为使用WebDriver就可以设置超时时间和轮询时间,而 Java 接口就提供了FluentWait类...

元素操作

  • 元素定位:对网页页面的操作,通常操作的都是特定元素,因此对 Html 页面元素的定位是一个十分重要的操作。

    元素定位可分为如下两种定位类型:

    • 单元素定位:获取单个标签元素
    • 多元素定位:获取多个标签元素

    Selenium 提供的元素定位接口如下表所示:

    单元素定位 多元素定位 释义
    find_element_by_id find_elements_by_id 通过元素 id 定位
    find_element_by_class_name find_elements_by_class_name 通过元素类名定位
    find_element_by_tag_name find_elements_by_tag_name 通过元素标签定位
    find_element_by_css_selector find_elements_by_css_selector 通过 css 选择器定位
    find_element_by_xpath find_elements_by_xpath 通过元素 xpath 定位
    find_element_by_name find_elements_by_name 通过元素 name 属性定位
    find_element_by_link_text find_elements_by_link_text 通过元素完整超链接定位
    find_element_by_partial_link_text find_elements_by_partial_link_text 通过元素部分连接定位
    find_element find_elements 通用元素定位接口

    find_element(..)/find_elements(..) 是其他所有元素定位方式的通用接口,具体使用请查看以下示例:

    # index.html
    # 
    Hello Selenium
    # # from selenium import webdriver from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By driver = webdriver.Chrome() driver.get('http://127.0.0.1:5500/index.html') # 单元素定位 try: ele = driver.find_element_by_id('myId') print(f'find_element_by_id ==> {ele.text}') ele = driver.find_element_by_class_name('myClass') print(f'find_element_by_class_name ==> {ele.text}') ele = driver.find_element_by_tag_name('div') print(f'find_element_by_tag_name ==> {ele.text}') ele = driver.find_element_by_css_selector('div#myId') print(f'find_element_by_css_selector ==> {ele.text}') ele = driver.find_element_by_xpath(r'//*[@id="myId"]') print(f'find_element_by_xpath ==> {ele.text}') ele = driver.find_element_by_name('myName') print(f'find_element_by_name ==> {ele.get_attribute("value")}') ele = driver.find_element_by_link_text('selected by link text') print(f'find_element_by_link_text ==> {ele.text}') ele = driver.find_element_by_partial_link_text('link text') print(f'find_element_by_partial_link_text ==> {ele.text}') ele = driver.find_element(By.ID, 'myId') print(f'find_element(By.ID) ==> {ele.text}') ele = driver.find_element(By.CLASS_NAME, 'myClass') print(f'find_element(By.CLASS_NAME) ==> {ele.text}') ele = driver.find_element(By.CSS_SELECTOR, 'div#myId') print(f'find_element(By.CSS_SELECTOR) ==> {ele.text}') ele = driver.find_element(By.XPATH, r'//*[@id="myId"]') print(f'find_element(By.XPATH) ==> {ele.text}') except NoSuchElementException as e: print(e) # 多元素获取 for ele in driver.find_elements_by_css_selector('div.myClass'): print(f'find_elements_by_... ===> {ele.text}') for ele in driver.find_elements(By.CSS_SELECTOR, 'div.myClass'): print(f'find_elements ===> {ele.text}')

    :Selenium WebDriver 中元素类型为WebElement,对元素的操作都封装到该类里面。

  • 属性获取:定位到元素之后,就可以对元素的属性进行获取。

    Selenium WebDriver 中对元素属性获取的通用方式为:WebElement.get_attribute(name),比如:

    # Check if the "active" CSS class is applied to an element.
    is_active = "active" in target_element.get_attribute("class")
    

    另外,对于一些常见的标签属性,Selenium WebDriver 提供了一些更加简便的获取方式,如下所示:

    • WebElement.text:获取元素文本内容
    • WebElement.tag_name:获取元素标签名
    • WebElement.size:获取元素大小
    • WebElement.location:获取元素位置
  • iframe 切换:当前页面嵌套最常用的方式就是 iframe 标签,Selenium 中无法直接获取 iframe 内部页面内容,因此,如果想操作 iframe 内部页面,则需要将驱动切换到要进行操作的 iframe 元素上,切换的方式为:webdriver.switch_to.frame,具体使用方式如下所示:

    • 定位 iframe 元素并进行切换:如下代码所示:

      # 
      # #
      from selenium import webdriver from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time driver = webdriver.Chrome() driver.get('http://127.0.0.1:5500/index.html') iframe = driver.find_element(By.CSS_SELECTOR, '#myFrameID') # 切换到 iframe driver.switch_to.frame(iframe) inputKeyWord = driver.find_element(By.CSS_SELECTOR, '#kw') inputKeyWord.send_keys('Selenium') time.sleep(0.5) searchBtn = driver.find_element(By.CSS_SELECTOR, '#su') searchBtn.click()
    • 通过 ID/名称进行切换:如果 iframe 设置了id获取name属性,则可以直接进行切换,如下所示:

      # 
      # #
      from selenium.common.exceptions import NoSuchFrameException try: # 根据 id 进行切换 driver.switch_to.frame('myFrameID') # 根据 name 进行切换 driver.switch_to.frame('myFrameName') except NoSuchFrameException: pass
    • 通过索引进行切换:在 JavaScript 中,window.frames是一个类数组对象,其存放了当前页面中所有的框架元素,每个元素都是一个Window对象,通过该数组我们可以很方便获取到对应索引的框架对象。同样,Selenium 也支持我们通过索引对框架进行切换,如下所示:

      # 
      # #
      # ... try: # 切换到第 1 个 iframe driver.switch_to.frame(0) # ... except NoSuchFrameException: pass
      • 退出 iframe:对子页面操作完毕后,就可以退出子页面,回到主页面,退出方式如下所示:
      # switch back to default content
      driver.switch_to.default_content()
      

鼠标操作

Selenium WebDriver 中,将有关鼠标操作的接口都封装到 ActionChains 类中,其鼠标操作相关接口如下表所示:

鼠标操作 释义
click 鼠标左键单击
click_and_hold 鼠标左键保持按下状态
context_click 鼠标右键单击
double_click 鼠标左键双击
drag_and_drop 拖放
drag_and_drop_by_offset 拖放到指定偏移位置
move_by_offset 鼠标移动到指定偏移位置
move_to_element 鼠标移动到指定元素中间位置(也起悬停作用)
move_to_element_with_offset 鼠标移动到指定元素上的偏移位置
release 释放鼠标按下状态(即松开鼠标)

以下对几个常用的鼠标操作进行简介:

  • context_click(on_element=None):表示鼠标右击操作。
    当参数on_elementNone时,表示右击鼠标所在位置。
    当参数on_element指定相关元素时,表示右击指定元素。

    from selenium.webdriver.common.action_chains import ActionChains
    
    driver = webdriver.Chrome()
    driver.get('http://127.0.0.1:5500/index.html')
    
    # 最大话窗口
    driver.maximize_window()
    # 获取窗口大小
    width, height = driver.get_window_size().values()
    print(f'[{width},{height}]')
    # 构造动作链
    actions = ActionChains(driver)
    # 移动到窗口中间
    actions.move_by_offset(width/2, height/2)
    # 鼠标右击
    actions.context_click()
    # 执行动作链
    actions.perform()
    

    ActionChains表示一系列动作的组合,前面介绍的接口调用只是将相应的动作描述存储进ActionChains实例内部的一个队列之中,因此最后必须调用perform()函数依序触发实际动作。

  • move_to_element:表示将鼠标移动到指定元素上,实际鼠标此时会其 悬停 作用,这对于那些会响应鼠标悬停的元素十分有用。如下所示:

    #   
    #     
    #   
    #   
    #     
    #
    hover me to show the menu
    # #
    # # from selenium.webdriver.chrome import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By driver = webdriver.Chrome() driver.get('http://127.0.0.1:5500/index.html') ActionChains(driver).move_to_element(driver.find_element(By.CSS_SELECTOR, '#show_menu')).perform()
  • drag_and_drop:表示支持拖放元素。如下所示:

    # 

    Drag the image into the rectangle

    #
    # # from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By driver = webdriver.Chrome() driver.get('http://127.0.0.1:5500/index.html') actions = ActionChains(driver) dragEl = driver.find_element(By.CSS_SELECTOR, '#dragableElement') dropAreaEl = driver.find_element(By.CSS_SELECTOR, '#dropArea') actions.drag_and_drop(dragEl, dropAreaEl) actions.perform()

    drag_and_drop(..)似乎无法在 Html5 上执行,参考:issue。
    如果遇到这种情况,那么可以通过原生 JavaScript 模拟生成鼠标拖拽事件方式进行执行,如下所示:

    driver = webdriver.Chrome()
    driver.get('http://127.0.0.1:5500/index.html')
    
    # format 中使用 {{ 表示转义为一个 {
    drag_and_drop_script = """
    const dataTransfer = new DataTransfer();
    dataTransfer.setData('text/plain', '{sourceId}');
    const target = document.querySelector('{target}')
    target.dispatchEvent(new DragEvent('drop', {{ dataTransfer: dataTransfer }}));
    """.format(sourceId='dragableElement',target='#dropArea')
    
    driver.execute_script(drag_and_drop_script)
    

键盘操作

Selenium WebDriver 中模拟键盘操作提供的接口主要为send_keyskey_downkey_up

  • send_keys:表示发送按键字符。

    对应不同的场景,send_keys来源不同,比如:

    • 对于节点元素的键盘操作,对应的接口为WebDriver.send_keys(*value)
    • 对于执行链的键盘操作,对应的接口为ActionChains.send_keys(*keys_to_send)

    send_keys的参数为输入的键盘字符串,对于特殊按键,比如回车,删除键等,其键盘字符封装到类Keys中。

    下面列举一些常见的键盘操作:

    键盘操作 释义
    send_keys('Selenium 你好') 按键发送字符串Selenium 你好
    sned_keys(Keys.ENTER) 回车键(Enter)
    sned_keys(Keys.BACK_SPACE) 删除键(BackSpace)
    sned_keys(Keys.SPACE) 空格键(Space)
    sned_keys(Keys.TAB) 制表键(Tab)
    sned_keys(Keys.ALT) ALT 键
    sned_keys(Keys.CONTROL) Ctrl 键
    sned_keys(Keys.SHIFT) Shift 键
    sned_keys(Keys.ARROW_DOWN) 方向下键(类似的还有Keys.ARROW_UP/Keys.ARROW_LEFT/Keys.ARROW_RIGHT
    sned_keys(Keys.F1) F1 键(类似的还有F2~F12

    除了特殊字符外,send_keys还支持组合按键,只需将组合按键键值传递给该函数即可,比如:

    组合按键 释义
    send_keys(Keys.CONTROL,'a') 全选(
    send_keys(Keys.CONTROL,'c') 复制(
    send_keys(Keys.CONTROL,'v') 粘贴(

    示例:如下所示,打开网页后按下全选:

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    
    driver = webdriver.Chrome()
    driver.get('https://www.jianshu.com')
    
    html = driver.find_element(By.CSS_SELECTOR, 'html')
    html.send_keys(Keys.CONTROL, 'a')
    
  • key_down:表示按下给定键,并保持按下状态(不释放)。
    该方法应当只用于 Ctrl 键、Alt 键 和 Shift 键的按键操作。

  • key_up:表示释放指定按键。

key_downkey_up只存在于ActionChains中,因此构建按键动作链时,才可以进行使用。

示例:如下所示,打开网页后按下全选:

driver = webdriver.Chrome()
driver.get('https://www.jianshu.com')

# 动作链:按下,按下 a,松开
ActionChains(driver).key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()

浏览器操作

Headless 模式

  • Headless 模式:即无头/无界模式,即不显示本地浏览器,直接以后台运行操作。
    为浏览器设置 Headless 方法很简单,只需传递--headless/headless选项,如下所示:

    from selenium import webdriver
    from selenium.webdriver import ChromeOptions
    options = ChromeOptions()
    options.add_argument('headless')
    
    with webdriver.Chrome(options=options) as driver:
        driver.get('https://baidu.com')
        print(driver.title)
    

    :在很久以前,Selenium 中如果想后台运行自动化操作(即不显示浏览器),通常都是使用 PhantomJS,但后来随着 Chrome 浏览器等提供了 Headless 模式后,现在已经没有必要再使用 PhantomJS 了,PhantomJS 目前也处于不再维护的状态了。

页面源码实时获取

Selenium 虽然被称为 Web 应用程序自动化测试工具,但是它也可以用来进行爬虫抓取,尤其当网页中存在使用 JavaScript 进行异步渲染或者请求接口存在加密数据时,可以借助 Selenium 等待异步渲染完成后,再获取此时的页面数据即可,无需自己去解密加密数据,唯一的缺点就是由于存在渲染等因素,导致速度会比直接解析慢。

前面在 延时等待 章节也介绍过,由于当前主流的 Web 开发架构是 前后端分离,因此前端会存在大量请求后端数据,异步渲染页面等操作。

通常写爬虫,我们都会自己手动去抓取分析这些数据接口,但是有时候有些接口加密了,破解难度比较大,这时就可以借助 Selenium 来完成数据抓取,我们无需关心具体的数据请求过程,只需等到数据渲染到页面上时,直接获取页面数据即可,虽说牺牲了爬取速度,但获取难度直线下降啊。

最后,Selenium WebDriver 中获取当前页面实时源码方法为:WebDriver.page_source

执行 JavaScript 脚本

Selenium 虽然提供了很多 API 供我们操作浏览器,但是还是无法覆盖所有内容。

因此,Selenium 提供了一项本人认为是杀手级功能的特性:执行 JavaScript 脚本

这项功能其实赋予了我们几乎可以完全操控浏览器的功能,Selenium WebDriver 提供的接口为:WebDriver.execute_script(script, *args)

示例:加载网页后,让其垂直滚动到最底部。

from selenium import webdriver

driver = webdriver.Chrome()
driver.get('https://baidu.com')
# window.scrollTo(x,y)
driver.execute_script(f'window.scrollTo(0,{driver.get_window_size()["height"]});')

Cookie 操作

Selenium WebDriver 中对 Cookie 的操作,主要有如下几种:

Cookie 操作 释义
get_cookies() 获取所有 Cookie 信息
get_cookie(name) 获取名称为 name 的 Cookie 信息
add_cookie(cookie_dict) 添加 Cookie
delete_cookie(name) 删除名称为 name 的 Cookie
delete_all_cookies() 清空 Cookie

示例:如下所示:

from selenium import webdriver

with webdriver.Chrome() as driver:
    driver.get('https://baidu.com')

    print('------- Cookie Info (Original) -------------')
    # 打印 Cookie 信息
    print(driver.get_cookies())

    cookie_dict = {
        'name': 'Whyn_cookie_id',
        'value': 'Whyn_cookie_value',
        # 'path': r'/',
        # 'secure':False
        # 'expiry': 162721599
    }
    # 添加 Cookie
    driver.add_cookie(cookie_dict)
    print('------- Cookie Info (Add) -------------')
    for cookie in driver.get_cookies():
        print('''
        {{
            "domain": "{domain}",
            "{name}": {value},
            "path": {path},
            "secure": {secure},
            "expiry": {expiry}
        }}'''.format(
            domain=cookie.get('domain', None),
            name=cookie['name'],
            value=cookie['value'],
            path=cookie.get('path', '/'),
            secure=cookie.get('secure', False),
            expiry=cookie.get('expiry', None)
        ))

    # 删除 Cookie
    driver.delete_cookie('Whyn_cookie_id')
    print('------- Cookie Info (Delete) -------------')
    for cookie in driver.get_cookies():
        print('{"%s":"%s"}' %(cookie['name'],cookie['value']))
    
    # 清空 Cookie
    print('------- Cookie Info (Clear) -------------')
    driver.delete_all_cookies()
    for cookie in driver.get_cookies():
        print('{"%s":"%s"}' %(cookie['name'],cookie['value']))

截屏

通过搜索文档,可以发现,Selenium 提供了以下两种类型的截屏功能:

  • [WebElement.screenshot(filename)][WebElement.screenshot]:该方法可以对元素进行截屏,如下代码所示:

    
    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 selenium.webdriver import ChromeOptions
    
    options = ChromeOptions()
    options.add_argument('headless')
    
    with webdriver.Chrome(options=options) as driver:
        driver.get('https://www.jianshu.com')
        wait = WebDriverWait(driver,10)
        # 文章区域
        el = wait.until(EC.presence_of_element_located(
            (By.CSS_SELECTOR,'#list-container')
        ))
        # 浏览器窗口设置为元素大小,以保证能完全截取元素区域
        driver.set_window_size(el.size['width'],el.size['height'])
        el.screenshot('D:\\jianshu.png')
    
  • WebDriver.save_screenshot(filename):截取浏览器当前页面。具体代码如下所示:

    options = ChromeOptions()
    options.add_argument('headless')
    
    with webdriver.Chrome(options=options) as driver:
        driver.get('https://www.jianshu.com')
        wait = WebDriverWait(driver,10)
        # 文章区域
        wait.until(EC.presence_of_element_located(
            (By.CSS_SELECTOR,'#list-container')
        ))
    
        width = driver.execute_script("return document.documentElement.scrollWidth")
        height = driver.execute_script("return document.documentElement.scrollHeight")
        print(f'page scroll size: {width} x {height}')
        # 将窗口设置为页面滚动宽高
        driver.set_window_size(width, height)
        print('screenshot done') if driver.save_screenshot('D:\\jianshu.png') else print('screenshot failed!!')
    

:截屏时我们需要将窗口的宽高设置为元素/页面滚动宽高,这样就可以完整截取整个元素/页面内容,但一个前提是必须使用 Headless 模式,否则窃取的只是当前视口高度内容。

页面加载策略(Page loading strategy)

Selenium 加载页面时,可选择如下几种模式的加载策略:

  • normal:该模式会让 Selenium WebDriver 等待整个页面全部加载完成,也即 WebDriver 会等待直到页面触发load事件。
    normal模式为 Selenium 默认的页面加载策略。

  • eager:该模式下,Selenium WebDriver 等待直到文档解析完毕,也即页面触发DOMContentLoaded事件,此时页面中图片,CSS 和 子frame 等尚未真正进行加载。

  • none:该模式下,Selenium WebDriver 仅仅等待直到页面下载完成。

    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    options = Options()
    # 设置页面加载策略
    options.page_load_strategy = 'normal'
    driver = webdriver.Chrome(options=options)
    # Navigate to url
    driver.get("http://www.google.com")
    driver.quit()
    

到这里差不多已经把 Selenium 的基本用法都讲述了。

下面介绍几个好用的 Selenium 进阶用法。

巧用 Cookie

  • 复用 Cookie:如果我们使用 Selenium 模拟登录操作,当然是可行的,但是有些登录操作比较复杂,并且现在网站有相当多的登录验证都得人工进行操作才可以(比如图片识别...),用 Selenium 模拟登录通常来说是一个费力不讨好的事情,因为无论多复杂的登录操作,目的就是为了获取得到相应的 Cookie,而 Selenium 是有提供 Cookie 操作的 API 哦,那其实我们完全可以手动进行登录,然后直接从浏览器开发者工具抓取到需要的 Cookie 字符串,设置到 Selenium 中即可。具体代码如下所示:

    # 切割字符串,获取每条 Cookie 键值
    def str2Cookie(cookieStr):
        def getCookieInfo(cookie):
            return cookie.split('=', maxsplit=1)
    
        for cookie in cookieStr.split(';'):
            name, value = getCookieInfo(cookie.strip())
            yield {'name': name, 'value': value}
    
    def imitateLogin(driver):
        # 手动抓取的 Cookie 字符串
        cookieStr = r'__yadk_uid=vJVlSDVQ4aq4hdF3A0DbFmiTdt76cbOB; _ga=GA1.2.1004530019.1590339033; _gid=GA1.2.1890133014.1595150121; remember_user_token=W1syMjIyOTk3XSwiJDJhJDEwJE9XSC5RdnhmNDRKWkRVZS9rRWtrOC4iLCIxNTk1NzM4MDY5LjMxODAxOTIiXQ%3D%3D--7807e1b7a5480d4883e8884a74c2d18dbffb20d9; read_mode=day; default_font=font2; locale=zh-CN; _m7e_session_core=e0ee38cfe3ad01ef5263a1fc7d8e4a26; Hm_lvt_0c0e9d9b1e7d617b3e6842e85b9fb068=1595234295,1595234452,1595754538,1595785636; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%222222997%22%2C%22%24device_id%22%3A%221724796c82722-05b58554ba041f-d373666-1049088-1724796c828e0%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_utm_source%22%3A%22recommendation%22%2C%22%24latest_utm_medium%22%3A%22seo_notes%22%2C%22%24latest_utm_campaign%22%3A%22maleskine%22%2C%22%24latest_utm_content%22%3A%22user%22%2C%22%24latest_referrer_host%22%3A%22%22%7D%2C%22first_id%22%3A%221724796c82722-05b58554ba041f-d373666-1049088-1724796c828e0%22%7D; Hm_lpvt_0c0e9d9b1e7d617b3e6842e85b9fb068=1595789596'
        # 必须先访问页面,然后才能操作 Cookie
        driver.get('https://www.jianshu.com/')
        # 清空 Cookie
        driver.delete_all_cookies()
        # 解析 cookieStr,并添加到 selenium 当前会话的的 Cookie 中
        for cookie in str2Cookie(cookieStr):
            driver.add_cookie(cookie)
        # 刷新当前页面,使 Cookie 生效
        driver.refresh()
            
    if __name__ == '__main__':
        driver = webdriver.Chrome()
        imitateLogin(driver)
    

    结果如下图所示:

    巧用 Cookie
  • 持久化 Cookie:上面内容我们是直接手动获取 Cookie,这种做法可能存在 Cookie 抓取不完全,导致某些页面无法访问。其实更好地做法是对 Cookie 进行持久化,我们只需使用 Selenium 模拟一个登录,然后持久化此时的 Cookie,下次再次登录时,直接加载这些 Cookie,无需进行真实登录操作。具体持久化方法如下所示:

    # 持久化 Cookie
    def saveCookies(cookies,filename='cookies.json'):
        with open(filename,mode='w',encoding='utf-8') as file:
            import json
            file.write(json.dumps(cookies))
    
    # 加载 Cookie
    def loadCookies(filename='cookies.json'):
        cookies = None
        try:
            with open(filename,mode='r',encoding='utf-8') as file:
                import json
                cookies = json.loads(file.read())
        except FileNotFoundError:
            cookies = None
        except PermissionError:
            cookies = None
        return cookies
    
    if __name__ == '__main__':
        driver = webdriver.Chrome()
        # 需要先访问下网址
        driver.get('https://www.jianshu.com/')
    
        # 先获取持久化的 Cookie
        cookies = loadCookies()
        # Cookie 存在
        if cookies is not None:
            # 清空重置 Cookie
            driver.delete_all_cookies()
            # 添加 Cookie
            for cookie in cookies:
                driver.add_cookie(cookie)
            # 刷新一下
            driver.refresh()
        # Cookie 不存在,则进行真实登录
        else:
            # 此处进行真正的登录操作
            # 登录成功后,持久化此时的 Cookie
            saveCookies(driver.get_cookies())
            
        # 现在就是已登录状态
    

请求拦截

很多时候,我们其实需要对请求进行拦截,比如添加自定义请求头等操作,但是,Selenium 原生不提供请求拦截功能。因此,我们只能另辟渠道。

目前使用最多的应该是通过代理服务器对请求进行拦截,因为 Selenium 提供了设置代理的功能,这样就很好地对请求,甚至是响应(其实通过代理基本上什么都能弄,包括上面讲述的 巧用 Cookie _)进行拦截修改。

这里本人使用的代理库为:BrowserMob Proxy

将 BMP 设置为浏览器代理,这样 BMP 就可以对浏览器的所有请求和响应进行捕获,并且也可以将捕获的数据保存到 HAR 文件中,方便查看全部请求过程。
同时,BMP 提供了独立的代理服务,我们可以通过 RESTful API 设置代理服务器。
而如果想扩展更多的功能,BMP 还可以嵌入到我们的代码中进行调用,尤其是结合到 Selenium 代码中非常好用。

这里我们就直接以例子进行讲解,介绍下如何为 Selenium 请求添加自定义头部。

为了能有更大的扩展性,我们就通过代码来进行实现。

:BMP 的独立使用方法可以移步到 附录 区进行查看。

:由于 BMP 是采用 Java 代码进行编写,而我们这里是使用 Python 代码,所以我们这里无法直接将 BMP 嵌入到我们代码中使用。
我这边的做法是新开一个 Java 工程,然后调用 BMP 接口,创建一个代理服务器,具体过程如下所示:

  1. 首先创建一个普通的 Maven Java 工程,如下图所示:

    普通 Maven Java 工程
  2. 在 pom.xml 中导入 BMP 依赖,如下所示:

    
        
        
            1.8
            1.8
        
    
    
        
            
               net.lightbody.bmp
               browsermob-core
               2.1.5
            
        
    
    
  3. 使用以下代码生成证书、密钥和公钥(为了支持 HTTPS 代理):

    // 证书存放在 E:\\cert 目录中
    private static void generateRootCertificate(){
        // create a CA Root Certificate using default settings
        RootCertificateGenerator rootCertificateGenerator = RootCertificateGenerator.builder().build();
    
        // save the newly-generated Root Certificate and Private Key -- the .cer file can be imported
        // directly into a browser
        rootCertificateGenerator.saveRootCertificateAsPemFile(new File("E:\\cert\\certificate.cer"));
        rootCertificateGenerator.savePrivateKeyAsPemFile(new File("E:\\cert\\private-key.pem"), "password");
    
        // or save the certificate and private key as a PKCS12 keystore, for later use
        rootCertificateGenerator.saveRootCertificateAndKey("PKCS12", new File("E:\\cert\\keystore.p12"),
                "privateKeyAlias", "password");
    }
    

    :代码生成证书的详细内容,请参考:mitm module
    :也可以直接下载 BMP 提供的证书:ca-certificate-rsa.cer
    但是还是建议使用代码生成证书,这样更加安全。

  4. 为 Chrome 浏览器添加上一步生成的证书,具体步骤如下所示:

    • 打开页面:chrome://settings/?search=certificate,找到Manage certificates条目
    • 点击Manage certificates条目,弹出证书导入框,选择Trusted Root Certification Authorities标签
    • 选择Import按钮,导入我们生成的证书。
      对于上述代码,我们生成的证书路径为:E:\cert\certificate.cer
    证书导入
  5. 编写代码,创建代理服务器,具体代码如下:

    public class ProxyServer {
    
        public static void main(String[] args) {
    //        ProxyServer.generateRootCertificate();
            BrowserMobProxy proxy = new BrowserMobProxyServer();
            // 代理服务器配置证书公钥(不是代码自动生成的证书可以不用进行设置)
            proxy.setMitmManager(ProxyServer.configKeystore());
            // 全局自定义请求头
            proxy.addHeader("MyHeader", "This is a customed global header");
            // 请求拦截器
            proxy.addRequestFilter(new RequestFilter() {
                @Override
                public HttpResponse filterRequest(HttpRequest request, HttpMessageContents contents, HttpMessageInfo messageInfo) {
                    System.out.println("filter Reqeust  => " + messageInfo.getOriginalUrl());
                    // 拦截请求,添加请求头
                    request.headers().add("FilterHeader", "This is a filter header");
                    return null;
                }
            });
    
            // 响应拦截器
            proxy.addResponseFilter(new ResponseFilter() {
                @Override
                public void filterResponse(HttpResponse response, HttpMessageContents contents, HttpMessageInfo messageInfo) {
                    System.out.println("filter rEsponse......");
                }
            });
    
            // 启动代理服务器
            proxy.start(0);
            // 获取代理服务器端口
            int port = proxy.getPort(); // get the JVM-assigned port
            System.out.println("proxy server running on port " + port);
        }
    
        // 获取公钥内容
        private static ImpersonatingMitmManager configKeystore() {
            CertificateAndKeySource existingCertificateSource =
                    new KeyStoreFileCertificateSource("PKCS12", new File("E:\\cert\\keystore.p12"), "privateKeyAlias", "password");
    
            // configure the MitmManager to use the custom KeyStore source
            ImpersonatingMitmManager mitmManager = ImpersonatingMitmManager.builder()
                    .rootCertificateSource(existingCertificateSource)
                    .build();
    
            return mitmManager;
        }
    }
    

    :上述代码我们通过proxy.addHeader(..)增加了一个全局请求头:MyHeader: This is a customed global header
    通过在请求拦截器中增加了一个请求头:FilterHeader: This is a filter header

  6. 执行上述代码,启动代理服务器,从输入内容中可以查看到代理服务器的端口号,如下图所示:

    proxy server port
  7. Selenium 配置代理,如下代码所示:

    # 注意端口号
    PROXY = '127.0.0.1:3582'
    # 设置代理
    webdriver.DesiredCapabilities.CHROME['proxy'] = {
        'httpProxy': PROXY,
        'sslProxy': PROXY,
        "proxyType": "MANUAL",
    }
    
    driver = webdriver.Chrome()
    driver.get('https://httpbin.org/headers')
    print(driver.page_source)
    

    上述代码执行结果如下图所示:

    proxy result

反爬措施

由于使用 Selenium 可以很方便对网页进行自动化操作,这同时也表示说 Selenium 是一个非常好用的爬虫处理器。

也因此,有一些网站就增加了对 Selenium 的检测,以禁止 Selenium 进行爬虫操作。

目前使用最多的检测 Selenium 爬取的方式为:通过检测浏览器当前页面的window.navigator,如果其包含有webdriver这个属性,那就表示使用了 Selenium 进行爬取,反之,如果检测到webdriverundefined,则表示正常操作。

比如对于网站:https://antispider1.scrape.cuiqingcai.com/

如果我们直接使用 Selenium 进行爬取,如下所示:

driver = webdriver.Chrome()
driver.get('https://antispider1.scrape.cuiqingcai.com/')
print(driver.page_source)

运行上述代码后,可以看到如下结果:

driver.get

而如果想解决这个问题,依据先前我们介绍的原理可知,只要我们能将window.navigator.webdriver设置为undefined即可,
但是我们不能简单地调用如下代码进行设置:

driver.execute_script('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')

因为WebDriver.execute_script(..)是在页面加载完成后才执行,在时序上慢于网页检测。

因此,我们的变量修改必须尽早地在页面加载完成时进行设置。

而 Selenium 中提供的相应机制为:CDP(即 Chrome Devtools-Protocol,Chrome 开发工具协议)
对应的接口为:WebDriver.execute_cdp_cmd(cmd, cmd_args)

对应于上述示例,如果我们想要尽早在页面加载完成时执行我们的 JavaScript 脚本,需要使用的 CDP 命令为:Page.addScriptToEvaluateOnNewDocument,具体的代码如下所示:

options = ChromeOptions()
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)

browser=webdriver.Chrome(options=options)
browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
    'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
})
browser.get('https://antispider1.scrape.cuiqingcai.com/')

附录

  • 各浏览器 WebDriver 下载地址:quick-reference

  • BrowserMob Proxy 独立使用:下面介绍下 BMP 作为独立应用的代理服务类的基本用法,主要有如下内容:

    • 首先下载 BMP 的可执行命令行工具,可以在 releases page 下载或源码编译

    • 进行 BMP 源码bin目录下,可以看到可执行文件browsermob-proxy,输入以下命令,启动 BMP 的 RESTful API:

      # 指定 BMP 运行在 8080 端口
      $ ./browsermob-proxy -port 8080
      
    • 访问 BMP 的/proxy接口,生成一个代理服务器实例:

      $  curl -X POST http://localhost:8080/proxy
        {"port":8081}
      

      :返回的内容{"port":8081}表示代理服务器的端口号
      :到这里,其实我们就可以把 Selenium 设置到代理服务器127.0.0.1:8081

    • 接下来我们就可以根据 BMP 提供的其他 RESTful 服务对我们刚才生成的代理进行设置,以下简单列举设置:

      • 查询 BMP 创建代理数量:对应接口为/proxy,如下所示:
      $ curl -X GET http://localhost:8080/proxy
      {"proxyList":[{"port":8081}]}
      
      • 关闭代理服务器:对应接口为/proxy/[port],如下所示:
      # 关闭端口号为 8081 的代理服务器
      $ curl -X DELETE http://localhost:8080/proxy/8081
      
      • 设置或覆盖代理服务器请求头:接口为/proxy/[port]/headers,如下所示:
      # 设置代理服务器 8081 的请求头
      $ curl -X POST -d "{\"User-Agent\": \"this is my user agent\"}" --header "Content-Type: application/json" http://localhost:8080/proxy/8081/headers
      # 查看设置效果
      $ curl http://httpbin.org/headers --proxy http://localhost:8081
      

      更多 BMP 提供的 RESTful 服务,请参考:REST API

参考

  • 官方文档
  • Selenium - wiki

你可能感兴趣的:(Selenium 简介)