Web自动化

有些网站的数据都是通过接口的形式传输或者是一些 JSON 的数据,然后经过 JavaScript 渲染得出来的。用 requests 来爬取内容,爬取下来的只能是服务器端网页的源码,这和浏览器渲染以后的页面内容是不一样的。

真正的数据是经过 JavaScript 执行后,渲染出来的,数据来源可能是 Ajax,也可能是页面里的某些 Data,或者是一些 ifame 页面等。不过,大多数情况下极有可能是 Ajax 接口获取的。因此我们需要分析Ajax请求,分析这些接口的调用方式,通过抓包工具或者浏览器的“开发者工具”,找到数据的请求链接,然后再用程序来模拟。但是,抓包分析流的方式,也存在一定的缺点。因为有些接口带着加密参数,比如 token、sign 等等,模拟难度较大。

Puppeteer、Pyppeteer、Selenium、Splash 等自动化框架出现了。使用这些框架获取HTML源码,这样我们爬取到的源代码就是JavaScript 渲染以后的真正的网页代码,便于数据提取。同时,也就绕过分析 Ajax 和一些 JavaScript 逻辑的过程。这种方式就做到了可见即可爬,难度也不大,同时适合大批量的采集。

selenium

  • 安装selenium(pip安装)
1
pip install selenium
1
pip install selenium -i https://mirrors.aliyun.com/pypi/simple/
  • Pycharm安装

这里的版本选择尽量与浏览器版本一致, win64选win32即可,把下载的浏览器驱动放在python解释器所在的文件夹

初识selenium

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

  • 安装

pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple selenium

  • 以下是一个最简单的案例,通过模拟键盘输入mac book并通过回车进行搜素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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('mac book')
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()

元素定位

  • 方式一
1
el = driver.find_element_by_xxx(value)
  • 方式二
1
2
3
from selenium.webdriver.common.by import By
driver.find_element(By.xxx,value)
driver.find_elements(By.xxx, value)

xxx 处的方式一共有八种

id
name
class
tag
link
partial
xpath
css

元素操作

输入文字时用send_keys()方法,清空文字时用clear()方法,点击按钮时用click()方法。示例如下

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

  • 从定位到的元素中获取数据
1
2
el.get_attribute(key)           # 获取key属性名对应的属性值
el.text # 获取开闭标签之间的文本内容
  1. 对定位到的元素的操作
1
2
3
4
el.click()                      # 对元素执行点击操作
el.submit() # 对元素执行提交操作
el.clear() # 清空可输入元素中的数据
el.send_keys(data) # 向可输入元素输入数据

动作链

  • 以上动作们都是针对某个节点进行操作,例如点击这些,不能实现拖曳等动作,动作连就是为了帮助完成拖曳等连续动作,实现一个节点的拖曳操作,将某个节点从一处拖曳到另外一处。
1
2
3
4
5
6
7
8
9
10
11
12
driver = webdriver.Chrome()

dragger = driver.find_element(By.ID, 'dragger') # 被拖拽元素
item = driver.find_element(By.XPATH, '//div[text()="Item 1"]') # 目标元素
action = ActionChains(driver)

# 以下三种方式均可实现拖曳,使dragger移动到item
action.drag_and_drop(dragger, item).perform()
action.click_and_hold(dragger).release(item2).perform()
action.click_and_hold(dragger).move_to_element(item3).release().perform()

# 在x轴或y轴移动

执行js

  • selenium有时候页面上操作无法实现的,这时候就需要借助JS来完成

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

1
2
3
4
5
6
7
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)

页面等待

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

    • 强制等待

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      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秒还没有找到,则抛出异常,隐式等待比较智能,它可以通过全局配置,但是只能用于元素定位

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

显示等待和隐式等待的区别
selenium显示等待,就是明确要等到某个元素的出现或者是某个元素的可点击等条件,等不到,就一直等,除非在规定的时间之内都没找到,就会跳出异常Exception。

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

其他操作

  • 我们写的是爬虫程序,目的是数据,并不需要看网页。如下可设置浏览器后台运行
1
2
3
4
5
6
7
8
9
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)
  • selenium 处理cookie
1
2
3
4
5
6
7
8
# 通过driver.get_cookies()能够获取所有的cookie
dictCookies = driver.get_cookies()
# 添加cookie
driver.add_cookie(dictCookies)
# 删除一条cookie
driver.delete_cookie("CookieName")
# 删除所有的cookie
driver.delete_all_cookies()
  • 页面前进和后退
1
2
3
4
driver.forward()     # 前进
driver.back() # 后退
driver.refresh() # 刷新
driver.close() # 关闭当前窗口

滑动验证案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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,麻烦自不必说,难度不是一般大。
  • pyperteer成为了爬虫界的又一新星。相比于selenium具有异步加载、速度快、具备有界面/无界面模式、伪装性更强不易被识别为机器人,同时可以伪装手机平板等终端;虽然支持的浏览器比较单一,但在安装配置的便利性和运行效率方面都要远胜selenium。

pyppeteer是非官方 Python 版本的 Puppeteer 库,浏览器自动化库。 Puppeteer是谷歌出品的一款基于Node.js开发的一款工具,主要是用来操纵Chrome浏览器的 API,通过Javascript代码来操纵Chrome浏览器,完成数据爬取、Web程序自动测试等任务。Puppeteer是 Google 基于 Node.js 开发的工具,调用 Chrome 的 API,通过 JavaScript 代码来操纵 Chrome 完成一些操作,用于网络爬虫、Web 程序自动测试等。

  • pyppeteer 使用了 Python 异步协程库asyncio,可整合 Scrapy 进行分布式爬虫。

  • 安装

1
pip3 install pyppeteer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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", '123456@qq.com', {
"c": random.randint(30, 60)
})
await page.type("#nloginpwd", '1', {
"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())