爬虫篇:Ajax请求处理技术总结(下)—— 模拟浏览器行为

每篇一句:

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进行了介绍还有部分官方文档的翻译,写的比较完善。

如果文中有什么不足或错误之处,欢迎指出!

猜你喜欢

转载自blog.csdn.net/Ha_hha/article/details/79807065