【Python】Selenium

528 阅读10分钟

爬虫与反爬虫的斗争

image.png

爬虫建议

  • 尽量减少请求次数

    • 保存获取到的HTML,供查错和重复使用
  • 关注网站的所有类型的页面

    • H5页面
    • APP
  • 多伪装

    • 代理IP(基本不怎么使用)

获取随机的请求头:

代码实例:

from fake_useragent import UserAgent
ua = UserAgent()  # 实例化 UserAgent

for i in range(10):
    headers = {
        'User-Agent': ua.random  # 随机取出一个 UserAgent
    }
    print(headers)

Ajax基本介绍

动态了解HTML技术:

  • JavaScript

    • 是网络上最常用的脚本语言,它可以收集用户的跟踪数据,不需要重载页面直接提交表单,在页面嵌入多媒体文件,甚至运行网页
  • jQuery

    • jQuery是一个快速、简洁的JavaScript框架,封装了JavaScript常用的功能代码
  • Ajax

    • Ajax可以使用网页实现异步更新,可以在不重新加载整个网页的情况下,对网页的某部分进行更新

获取Ajax数据的方式

  1. 直接分析Ajax调用的接口。然后通过代码请求这个接口。
  2. 使用 Selenium+chromedriver 模拟浏览器行为获取数据。

Selenium+chromedriver

selenium介绍:

pip install selenium

Selenium快速入门:

import time
from selenium import webdriver

# 实例化浏览器
driver = webdriver.Chrome()

# 发送请求
driver.get('https://www.baidu.com')

time.sleep(5)

# 退出浏览器
driver.quit()

注意:如果是高版本的 Selenium,程序运行完会立即关闭浏览器窗口。解决:可以通过 time.sleep()设置延迟后再关闭,或者选择低版本的 Selenium。

或者:

from selenium.webdriver import Chrome

chromedriver = "/usr/local/bin/chromedriver"

web = Chrome()
web.get('https://www.baidu.com')

driver 定位元素之代码实例:

import time

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

driver = webdriver.Chrome()
driver.get('http://www.baidu.com')

# 通过 id 值定位
driver.find_element(By.ID, 'kw').send_keys('生产者与消费者模式')

driver.find_element(By.ID, 'su').click()
time.sleep(3)

# 通过 class 值定位
driver.find_element(By.CLASS_NAME, 's_ipt').send_keys('生产者与消费者模式')
time.sleep(3)

# 通过 name 定位
driver.find_element(By.NAME, 'wd').send_keys('生产者与消费者模式')

# 通过 tag_name 定位
tagName = driver.find_elements(By.TAG_NAME, 'input')  # 查找所有input标签
print(tagName)

# 通过 xpath 语法定位
driver.find_element(By.XPATH, '//*[@id="kw"]').send_keys('元森')

# 通过 css 语法定位
driver.find_element(By.CSS_SELECTOR, '#kw').send_keys('可口可乐')

# 通过文本定位 - 精准
driver.find_element(By.LINK_TEXT, '图片').click()

driver 操作表单元素之代码实例:

import time
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()

url = 'https://kyfw.12306.cn/otn/regist/init'
driver.get(url)

"""定位到 select 标签,实例化 select 对象"""
selectTag = Select(driver.find_element(By.ID, 'cardType'))
time.sleep(2)

# 根据属性支定位
selectTag.select_by_value('C')
time.sleep(2)

# 根据索引定位(从0开始)
# selectTag.select_by_index(2)

Selenium 模拟登录豆瓣之代码实例:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

"""模拟登录豆瓣"""

# 加载驱动
driver = webdriver.Chrome()

# 拿到目标url
driver.get('https://www.douban.com/')

""" iframe 是 HTML 的一个标签,作用:文档中的文档
如果要找的标签元素被 iframe 标签嵌套,需要切换 iframe
"""
# 切换 iframe
login_iframe = driver.find_element(By.XPATH, '//*[@id="anony-reg-new"]/div/div[1]/iframe')

driver.switch_to.frame(login_iframe)  # 包含要切换焦点的所有选项的对象

time.sleep(2)

# 1、切换登录方式,定位到 密码登录(这一步之前要确定是否要切换 iframe)
driver.find_element(By.CLASS_NAME, 'account-tab-account').click()
time.sleep(1)

# 2、输入账号密码
user_input = driver.find_element(By.ID, 'username').send_keys('maria')
time.sleep(1)
pwd_input = driver.find_element(By.ID, 'password').send_keys('123456')
time.sleep(2)

# 3、点击登录豆瓣
# 定位登录按钮点击,如果在定位元素时,属性出现空格的状态,一般是选择属性当中的一部分
driver.find_element(By.CLASS_NAME, 'btn-account ').click()
time.sleep(2)

鼠标行为链

有时候在页面中的操作可能要有很多步,那么这时候可以使用鼠标行为链类ActionChains来完成。比如现在要将鼠标移动到某个元素上并执行点击事件。

actions = ActionChains(driver)
actions.move_to_element(inputTag)
actions.send_keys_to_element(inputTag,'python')
actions.move_to_element(submitTag)
actions.context_click()
actions.click(submitTag)
actions.perform()

还有更多的鼠标相关的操作:

练习:

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome()  # 加载驱动
driver.get('https://www.baidu.com')  # 拿到目标URL
inputTag = driver.find_element(By.ID, 'kw')  # 定位到百度输入框
buttonTag = driver.find_element(By.ID, 'su')  # 定位到百度按钮

actions = ActionChains(driver)  # 实例化一个鼠标行为链的对象
actions.send_keys_to_element(inputTag, 'python')  # 在已经定位好的输入框输入内容

time.sleep(1)

# 第一种方法
# 注意你用的逻辑操作和鼠标行为链没有相关 那么这些个操作需要放到perform()的外面
# buttonTag.click()

# 第二张方法 在鼠标行为链中进行操作,没有问题
actions.move_to_element(buttonTag)
actions.click(buttonTag)
actions.perform()  # 提交行为链的操作
time.sleep(2)

Cookie操作

  • 获取所有的cookie
cookies = driver.get_cookies()
  • 根据 cookie 的 name 获取 cookie
value = driver.get_cookie(name)
  • 删除某个cookie
driver.delete_cookie('key')

selenium携带cookie登录qq空间

from selenium import webdriver
from selenium.webdriver.common.by import By
import time
import requests

# 模拟登录qq空间
# 加载驱动
driver = webdriver.Chrome()

driver.get('https://xui.ptlogin2.qq.com/cgi-bin/xlogin?proxy_url=https%3A//qzs.qq.com/qzone/v6/portal/proxy.html&daid'
           '=5&&hide_title_bar=1&low_login=0&qlogin_auto_login=1&no_verifyimg=1&link_target=blank&appid=549000912'
           '&style=22&target=self&s_url=https%3A%2F%2Fqzs.qzone.qq.com%2Fqzone%2Fv5%2Floginsucc.html%3Fpara%3Dizone'
           '&pt_qr_app=%E6%89%8B%E6%9C%BAQQ%E7%A9%BA%E9%97%B4&pt_qr_link=http%3A//z.qzone.com/download.html'
           '&self_regurl=https%3A//qzs.qq.com/qzone/v6/reg/index.html&pt_qr_help_link=http%3A//z.qzone.com/download'
           '.html&pt_no_auth=0')

# 定位头像图片
imgTag = driver.find_element(By.CLASS_NAME, 'face')
imgTag.click()
time.sleep(2)

# 获取cookie
cookie = driver.get_cookies()  # 返回一个列表
# print(cookie, type(cookie))

# 通过for循环打印,发现它是一个列表,里面存放的是字典格式的数据
# for i in cookie:
#     print(i, type(i))

cookie = [item['name'] + '+' + item['value'] for item in cookie]
cookieStr = '; '.join(cookie)
print(cookieStr)
time.sleep(2)

# 目标URL
url = 'https://user.qzone.qq.com/1142667439'
headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/114.0.0.0 Safari/537.36",
    'cookie': cookieStr
}
response = requests.get(url, headers=headers)
time.sleep(1)
with open('qq空间.html', mode='w', encoding='utf-8', newline='') as f:
    f.write(response.text)
print(response.text)

页面等待:

现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个demo元素完全加载出来了。如果实际页面等待时间过长导致某个元素还没出来,但是如果代码直接使用 WebElement,那么就会抛出 NullPointer 的异常。为了解决这个问题。Selenium 提供了三种等待方式:一种是隐式等待、一种是显式等待。

强制等待:【走到某一步,强制执行等待,效率最低】
time.sleep()
隐式等待:调用 driver.implicitly_wait。那么在获取不可用的元素之前,会先等待10秒钟的时间。
driver.implicitly_wait(10)
显示等待:【最常用的,一旦满足条件,就继续往下执行】

表明某个条件成立后才执行获取元素的操作。也可以在等待的时候指定一个最大的时间,如果超过这个时间那么就抛出一个异常。显示等待应该使用selenium.webdriver.support.excepted_conditions 期望的条件和 selenium.webdriver.support.ui.WebDriverWait 来配合完成。

代码实例:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

# 加载驱动
driver = webdriver.Chrome()
driver.get('https://www.baidu.com/')

"""强制等待:
相当于阻塞当前线程,爬虫效率较低,不建议过多使用,会严重影响脚本性能
"""
# 定位输入框
# inputTag = driver.find_element(By.ID, 'kw').send_keys('python')
# time.sleep(3)

"""隐式等待:
只要找到元素就会立即执行,如果找不到会等待;
好处:只需设置一次,全局生效。如果超时时间内网页完成了全部加载,则立即进行下面的操作。
劣势:需要等待网页所有元素都加载完成后才执行下面的操作,
如果需要操作的元素提前加载好了,但是其它无关紧要的元素还没加载完成,那么会浪费时间去等待其它元素加载完成。
"""
# driver.implicitly_wait(6)
# btnTag = driver.find_element(By.ID, 'su')
# btnTag.click()

"""显示等待:
指定某个条件,然后设置最长等待时间
如果在这个时间内还没找到元素,便会抛出异常,只有当条件满足时才会执行后面的代码
好处:解决了隐式等待的不足
缺点:稍微复杂一点,需要学习成本
"""
from selenium.webdriver.support import expected_conditions as EC  # 核心
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

driver.get('https://www.baidu.com/')
try:
    # 直到指定元素加载出来,我只等待5秒
    element = WebDriverWait(driver, 5).until(
        # presence_of_element_located:某个元素已经加载完毕了
        # EC.presence_of_all_elements_located():网页中所有满足条件的元素都加载完成
        # EC.element_to_be_clickable():某个元素可以点击了
        EC.presence_of_element_located((By.XPATH, '//*[@id="su"]'))
    )
    text = driver.page_source  # element的源代码
    print(text)
finally:
    driver.quit()

一些其他的等待条件:

  • presence_of_element_located:某个元素已经加载完毕了。
  • presence_of_all_elements_located:网页中所有满足条件的元素都加载完毕了。
  • element_to_be_clickable:某个元素是可以点击了。

更多条件请参考:selenium-python.readthedocs.io/waits.html

操作多窗口与页面切换:

切换界面:

  • [0] 代表的第一个最开始打开的那个,

  • [1] 代表第二个

  • [-1] 切换到最新打开的窗口

  • [-2] 倒数第二个打开的窗口

# 切换界面
driver.switch_to.window(driver.window_handles[1])

切换至iframe:

# 可以通过By.ID,By.CLASS_NAME...定位
login_iframe = driver.find_element(By.XPATH, '//*[@id="anony-reg-new"]/div/div[1]/iframe')
driver.switch_to.frame(login_iframe)

selenium 执行 js 语句:

有时候 selenium 提供的方法会有一些问题,或者执行起来比较麻烦, 这时使用通过 selenium 执行 js 会简单些。

语法:

execute_script(script, *args)
描述:用来执行js语句
参数 script:待执行的 js 语句,如果有多个js语句,使用英文分号;连接

1、滚动页面:

driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')

2、js点击:

btn = driver.find_element(By.CLASS_NAME, 'my_button')
driver.execute_script('arguments[0].click()', btn)

3、等待元素出现后,再进行交互

element = driver.find_element(By.css_selector,'#my-element')
driver.execute_script('''
    var wait = setInterval(function(){
        if (arguments[0].offsetParent != null){
            clearInterval(wait);
            # 元素现在可见,可以对其进行操作
        }
    }, 100);
''', element)

4、执行自定义 js:

使用 execute_script 方法执行编写的任何自定义的js代码,以完成测试或自动化任务。
driver.execute_script('console.log("hello, world");')

5、打开多窗口

driver.execute_script('window.open("https://www.douban.com")')

selenium 高级语法:

  • page_source:获取 element 源代码
  • find():在 element 源代码中寻找某个字符串是否存在
  • find_element(By.LINK_TEXT):根据链接文本获取,一般处理翻页问题
  • node.get_attribute:node 代表节点名,get_attribute 代表获取的属性名
  • node.text():获取节点的文本内容,包含子节点和后代节点
# page_source:获取 element 源代码
html = driver.page_source
print(html)
"""find():在html源码中查找某个字符是否存在
如果存在,则返回一段数字
若不存在,不会报错,返回 -1
使用场景:翻页爬取
"""
print(html.find('kw'))  # 找得到,则返回一段数字
"""find_element(By.LINK_TEXT, '链接文本')"""
driver.find_element(By.LINK_TEXT, '下一页').click()  # 跳转到下一页
"""node.get_attribute('属性名'),node代表你想获取的节点"""
# url = 'https://movie.douban.com/top250'
# driver.get(url)
# a_tag = driver.find_element(By.XPATH, '//div[@class="item"]/div[@class="pic"]/a')
# print(a_tag.get_attribute('href'))
"""node.text 获取节点的文本内容,包含子节点和后代节点"""
driver.get('https://movie.douban.com/top250')
time.sleep(2)
div_tag = driver.find_element(By.XPATH, '//div[@class="hd"]')
print(div_tag.text)

selenium 设置界面模式:

绝大多数服务器是没有界面的,selenium 控制谷歌浏览器也是存在无界面模式的,简称为无头模式

options = webdriver.ChromeOptions()  # 开启无界面模式

options.add_argument("--headless")  # 配置对象添加开启无界面模式的命令

driver = webdriver.Chrome(options=options)  # 实例化带有配置对象的 driver 对象

driver.get('http://www.baidu.com/')
html = driver.page_source  # page_source:获取 element 源代码
print(html)

time.sleep(2)
driver.quit()

selenium 被识别问题 解决方案:

selenium 做爬虫能解决很多反爬问题,但是 selenium 也有很多特征可以被识别, 比如用 selenium 驱动浏览器后 window.navigator.webdriver 值是 true,而正常运行浏览器该值是未定义的(undefined)。

from selenium import webdriver
import time


# 使用 Chrome 开发者模式
options = webdriver.ChromeOptions()
options.add_experimental_option('excludeSwitches', ['enable-automation'])

# 禁用启用 Blink 运行时的功能
options.add_argument("--disable-blink-features=AutomationControlled")

# selenium执行cdp命令,再次覆盖window.navigator.webdriver的值
driver = webdriver.Chrome(options=options)
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        Object.defineProperty(navigator, 'webdriver', {
                      get: () => undefined
                    })
    """
})
driver.get('https://www.baidu.com/')
time.sleep(2)

综合案例:爬取部分京东的商品数据

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

"""
需求:
拿到对应的价格/书名/评价数量/店铺或者出版社 并将数据保存到csv文件中
第一步 页面结构分析
通过分析每一本书都是一个li标签 那么这页所有的数据都是在一个ul标签里面
京东的数据页面 上来回加载一部分数据(30个) 剩下的一部分数据 通过拖动滚轮然后缓缓的加载出来 (60个)

思路分析:
1 先打开网站 https://www.jd.com/
2 输入内容(爬虫书)
3 点击搜索
4 想办法把滚轮拖动到最后
5 爬取数据/解析数据
6 翻页处理  pn-next disabled
代码实现:
"""


class jd_spider:
    # 初始化
    def __init__(self):
        self.driver = webdriver.Chrome()  # 加载驱动
        self.driver.get('https://www.jd.com/')  # 拿到目标url
        input_tag = self.driver.find_element(By.XPATH, '//*[@id="key"]')
        input_tag.send_keys('爬虫书')
        time.sleep(2)

        btn_tag = self.driver.find_element(By.XPATH, '//*[@id="search"]/div/div[2]/button')
        btn_tag.click()
        time.sleep(2)

    # 解析数据,定义函数
    def parse_html(self):

        # 滑动到页面底部
        self.driver.execute_script(
            'window.scrollTo(0,document.body.scrollHeight)'
        )
        time.sleep(2)

        # 通过抓包定位到商品信息,全部存储在ul标签中的li标签中
        liList = self.driver.find_elements(By.XPATH, '//*[@id="J_goodsList"]/ul/li')
        for li in liList:
            try:
                # 定义一个字典,用于存储拿到的数据
                item = {}
                # 通过xpath定位拿到想要的数据
                item['price'] = li.find_element(By.XPATH, './/div[@class="p-price"]/strong').text.strip()
                item['book_name'] = li.find_element(By.XPATH, './/div[@class="p-name"]/a/em').text.strip()
                item['reviews_num'] = li.find_element(By.XPATH, './/div[@class="p-commit"]/strong').text.strip()
                item['book_name'] = li.find_element(By.XPATH, './/div[@class="p-shopnum"]/a').text.strip()
                print(item)
            except Exception as e:
                print(e)

    def next_html(self):
        while True:
            self.parse_html()
            if self.driver.page_source.find('pn-next disabled') == -1:  # 找不到,不存在时返回-1
                next_tag = self.driver.find_element(By.XPATH, '//*[@id="J_bottomPage"]/span[1]/a[9]')
                next_tag.click()
            else:
                self.driver.quit()
                break


if __name__ == '__main__':
    spider = jd_spider()
    spider.next_html()