Day 22 22.1 Web自动化之selenium&pyppeteer

发布时间 2023-04-06 11:18:51作者: Chimengmeng

web自动化

  • 随着互联网的发展,前端技术也在不断变化,数据的加载方式也不再是单纯的服务端渲染了。
    • 现在你可以看到很多网站的数据可能都是通过接口的形式传输的,
    • 或者即使不是接口那也是一些 JSON 的数据,然后经过 JavaScript 渲染得出来的。
  • 这时,如果你还用 requests 来爬取内容,那就不管用了。
    • 因为 requests 爬取下来的只能是服务器端网页的源码,这和浏览器渲染以后的页面内容是不一样的。
    • 因为,真正的数据是经过 JavaScript 执行后,渲染出来的,数据来源可能是 Ajax,也可能是页面里的某些 Data,或者是一些 ifame 页面等。
    • 不过,大多数情况下极有可能是 Ajax 接口获取的。
  • 所以,很多情况我们需要分析 Ajax请求,分析这些接口的调用方式,通过抓包工具或者浏览器的“开发者工具”,找到数据的请求链接,然后再用程序来模拟。
    • 但是,抓包分析流的方式,也存在一定的缺点。
    • 因为有些接口带着加密参数,比如 token、sign 等等,模拟难度较大;
  • 那有没有一种简单粗暴的方法,
    • 这时 Puppeteer、Pyppeteer、Selenium、Splash 等自动化框架出现了。
    • 使用这些框架获取HTML源码,这样我们爬取到的源代码就是JavaScript 渲染以后的真正的网页代码,数据自然就好提取了。
    • 同时,也就绕过分析 Ajax 和一些 JavaScript 逻辑的过程。
    • 这种方式就做到了可见即可爬,难度也不大,同时适合大批量的采集。
  • Selenium:
    • 作为一款知名的Web自动化测试框架,支持大部分主流浏览器,提供了功能丰富的API接口,常常被我们用作爬虫工具来使用。
    • 然而selenium的缺点也很明显
      • 速度太慢
      • 对版本配置要求严苛
      • 最麻烦是经常要更新对应的驱动

(一)selenium

【1】安装

  • 由于sleenium4.1.0需要python3.7以上方可支持,请注意自己的python版本。

方式一:pip安装

  • Python3.x安装后就默认就会有pip(pip.exe默认在python的Scripts路径下),
  • 打开 cmd,使用pip安装。

pip install selenium

  • 首次安装会有进度条,而且装出来是多个包(依赖于其他第三方库)。
  • 如果安装慢(默认连接官网),可以指定国内源。

pip install selenium -i https://mirrors.aliyun.com/pypi/simple/

方式二:Pycharm安装

  • Pycharm-File-Setting-Project:xxxx-Python Interpreter,点击+号

chrome驱动

  • 根据你电脑的不同自行选择吧.
    • win64选win32即可.
    • 把你下载的浏览器驱动放在python解释器所在的文件夹

【2】quick start

  • selenium最初是一个自动化测试工具,

    • 而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题
    • selenium本质是通过驱动浏览器,完全模拟浏览器的操作,
    • 比如跳转、输入、点击、下拉等,来拿到网页渲染之后的结果,可支持多种浏览器
  • 安装pip包

pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple selenium
  • 案例:
from selenium import webdriver
from selenium.webdriver.common.by import By  # 按照什么方式查找,By.ID,By.CSS_SELECTOR
from selenium.webdriver.common.keys import Keys  # 键盘按键操作
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait  # 等待页面加载某些元素

chrome = webdriver.Chrome()

try:
    chrome.get('https://www.jd.com')
    input_tag = chrome.find_element(By.ID, 'key')
    input_tag.send_keys('hellokitty')
    input_tag.send_keys(Keys.ENTER)
    wait = WebDriverWait(chrome, 10)
    # time.sleep(3)
    wait.until(EC.presence_of_element_located((By.ID, 'J_goodsList')))  # 等到id为J_goodsList的元素加载完毕,最多等10秒
    chrome.save_screenshot("hellokitty.png")

finally:
    chrome.close()

【3】元素定位

# 方式1
el = driver.find_element_by_xxx(value)
# 方式2: 推荐
from selenium.webdriver.common.by import By
driver.find_element(By.xxx,value)
driver.find_elements(By.xx, value)  # 返回列表
  • 八种方式:
 id
 name
 class 
 tag 
 link 
 partial 
 xpath
 css 

【4】元素操作(节点交互)

  • Selenium可以驱动浏览器来执行一些操作,也就是说可以让浏览器模拟执行一些动作。
    • 比较常见的用法有:
      • 输入文字时用send_keys()方法,
      • 清空文字时用clear()方法,
      • 点击按钮时用click()方法。
      • 示例如下:

find_element方法仅仅能够获取元素对象,接下来就可以对元素执行以下操作 从定位到的元素中提取数据的方法

(1)从定位到的元素中获取数据

el.get_attribute(key)           # 获取key属性名对应的属性值
el.text                        	# 获取开闭标签之间的文本内容

(2)对定位到的元素的操作

el.click()                      # 对元素执行点击操作
el.submit()                     # 对元素执行提交操作
el.clear()                      # 清空可输入元素中的数据
el.send_keys(data)              # 向可输入元素输入数据
from selenium import webdriver
import time
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://www.taobao.com')

q = browser.find_element(By.ID, "q")
q.send_keys('MAC')
time.sleep(1)
q.clear()
q.send_keys('IPhone')
button = browser.find_element(By.CLASS_NAME, "btn-search")
button.click()

【5】动作链

  • 在上面的实例中,一些交互动作都是针对某个节点执行的。

    • 比如,对于输入框,我们就调用它的输入文字和清空文字方法;
    • 对于按钮,就调用它的点击方法。
    • 其实,还有另外一些操作,它们没有特定的执行对象,
      • 比如鼠标拖曳、键盘按键等,
      • 这些动作用另一种方式来执行,
      • 那就是动作链。
  • 比如,现在实现一个节点的拖曳操作,将某个节点从一处拖曳到另外一处,可以这样实现:

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

driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.maximize_window()
driver.get('http://sahitest.com/demo/dragDropMooTools.htm')

dragger = driver.find_element(By.ID, 'dragger')  # 被拖拽元素
item1 = driver.find_element(By.XPATH, '//div[text()="Item 1"]')  # 目标元素1
item2 = driver.find_element(By.XPATH, '//div[text()="Item 2"]')  # 目标2
item3 = driver.find_element(By.XPATH, '//div[text()="Item 3"]')  # 目标3
item4 = driver.find_element(By.XPATH, '//div[text()="Item 4"]')  # 目标4
action = ActionChains(driver)
action.drag_and_drop(dragger, item1).perform()  # 1.移动dragger到目标1
sleep(2)
action.click_and_hold(dragger).release(item2).perform()  # 2.效果与上句相同,也能起到移动效果
sleep(2)
action.click_and_hold(dragger).move_to_element(item3).release().perform()  # 3.效果与上两句相同,也能起到移动的效果
sleep(2)
action.click_and_hold(dragger).move_by_offset(800, 0).release().perform()
sleep(2)
driver.quit()

【6】执行JS

  • selenium并不是万能的,有时候页面上操作无法实现的,这时候就需要借助JS来完成了。

  • 当页面上的元素超过一屏后,想操作屏幕下方的元素,是不能直接定位到,会报元素不可见的。

    • 这时候需要借助滚动条来拖动屏幕,使被操作的元素显示在当前的屏幕上。
    • 滚动条是无法直接用定位工具来定位的。
    • selenium里面也没有直接的方法去控制滚动条,这时候只能借助Js代码了,
    • 还好selenium提供了一个操作js的方法:
      • execute_script(),可以直接执行js的脚本。
  • 代码如下:

import time
from selenium import webdriver

browser = webdriver.Chrome()
browser.get('https://www.jd.com/')
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
time.sleep(3)

【7】页面等待

(1)为什么需要等待

  • 如果网站采用了动态html技术,那么页面上的部分元素出现时间便不能确定,这个时候就可以设置一个等待时间,强制等待指定时间,等待结束之后进行元素定位,如果还是无法定位到则报错

(2)页面等待的三种方法

  • 强制等待

    也叫线程等待, 通过线程休眠的方式完成的等待,如等待5秒: Thread sleep(5000),一般情况下不太使用强制等待,主要应用的场景在于不同系统交互的地方。

    import time
    time.sleep(n)      # 阻塞等待设定的秒数之后再继续往下执行
    
  • 显式等待

    也称为智能等待,针对指定元素定位指定等待时间,在指定时间范围内进行元素查找,找到元素则直接返回,如果在超时还没有找到元素,则抛出异常,显示等待是 selenium 当中比较灵活的一种等待方式,他的实现原理其实是通过 while 循环不停的尝试需要进行的操作。

    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
     # 每隔 0.5s 检查一次(默认就是 0.5s), 最多等待 10 秒,否则报错。如果定位到元素则直接结束等待,如果在10秒结束之后仍未定位到元素则报错
     wait = WebDriverWait(chrome, 10,0.5)
     wait.until(EC.presence_of_element_located((By.ID, 'J_goodsList'))) 
    
    
  • 隐式等待 隐式等待设置之后代码中的所有元素定位都会做隐式等待

    通过implicitly Wait完成的延时等待,注意这种是针对全局设置的等待,如设置超时时间为10秒,使用了implicitlyWait后,如果第一次没有找到元素,会在10秒之内不断循环去找元素,如果超过10秒还没有找到,则抛出异常,隐式等待比较智能,它可以通过全局配置,但是只能用于元素定位

    driver.implicitly_wait(10)    # 在指定的n秒内每隔一段时间尝试定位元素,如果n秒结束还未被定位出来则报错
    

(3)注意:

  • Selenium显示等待和隐式等待的区别
    • 1、selenium的显示等待

      • 原理:显示等待,就是明确要等到某个元素的出现或者是某个元素的可点击等条件,等不到,就一直等,除非在规定的时间之内都没找到,就会跳出异常Exception
      • (简而言之,就是直到元素出现才去操作,如果超时则报异常)
    • 2、selenium的隐式等待

      • 原理:隐式等待,就是在创建driver时,为浏览器对象创建一个等待时间,这个方法是得不到某个元素就等待一段时间,直到拿到某个元素位置。
      • 注意:在使用隐式等待的时候,实际上浏览器会在你自己设定的时间内部断的刷新页面去寻找我们需要的元素

【8】selenium的其他操作

(1)无头浏览器

  • 我们已经基本了解了selenium的基本使用了.
    • 但是呢, 不知各位有没有发现, 每次打开浏览器的时间都比较长. 这就比较耗时了.
    • 我们写的是爬虫程序. 目的是数据. 并不是想看网页.
    • 那能不能让浏览器在后台跑呢?
      • 答案是可以的
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options

opt = Options()
opt.add_argument("--headless")
opt.add_argument('--disable-gpu')
opt.add_argument("--window-size=4000,1600")  # 设置窗口大小

web = Chrome(options=opt)

(2)selenium 处理cookie

# 通过driver.get_cookies()能够获取所有的cookie
dictCookies = driver.get_cookies()
# 添加cookie
driver.add_cookie(dictCookies)
# 删除一条cookie
driver.delete_cookie("CookieName")
# 删除所有的cookie
driver.delete_all_cookies()

(3)页面前进和后退

driver.forward()     # 前进
driver.back()        # 后退
driver.refresh() 		 # 刷新
driver.close()       # 关闭当前窗口

【9】滑动验证案例

import time
from selenium import webdriver
from selenium.webdriver.common.by import By  # 按照什么方式查找,By.ID,By.CSS_SELECTOR
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait  # 等待页面加载某些元素
import cv2

from urllib import request
from selenium.webdriver.common.action_chains import ActionChains


def get_distance():
    background = cv2.imread("background.png", 0)
    gap = cv2.imread("gap.png", 0)

    res = cv2.matchTemplate(background, gap, cv2.TM_CCOEFF_NORMED)
    value = cv2.minMaxLoc(res)[2][0]
    print(value)
    return value * 278 / 360


def main():
    chrome = webdriver.Chrome()
    chrome.implicitly_wait(5)

    chrome.get('https://passport.jd.com/new/login.aspx?')

    login = chrome.find_element(By.CLASS_NAME, 'login-tab-r')
    login.click()

    loginname = chrome.find_element(By.ID, 'loginname')
    loginname.send_keys("123@qq.com")

    nloginpwd = chrome.find_element(By.ID, 'nloginpwd')
    nloginpwd.send_keys("987654321")

    loginBtn = chrome.find_element(By.CLASS_NAME, 'login-btn')
    loginBtn.click()

    img_src = chrome.find_element(By.XPATH, '//*[@class="JDJRV-bigimg"]/img').get_attribute("src")
    temp_src = chrome.find_element(By.XPATH, '//*[@class="JDJRV-smallimg"]/img').get_attribute("src")
    request.urlretrieve(img_src, "background.png")
    request.urlretrieve(temp_src, "gap.png")

    distance = int(get_distance())
    print("distance:", distance)

    print('第一步,点击滑动按钮')
    element = chrome.find_element(By.CLASS_NAME, 'JDJRV-slide-btn')
    ActionChains(chrome).click_and_hold(on_element=element).perform()  # 点击鼠标左键,按住不放

    ActionChains(chrome).move_by_offset(xoffset=distance, yoffset=0).perform()
    ActionChains(chrome).release(on_element=element).perform()


if __name__ == '__main__':
    main()

(二) pyppeteer

  • 由于Selenium流行已久,现在稍微有点反爬的网站都会对selenium和webdriver进行识别,网站只需要在前端js添加一下判断脚本,很容易就可以判断出是真人访问还是webdriver。
    • 虽然也可以通过中间代理的方式进行js注入屏蔽webdriver检测,
    • 但是webdriver对浏览器的模拟操作(输入、点击等等)都会留下webdriver的标记,
    • 同样会被识别出来,要绕过这种检测,只有重新编译webdriver,麻烦自不必说,难度不是一般大。
  • 由于Selenium具有这些严重的缺点。
    • pyperteer成为了爬虫界的又一新星。
    • 相比于selenium具有异步加载、速度快、具备有界面/无界面模式、伪装性更强不易被识别为机器人,
    • 同时可以伪装手机平板等终端;
    • 虽然支持的浏览器比较单一,
    • 但在安装配置的便利性和运行效率方面都要远胜selenium。

  • pyppeteer无疑为防爬墙撕开了一道大口子,
    • 针对selenium的淘宝、美团、文书网等网站,目前可通过该库使用selenium的思路继续突破,毫不费劲。
  • 介绍Pyppeteer之前先说一下Puppeteer,
    • Puppeteer是谷歌出品的一款基于Node.js开发的一款工具,
    • 主要是用来操纵Chrome浏览器的 API,
    • 通过Javascript代码来操纵Chrome浏览器,完成数据爬取、Web程序自动测试等任务。
  • pyppeteer是非官方 Python 版本的 Puppeteer 库,
    • 浏览器自动化库,
    • 由日本工程师开发。
  • Puppeteer是 Google 基于 Node.js 开发的工具,
    • 调用 Chrome 的 API,
    • 通过 JavaScript 代码来操纵 Chrome 完成一些操作,
    • 用于网络爬虫、Web 程序自动测试等。
  • pyppeteer 使用了 Python 异步协程库asyncio,可整合 Scrapy 进行分布式爬虫。

puppet 木偶,puppeteer 操纵木偶的人。

pip3 install pyppeteer
  • 滑动验证案例
import random
from pyppeteer import launch
import asyncio
import cv2
from urllib import request


async def get_track():
    background = cv2.imread("background.png", 0)
    gap = cv2.imread("gap.png", 0)

    res = cv2.matchTemplate(background, gap, cv2.TM_CCOEFF_NORMED)
    value = cv2.minMaxLoc(res)[2][0]
    print(value)
    return value * 278 / 360


async def main():
    browser = await launch({
        "headless": False,  # headless指定浏览器是否以无头模式运行,默认是True。
        "args": ['--window-size=1366,768'],
    })
    # 打开新的标签页
    page = await browser.newPage()
    # 设置页面大小一致
    await page.setViewport({"width": 1366, "height": 768})
    # 访问主页
    await page.goto("https://passport.jd.com/new/login.aspx?")

    # evaluate()是执行js的方法,js逆向时如果需要在浏览器环境下执行js代码的话可以利用这个方法
    # js为设置webdriver的值,防止网站检测
    # await page.evaluate('''alert("马上输入用户名密码了!")''')
    # await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')
    # await page.screenshot({'path': './1.jpg'})   # 截图保存路径
    # 单击事件
    await page.click('div.login-tab-r')
    # 模拟输入用户名和密码,输入每个字符的间隔时间delay ms
    await page.type("#loginname", '324534534@qq.com', {
        "c": random.randint(30, 60)
    })
    await page.type("#nloginpwd", '345653332', {
        "delay": random.randint(30, 60)
    })

    # page.waitFor 通用等待方式,如果是数字,则表示等待具体时间(毫秒): 等待2秒
    await page.waitFor(2000)
    await page.click("div.login-btn")
    await page.waitFor(2000)
    # page.jeval(selector,pageFunction)#定位元素,并调用js函数去执行
    img_src = await page.Jeval(".JDJRV-bigimg > img", "el=>el.src")
    temp_src = await page.Jeval(".JDJRV-smallimg > img", "el=>el.src")

    request.urlretrieve(img_src, "background.png")
    request.urlretrieve(temp_src, "gap.png")

    # 获取gap的距离
    distance = await get_track()
    """
        # Pyppeteer 三种解析方式
        Page.querySelector()  # 选择器
        Page.querySelectorAll()
        Page.xpath()  # xpath  表达式
        # 简写方式为:
        Page.J(), Page.JJ(), and Page.Jx()
        """
    el = await page.J("div.JDJRV-slide-btn")
    # 获取元素的边界框,包含x,y坐标
    box = await el.boundingBox()
    await page.hover("div.JDJRV-slide-btn")
    await page.mouse.down()
    # steps 是指分成几步来完成,steps越大,滑动速度越慢
    await page.mouse.move(box["x"] + distance + random.uniform(30, 33), box["y"], {"steps": 100})
    await page.waitFor(1000)
    await page.mouse.move(box["x"] + distance + 29, box["y"], {"steps": 100})
    await page.mouse.up()
    await page.waitFor(2000)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())