爬虫和反爬虫的斗争
在学习 Selenium 之前,我们先来看下爬虫和反爬虫的斗争:
这个 Selenium 跟爬虫到底有什么渊源呢?
- Selenium 可以便捷的获取网站中动态加载的数据
- 便捷的实现模拟登陆
动态 HTML 技术
接下来我们就分别看下 Selenium 到底是如何实现的,首先我们先了解下什么是动态 HTML 技术。
JavaScript
是网络上最常用的脚本语言,它可以收集用户的跟踪数据,不需要重载页面直接提交表单,在页面嵌入多媒体文件,甚至运行网页。
jQuery
jQuery 是一个快速、简介的 JavaScript 框架,封装了 JavaScript 常用的功能代码。
Ajax
Ajax 可以使用网页实现异步更新,可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
获取 Ajax 数据的方式:
- 直接分析 Ajax 调用的接口。然后通过代码请求这个接口。
- 使用 Selenium + ChromeDriver 模拟浏览器行为获取数据。
方式 | 优点 | 缺点 |
---|---|---|
分析接口 | 直接可以请求到数据。不需要做一些解析工作。代码量少,性能高 | 分析接口比较复杂,特别是一些通过 JS 混淆的接口,要有一定的 JS 功底。容易被发现是爬虫。 |
Selenium | 直接模拟浏览器的行为。浏览器能请求到的,使用 Selenium 也能请求到。爬虫更稳定。 | 代码量多。性能低。 |
给大家举个例子,某视频,点击加载更多视频,整个页面并没有刷新,而且动态加载出来的数据,并没有在页面源代码中 ,这个大家可以查看页面源代码:
Selenium 介绍
Selenium 是一个 Web 的自动化测试工具,最初是为网站自动化测试而开发的,Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器,可以接收指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏。
Selenium 是需要安装的,安装通过 pip 命令:
pip install selenium
Selenium 的使用需要,下载浏览器驱动。
ChromeDriver 是一个驱动 Chrome 浏览器的驱动程序,使用他才可以驱动浏览器。当然针对不同的浏览器有不同的 driver。以下列出了不同浏览器及其对应的 driver:
- Chrome:http://npm.taobao.org/mirrors/chromedriver/
- Firefox:https://github.com/mozilla/geckodriver/releases
- Edge:https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
- Safari:https://webkit.org/blog/6900/webdriver-support-in-safari-10/
Selenium 入门
通过 Selenium 打开百度,输入 python 并进行搜索:
from selenium import webdriver
# 实例化浏览器
driver = webdriver.Chrome()
# 窗口最大化
driver.maximize_window()
# 发送请求
driver.get('https://www.baidu.com')
# 元素定位
driver.find_element_by_id('kw').send_keys('Python')
driver.find_element_by_id('su').click()
效果展示:
接下来我们看下 Selenium 动态加载与 requests 区别。
我们通过 Selenium 去请求掘金的首页,并获取标题:
from selenium import webdriver
from lxml import etree
wd = webdriver.Chrome(executable_path='./chromedriver')
wd.get('https://juejin.cn/')
# print(wd.page_source)
html = etree.HTML(wd.page_source)
title = html.xpath("//div[@class='title-row']/a/text()")
print(title)
我们通过 requests 模块去请求掘金首页,并获取标题:
import requests
from lxml import etree
r = requests.get('https://juejin.cn/')
# print(r.text)
html = etree.HTML(r.text)
title = html.xpath("//div[@class='title-row']/a/text()")
print(title)
代码比较简单,大家可以自行运行看下区别。
driver 对象的常用属性和方法
定位和操作:
driver.find_element_by_id("kw").send_keys("长城") # 通过 ID 定位
driver.find_element_by_id("su").click()
查看请求信息:
driver.page_source # 页面源码
driver.get_cookies() # 获取 cookie
driver.current_url # 当前请求的 URL 地址
退出:
driver.close() # 退出当前页面
driver.quit() # 退出浏览器
前进和后退:
driver.back() # 后退
driver.forward() # 前进
定位元素
find_element_by_id:根据 id 来查找某个元素。
from selenium.webdriver.common.by import By
submitTag = driver.find_element_by_id('su')
submitTag1 = driver.find_element(By.ID,'su')
上面写了两种方式定位页面元素,这里推荐大家用 find_element 的方式。
find_element_by_class_name:根据类名查找元素。
submitTag = driver.find_element_by_class_name('su')
# 百度页面输入 Python
driver.find_element_by_class_name('s_ipt').send_keys('python')
submitTag1 = driver.find_element(By.CLASS_NAME,'su')
find_element_by_name:根据 name 属性的值来查找元素。
submitTag = driver.find_element_by_name('email')
# 百度页面输入 Python
driver.find_element_by_class_name('wd').send_keys('python')
submitTag1 = driver.find_element(By.NAME,'email')
find_element_by_tag_name:根据标签名来查找元素。
submitTag = driver.find_element_by_tag_name('div')
submitTag1 = driver.find_element(By.TAG_NAME,'div')
find_element_by_xpath:根据 xpath 语法来获取元素。
submitTag = driver.find_element_by_xpath('//div')
# 百度页面输入 Python
driver.find_element_by_xpath('//input[@id="kw"]').send_keys('python')
submitTag1 = driver.find_element(By.XPATH,'//div')
find_element_by_css_selector:根据 css 选择器选择元素。
submitTag = driver.find_element_by_css_selector('//div')
# 百度页面输入 Python
driver.find_element_by_css_selector('.s_ipt').send_keys('python')
submitTag1 = driver.find_element(By.CSS_SELECTOR,'//div')
标签对象提取文本内容和属性值:
- 获取文本 element.text:通过定位获取的标签对象的 text 属性,获取文本内容。
- 获取属性值
element.get_attribute('属性值')
:通过定位获取的标签对象的 get_attribute 函数,传入属性名,来获取属性的值。
Selenium 执行 JavaScript 代码
我们在获取页面数据的时候,通常会向下滑动页面,这个时候我们怎么借助 Selenium 来帮助我们来完成呢?
# 滚轮向下滑动一屏的距离
window.scrollTo(0, document.body.scrollHeight)
Window 对象方法参考链接:
https://www.runoob.com/jsref/obj-window.html
在爬虫里面我们用的最多的就是这两个属性:
Selenium 操作表单
Selenium 操作表单,一般都是用来模拟登陆,获取 cookie,一般分为两步:
- 第一步:找到用户名和密码的元素
- 第二步:使用 send_keys(value),将数据填充进去
听上去是不是很简单,但实际操作起来可能会遇到一些问题,我们来看下模拟登陆的时候会遇到什么问题。
登录豆瓣练习:
from selenium import webdriver
import time
driver = webdriver.Chrome()
driver.get("https://www.douban.com/")
login_frame = driver.find_element_by_xpath('//div[@class="login"]/iframe')
driver.switch_to.frame(login_frame)
driver.find_element_by_class_name('account-tab-account').click()
driver.find_element_by_id("username").send_keys("123@qq.com")
driver.find_element_by_id("password").send_keys("")
time.sleep(3)
driver.find_element_by_class_name("//div[@class='account-form-field-submit ']/a").click()
# 复制页面 xpath
# driver.find_element_by_xpath("/html/body/div[1]/div[2]/div[1]/div[5]/a").click()
time.sleep(4)
cookies = {i['name']:i['value'] for i in driver.get_cookies()}
print(cookies)
driver.quit()
大家在做模拟登陆的时候,经常会遇到 iframe,这个时候我们需要先切换到 iframe 中去在进行操作,不然 Selenium 会定位不到页面元素。
Selenium 行为链
有时候在页面中的操作可能要有很多步,那么这时候可以使用鼠标行为链类 ActionChains 来完成。比如现在要将鼠标移动到某个元素上并执行点击事件。
通过行为链的方式来打开百度进行搜索 Python:
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
driver = webdriver.Chrome()
driver.get('https://www.baidu.com/')
inputTag = driver.find_element_by_id('kw')
submitBtn = driver.find_element_by_id('su')
# 实例化动作链
actions = ActionChains(driver)
actions.move_to_element(inputTag)
actions.send_keys_to_element(inputTag,'python')
actions.move_to_element(submitBtn)
actions.click(submitBtn)
# 提交行为链上的动作
actions.perform()
12306 登录
from selenium import webdriver
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver import ChromeOptions
# 让 selenium 规避被检测的风险
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
browser = webdriver.Chrome(executable_path='./chromedriver', options=option)
browser.get("https://kyfw.12306.cn/otn/resources/login.html")
# 特征识别
script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
browser.execute_script(script)
browser.find_element_by_id('J-userName').send_keys('123@qq.com')
time.sleep(1)
browser.find_element_by_id('J-password').send_keys('xxx')
time.sleep(1)
browser.find_element_by_id('J-login').click()
time.sleep(3)
# span = browser.find_element(By.ID, 'nc_2_n1z')
span = browser.find_element(By.XPATH, '//span[@id="nc_1_n1z"]')
time.sleep(1)
actions = ActionChains(browser)
time.sleep(1)
# actions.click_and_hold(span).drag_and_drop_by_offset(span, 300, 0).perform()
actions.click_and_hold(span).move_by_offset(300, 0).perform()
页面等待
Selenium 的页面等待的分类:
- 强制等待
- 隐式等待
- 显示等待
强制等待其实就是我们平时用的 time.sleep(3),这种方式不管页面元素有没有加载出来都会进行等待,对我们的程序不是特别友好
隐式等待
现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个元素完全加载出来了。如果实际页面等待时间过长导致某个 dom 元素还没出来,但是你的代码直接使用了这个 WebElement,那么就会抛出 NullPointer 的异常。为了解决这个问题。所以 Selenium 提供了两种等待方式:一种是隐式等待、一种是显式等待。
隐式等待:调用 driver.implicitly_wait。那么在获取不可用的元素之前,会先等待 10 秒中的时间。如果第 3 秒元素已经加载出来,那么剩余 7 秒就不在等待。
from selenium import webdriver
driver = webdriver.Chrome()
# 请求网页
driver.get("https://www.douban.com/")
driver.implicitly_wait(10)
driver.find_element_by_id('asdsad')
如果在规定的等待时间内,页面元素还没有加载出来,就会报错。
显示等待
显示等待是表明某个条件成立后才执行获取元素的操作。也可以在等待的时候指定一个最大的时间,如果超过这个时间那么就抛出一个异常。显示等待应该使用 selenium.webdriver.support.excepted_conditions 期望的条件和 selenium.webdriver.support.ui.WebDriverWait 来配合完成。明确等待某一个元素,通常用在软件测试。
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
driver = webdriver.Chrome()
driver.get("https://www.baidu.com/")
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "myDynamicElement"))
)
finally:
driver.quit()
一些其他的等待条件:
- presence_of_element_located:某个元素已经加载完毕了。
- presence_of_all_emement_located:网页中所有满足条件的元素都加载完毕了。
- element_to_be_clickable:某个元素是可以点击了。
更多条件请参考:
http://selenium-python.readthedocs.io/waits.html
Selenium 滑动验证码
这是滑动验证码的平台:
https://www.geetest.com/demo/slide-float.html
我们可以用来进行测试。
打开 F12,找到滑动验证码的两张图片,我们发现背景图片有样式,当把这个样式去掉后,缺口图片消失了,这样我们就可以对比两张图片不同来找到滑动距离
当 style 样式取消掉后缺口图片消失,我们可以利用 opencv 来对比两张图片,找到滑动距离:
示例代码:
from io import BytesIO
import random
from Selenium import webdriver
from Selenium.webdriver.common.action_chains import ActionChains
from PIL import Image
import time
class GeetestLogin(object):
login_url = "https://www.geetest.com/demo/slide-float.html"
def __init__(self):
self.user_name = "123"
self.pass_word = "123"
self.driver = webdriver.Chrome()
def check_login(self):
try:
self.driver.find_element_by_xpath("//span[contains(text(),'创作中心')]")
return True
except Exception as e:
return False
def compare_pixel(self, image1, image2, i, j):
# 判断两个像素是否相同
pixel1 = image1.load()[i, j]
pixel2 = image2.load()[i, j]
threshold = 60
# pixel1[0,1,2] RGB 对比误差在 60 个像素内都算是相同
if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
pixel1[2] - pixel2[2]) < threshold:
return True
return False
def crop_image(self, image_file_name):
#截取验证码图片
time.sleep(2)
img = self.driver.find_element_by_class_name("geetest_canvas_img")
# img = self.driver.find_element_by_css_selector(".geetest_canvas_img.geetest_absolute")
location = img.location # {'x': 1078, 'y': 283}
print("图片的位置: ", location)
size = img.size
# 距离图片左边界距离 x, 距离图片上边界距离 y,
# 距离图片左边界距离+裁剪框宽度 x+w,距离图片上边界距离+裁剪框高度 y+h
x1, y1 = location["x"], location["y"]
x2, y2 = location["x"] + size['width'], location["y"] + size["height"]
# top, buttom, left, right = location["y"], location["y"]+size["height"], location["x"], location["x"]+size["width"]
print("验证码截图坐标: ", x1, y1, x2, y2)
screen_shot = self.driver.get_screenshot_as_png()
screen_shot = Image.open(BytesIO(screen_shot))
# captcha = screen_shot.crop((int(left), int(top), int(right), int(buttom)))
captcha1 = screen_shot.crop((int(x1), int(y1), int(x2), int(y2)))
captcha1.save(image_file_name)
return captcha1
def login(self):
try:
self.driver.maximize_window() # 将窗口最大化防止定位错误
except Exception as e:
pass
while not self.check_login():
self.driver.get(self.login_url)
username_ele = self.driver.find_element_by_id("username")
password_ele = self.driver.find_element_by_id("password")
username_ele.send_keys(self.user_name)
password_ele.send_keys(self.pass_word)
time.sleep(2)
#1. 点击登录调出滑动验证码
login_btn = self.driver.find_element_by_xpath("//div[@class='geetest_radar_tip']")
# login_btn = self.driver.find_element_by_css_selector(".btn.btn-login")
login_btn.click()
#等待一段时间,等待滑动验证码出现
time.sleep(5)
#执行 js 改变 css 样式,显示没有缺口的图!!!
self.driver.execute_script('document.querySelectorAll("canvas")[2].style=""')
#截取验证码
image1 = self.crop_image("captcha1.png")
# 执行 js 改变 css 样式,显示有缺口的图!!!!!重点是这一步!
self.driver.execute_script('document.querySelectorAll("canvas")[2].style="display: none;"')
image2 = self.crop_image("captcha2.png")
# 从 60 个像素开始比对
left = 60
has_find = False
# 从 60 个像素到图片的宽
for i in range(60, image1.size[0]):
if has_find:
break
# 图片的高从零开始
for j in range(image1.size[1]):
# 把两个图片位置传进去做对比
if not self.compare_pixel(image1, image2, i, j):
# i 是列 不一样的话 就是找到了
left = i
has_find = True
break
left -= 6
print(left)
# 拖动图片
# 根据偏移量获取移动轨迹
# 一开始加速,然后减速,生长曲线,且加入点随机变动
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = left * 3 / 4
# 间隔时间
t = 0.1
v = 0
while current < left:
if current < mid:
a = random.randint(2, 3)
else:
a = - random.randint(6, 7)
v0 = v
# 当前速度
v = v0 + a * t
# 移动距离 位移=初速度×时间+1/2×加速度×时间的平方
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
track.append(round(move))
slider = self.driver.find_element_by_css_selector(".geetest_slider_button")
# click_and_hold 点击按住 perform 执行
ActionChains(self.driver).click_and_hold(slider).perform()
for x in track:
ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
# 松开
ActionChains(self.driver).release().perform()
time.sleep(5)
if __name__ == "__main__":
gt = GeetestLogin()
gt.login()