每篇一句:
Never lie to someone who trust you. Never trust someone who lies to you.
前言:
上一篇文章中我们介绍了爬取动态网页的一种方式:逆向工程 。
这种方式不足之处就是:这种方式要求我们对JavaScript以及Ajax有一定的了解,而且当网页的JS代码混乱,难以分析的时候,上述过程会花费我们大量的时间和精力。
这时候,如果对爬虫执行效率没有过多要求,又不想浪费太多时间在了解JavaScript代码逻辑、找寻Ajax请求链接上,我们可以尝试以下思路:
- 模拟浏览器行为,通过使用浏览器渲染引擎对目标网页执行JavaScript代码,并解析HTML。
Selenium 简介:
Selenium 是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持市面上几乎所有的主流浏览器。
本来打算使用的是selenium + PhantomJS的组合,但发现Chrome以及FireFox也相继推出无头 ( headless ) 浏览器模式,个人比较倾向Chrome。本文采用的是Selenium+Chrome的组合。
接下来会结合一个具体网页介绍Selenium的使用。
示例:
还是以 新浪读书_书摘 为例:
由于Selenium使用较为简单,直接看示例代码,代码中含有足够的注释:
# coding=utf8
import time
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import NoSuchElementException
# 解析列表页内容
def getItem():
# 处理文章详情页面,具体方法根据具体情况添加
global location
# 每解析一篇文章location加一
location += 1
def getList():
global location
global driver
# 解析网页内容
divs = driver.find_elements_by_class_name("item")
# 构造动作链
actions = ActionChains(driver)
for i in range(location, len(divs)):
div = divs[i]
# 标题
title = div.find_element_by_tag_name("a").text
# URL
url = div.find_element_by_tag_name("a")
# url = div.find_element_by_tag_name('a').get_attribute("href")
# 进入文章详情页
actions.click(url)
actions.perform()
actions.reset_actions()
# 将driver转移到当前页面,接下来处理文章详情页面
driver.switch_to.window(driver.window_handles[1])
# 在此处调用处理文章详情页的方法,获取所需要的字段
getItem()
driver.close()
driver.switch_to.window(driver.window_handles[0])
print driver.title
# 判断“更多书摘”按钮是否存在,存在的话,返回元素
def loadMore():
global driver
elem = None
try:
elem = driver.find_element_by_id("subShowContent1_loadMore")
except NoSuchElementException:
pass
# 如果不存在或不可见,返回None
if elem is None or not(elem.is_displayed()):
return None
else:
return elem
# 每两次“更多书摘”后,会出现“下一页”按钮,判断"下一页"是否存在,存在的话,返回元素
def pagebox_next():
global driver
next_page = None
try:
next_page = driver.find_element_by_class_name("pagebox_next")
except NoSuchElementException:
pass
# 如果不存在或不可见,返回None
if next_page is None or not(next_page.is_displayed()):
return None
else:
return next_page
# 判断是否还有新的内容
def haveNext():
global location
global driver
more = loadMore()
if more: # 是否存在“更多书摘”按钮
return more
else:
# 无论是否还有“下一页”,都将location置为0
location = 0
more = pagebox_next()
return more
# 点击“更多书摘”后,之前内容并不会消失,所以设置一个变量记录当前位置
# 每次点击“下一页后,再次置为0
location = 0
# 指定driver的浏览器
# 创建Chrome的无头浏览器
# opt = webdriver.ChromeOptions()
# opt.set_headless()
# driver = webdriver.Chrome(options=opt)
# 创建可见的Chrome浏览器
driver = webdriver.Chrome()
# 设置浏览器的隐式等待时间
driver.implicitly_wait(30)
# get方法打开网页
driver.get("http://book.sina.com.cn/excerpt/")
# 分析文章列表
getList()
# 初始化动作链
actions_more = ActionChains(driver)
# 判断是否还有新的内容
more_page = haveNext()
while more_page:
# 添加点击动作
actions_more.click(more_page)
actions_more.perform()
actions_more.reset_actions()
# 每次翻页或刷新页面时要等待页面加载完成,
# 这里采用的是强制等待,效率较慢,不推荐这种做法;
# 比较适合的是采用Selenium的显性等待方法:"WebDriverWait",配合该类的until()和until_not()方法,就能够根据判断条件而进行灵活地等待了。
time.sleep(3)
# 分析加载的新内容,从location开始
getList()
# 是否存在更多或下一页
more_page = haveNext()
# 关闭浏览器
driver.close()
如果你想实际运行上述代码,请在运行之前确定(win10):
安装了Chrome浏览器(较新版本)。
安装了Chrome浏览器驱动(chromedriver.exe),并正确的添加到了环境变量
path
中。- 安装了Selenium。
简要解释:
Selenium 使用过程大致如下:
- 创建一个到浏览器的连接
from selenium import webdriver
driver = webfriver.Chrome()
# 创建无头浏览器方式:
# opt = webdriver.ChromeOptions()
# opt.set_headless()
# driver = webdriver.Chrome(options=opt)
- 设置等待时间:
driver.implicitly_wait(30)
- 此处是Selenium的隐式等待设置,我们设置了 30秒的延时,如果我们要查找的元素没有出现,Selenium 至多等待30秒,然后就会抛出异常。推荐配合显示等待一起使用
- 显示等待:"WebDriverWait",配合该类的until()和until_not()方法,就能够根据判断条件而进行灵活地等待了。
- 调用
get()
方法加载网页:
driver.get("http://book.sina.com.cn/excerpt/")
- 从网页获取需要的元素:
driver.find_element_by_id("item")
- 创建一条动作链:
actions = ActionChains(driver)
- 为动作链添加动作:
actions.click(url)
- 动作链执行:
actions.perform()
- 调用
close()
方法关闭浏览器:
driver.close()
之前的示例代码中包含了 翻页的逻辑判断,以及列表页与文章详情页之间的切换, 所以看起来可能比较复杂,但其实执行过程大致就是这个思路。
另外,示例代码并不是一个完整的爬虫,跳转到文章详情页之后,没有进行数据的获取。如果读者有需要的话,可以在示例代码中的getItem()
函数中添加具体的实现,之后还可以补充数据的储存等功能。
分析与比较:
使用Selenium特别要注意一点:
使用Selenium时,难免要进行 刷新或页面的切换,这时就要注意网页的响应时间。selenium不会等待网页响应完成再继续执行代码,它会直接执行。二者应该是不同的进程。这里可以选择设置隐性等待和显性等待。
隐形等待:是设置了一个最长等待时间,如果在规定时间内网页加载完成,则执行下一步,否则一直等到时间截止,然后抛出异常。需要特别说明的是:隐性等待对整个driver的周期都起作用,所以只要设置一次即可。
显性等待: WebDriverWait,配合该类的until()和until_not()方法,就能够根据判断条件而进行灵活地等待了。主要的意思就是:程序每隔xx秒看一眼,如果条件成立了,则执行下一步,否则继续等待,直到超过设置的最长时间,然后抛出TimeoutException。
- 如果同时设置了隐形等待和显性等待的话,在WebDriverWait中显性等待起主要作用,在其他操作中,隐形等待起决定性作用,要注意的是:最长的等待时间取决于两者之间的大者。
与“逆向工程”的比较:
前者(逆向工程)在运行上更快,开销更小。在实际情况中,大多数网页都可以被逆向,但是有些网页足够复杂逆向要花费极大的功夫。
后者(模拟浏览器行为)在思路上更直观,更容易被接受和理解。浏览器渲染引擎能够为我们节省了解网站后端工作原理的时间,但是渲染网页增大了开销,使其比单纯下载HTML更慢。另外,使用后者通常需要轮训网页来检查是否已经得到所需的HTML元素,这种方式非常脆弱,在网络较慢时经常会失败。
采用哪种方法,取决于爬虫活动中的具体情况:
易于逆向、对速度和资源要求高的,应使用前者;
难以逆向、没有工程上的优化的要求的,可以使用后者。
个人认为模拟浏览器的方法应尽量避免,因为浏览器环境对内存和CPU的消耗非常多,可以作为短期决绝方案,此时长期的性能和可靠性并不算重要;而作为长期解决方案,我会尽最大努力对网站进行逆向工程。
最后:
本文介绍了一种一个用于Web应用程序测试的工具Selenium,以及如何在不对网页进行逆向过程的情况下进行数据获取的思路。
如果读者想要深入了解Selenium相关内容,这里推荐一个知乎专栏 【每周一个小项目】 其中有六篇文章对Selenium常用的一些类、方法、API进行了介绍还有部分官方文档的翻译,写的比较完善。
如果文中有什么不足或错误之处,欢迎指出!