[Python爬虫] 八、动态HTML处理之Selenium与PhantomJS


往期内容提要:


Xpath helper 或者是 chrome 中的 copy xpath 都是从 element 中提取的数据,但是爬虫获取的是url对应的响应,往往和 elements 不一样,这是因为使用了JavaScript、jQuery、 Ajax 或 DHTML(Dynamic HTML, DHTML) 技术改变 / 加载内容的页面,网页中数据并不直接渲染,而是由前端异步获取;对此我们可以尝试从 JavaScript 代码里采集内容用 Python 的 第三方库运行(费时费力) ;此外部分网页通过 JavaScript 的加密库生成动态的 token,同时加密库再进行混淆。对此我们就只能慢慢调试,找到加密原理,但是同样耗时耗力。

对此, Python对上述问题提出了解决方法,即选择内置浏览器引擎的爬虫( PhantomJS, Selenium ),在浏览器引擎运行页面,直接采集你在浏览器里看到的页面,拿到数据,获取正确结果。今天我们来学习动态HTML处理之Selenium与PhantomJS。


一、Selenium与PhantomJS

(1)Selenium

Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动操作,不同是Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器)。

Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。

Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所以我们可以用一个叫 PhantomJS 的工具代替真实的浏览器。

可以从 PyPI 网站下载 Selenium库 https://pypi.python.org/simple/selenium ,也可以用 第三方管理器 pip用命令安装:sudo pip install selenium

Selenium 官方参考文档:http://selenium-python.readthedocs.io/index.html

(2)PhantomJS

PhantomJS 是一个基于Webkit的“无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器要高效。

如果我们把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理 JavaScrip、Cookie、headers,以及任何我们真实用户需要做的事情。

  • PhantomJS 是一个功能完善(虽然无界面)的浏览器而非一个 Python 库,所以它不需要像 Python 的其他库一样安装,但我们可以通过Selenium调用PhantomJS来直接使用。
  • 在Ubuntu16.04中可以使用命令安装:sudo apt-get install phantomjs
  • 如果其他系统无法安装,可以从它的官方网站http://phantomjs.org/download.html) 下载。
  • PhantomJS 官方参考文档:http://phantomjs.org/documentation

二、快速入门

Selenium 库里有个叫 WebDriver 的 API。WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 或者其他 Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。

扫描二维码关注公众号,回复: 9186857 查看本文章
# IPython2 测试代码

# 导入 webdriver
from selenium import webdriver

# 调用键盘按键操作时需要引入的Keys包
from selenium.webdriver.common.keys import Keys

# 调用环境变量指定的PhantomJS浏览器创建浏览器对象
driver = webdriver.PhantomJS()

# 如果没有在环境变量指定PhantomJS位置
# driver = webdriver.PhantomJS(executable_path="./phantomjs"))

# get方法会一直等到页面被完全加载,然后才会继续程序,通常测试会在这里选择 time.sleep(2)
driver.get("http://www.baidu.com/")

# 获取页面名为 wrapper的id标签的文本内容
data = driver.find_element_by_id("wrapper").text

# 打印页面标题 "百度一下,你就知道"
print driver.title

# 生成当前页面快照并保存
driver.save_screenshot("baidu.png")

# id="kw"是百度搜索输入框,输入字符串"长城"
driver.find_element_by_id("kw").send_keys(u"长城")

# id="su"是百度搜索按钮,click() 是模拟点击
driver.find_element_by_id("su").click()

# ctrl+a 全选输入框内容
driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'a')

# ctrl+x 剪切输入框内容
driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'x')

# 获取href值
driver.find_element_by_xpath("//div[@id='u1']/a[2]").get_attribute('href')

# 模拟Enter回车键,替代点击操作
driver.find_element_by_id("su").send_keys(Keys.RETURN)

# 清除输入框内容
driver.find_element_by_id("kw").clear()

# 关闭当前页面,如果只有一个页面,会关闭浏览器
# driver.close()

# 关闭浏览器
driver.quit()

三、页面操作

一、加载网页:

  • from selenium import webdriver
  • driver = webdriver.PhantomJS(“c:…/pantomjs.exe”)
  • driver.get(“http://www.baidu.com/”) driver.save_screenshot(“长城.png”)

二、定位和操作:

  • driver.find_element_by_id(“kw”).send_keys(“长城”)
  • driver.find_element_by_id(“su”).click()

三、查看请求信息:

  • driver.page_source 返回页面源码
    driver.title 返回页面标题
    drive.current_url 返回当前页面的URL
    driver.get_cookies() 返回页面cookies
  • size 获取元素的尺寸
    text 获取元素的文本
    get_attribute(name) 获取元素的属性值
    tag_name 获取元素的tagName
    location 获取元素坐标,先找到要获取的元素,再调用该方法
    is_displayed() 设置该元素是否可见
    is_enabled() 判断元素是否被使用
    is_selected() 判断元素是否被选中

四、鼠标操作:

  • click(elem) 单击鼠标点击元素elem
  • click_and_hold(elem) 按下鼠标左键在一个元素上
  • context_click(elem) 右击鼠标点击元素elem,另存为等行为
  • double_click(elem) 双击鼠标点击元素elem,地图web可实现放大功能
  • drag_and_drop(source,target) 拖动鼠标,源元素按下左键移动至目标元素释放
  • move_to_element(elem) 鼠标移动到一个元素上
  • perform() 在通过调用该函数执行ActionChains中存储行为

五、键盘操作

  • send_keys(Keys.ENTER) 按下回车键 (和Keys.RETURN 没有区别,键值都是 13)
  • send_keys(Keys.TAB) 按下Tab制表键
  • send_keys(Keys.SPACE) 按下空格键
  • space send_keys(Kyes.ESCAPE) 按下回退键Esc
  • send_keys(Keys.BACK_SPACE) 按下删除键
  • BackSpace send_keys(Keys.SHIFT)按下shift键
  • send_keys(Keys.CONTROL) 按下Ctrl键
  • send_keys(Keys.ARROW_DOWN)按下鼠标光标向下按键
  • send_keys(Keys.CONTROL,‘a’) 组合键全选Ctrl+A
  • send_keys(Keys.CONTROL,‘c’) 组合键复制Ctrl+C
  • send_keys(Keys.CONTROL,‘x’)组合键剪切Ctrl+X
  • send_keys(Keys.CONTROL,‘v’) 组合键粘贴Ctrl+V

六、JavaScript操作

  • driver.execute_script(“some javascript code here”);

七、退出

  • driver.close() #退出当前页面
  • driver.quit() #退出浏览器

(1) 定位元素 (WebElements)

Selenium 的 WebDriver提供了各种方法来寻找元素,关于元素的选取,有如下的API 单个元素选取:

find_element_by_id
find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector

find_element 和find_elements的区别:返回一个和返回一个列表。

  1. By ID

    <div id="coolestWidgetEvah">...</div>
    
    • 实现

      element = driver.find_element_by_id("coolestWidgetEvah")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By element = driver.find_element(by=By.ID, value="coolestWidgetEvah")
      
  2. By Class Name

    <div class="cheese"><span>Cheddar</span></div><div class="cheese"><span>Gouda</span></div>
    
    • 实现

      cheeses = driver.find_elements_by_class_name("cheese")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      cheeses = driver.find_elements(By.CLASS_NAME, "cheese")
      
  3. By Tag Name

    <iframe src="..."></iframe>
    
    • 实现

      frame = driver.find_element_by_tag_name("iframe")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      frame = driver.find_element(By.TAG_NAME, "iframe")
      
  4. By Name

    <input name="cheese" type="text"/>
    
    • 实现

      cheese = driver.find_element_by_name("cheese")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      cheese = driver.find_element(By.NAME, "cheese")
      
  5. By Link Text

    <a href="http://www.google.com/search?q=cheese">cheese</a>
    
    • 实现

      cheese = driver.find_element_by_link_text("cheese")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      cheese = driver.find_element(By.LINK_TEXT, "cheese")
      
  6. By Partial Link Text

    <a href="http://www.google.com/search?q=cheese">search for cheese</a>>
    
    • 实现

      cheese = driver.find_element_by_partial_link_text("cheese")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      cheese = driver.find_element(By.PARTIAL_LINK_TEXT, "cheese")
      
  7. By CSS

    <div id="food"><span class="dairy">milk</span><span class="dairy aged">cheese</span></div>
    
    • 实现

      cheese = driver.find_element_by_css_selector("#food span.dairy.aged")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      cheese = driver.find_element(By.CSS_SELECTOR, "#food span.dairy.aged")
      
  8. By XPath

    <input type="text" name="example" />
    <INPUT type="text" name="other" />
    
    • 实现

      inputs = driver.find_elements_by_xpath("//input")
      ------------------------ or -------------------------
      from selenium.webdriver.common.by import By
      inputs = driver.find_elements(By.XPATH, "//input")
      

(2) 鼠标动作

有些时候,我们需要再页面上模拟一些鼠标操作,比如双击、右击、拖拽甚至按住不动等,我们可以通过导入 ActionChains 类来做到,常见的操作元素方法如下:

  • clear 清除元素的内容
  • send_keys 模拟按键输入 【如果需要输入中文,防止编码错误使用send_keys(u"中文用户名")】
  • click 点击元素
  • submit 提交表单
#导入 ActionChains 类
from selenium.webdriver import ActionChains

# 鼠标移动到 ac 位置
ac = driver.find_element_by_xpath('element')
ActionChains(driver).move_to_element(ac).perform()

# 在 ac 位置单击
ac = driver.find_element_by_xpath("elementA")
ActionChains(driver).move_to_element(ac).click(ac).perform()

# 在 ac 位置双击
ac = driver.find_element_by_xpath("elementB")
ActionChains(driver).move_to_element(ac).double_click(ac).perform()

# 在 ac 位置右击
ac = driver.find_element_by_xpath("elementC")
ActionChains(driver).move_to_element(ac).context_click(ac).perform()

# 在 ac 位置左键单击hold住
ac = driver.find_element_by_xpath('elementF')
ActionChains(driver).move_to_element(ac).click_and_hold(ac).perform()

# 将 ac1 拖拽到 ac2 位置
ac1 = driver.find_element_by_xpath('elementD')
ac2 = driver.find_element_by_xpath('elementE')
ActionChains(driver).drag_and_drop(ac1, ac2).perform()

(3) 填充表单

我们已经知道了怎样向文本框中输入文字,但是有时候我们会碰到<select> </select>标签的下拉框。直接点击下拉框中的选项不一定可行。

<select id="status" class="form-control valid" onchange="" name="status">
    <option value=""></option>
    <option value="0">未审核</option>
    <option value="1">初审通过</option>
    <option value="2">复审通过</option>
    <option value="3">审核不通过</option>
</select>

Selenium专门提供了Select类来处理下拉框。 其实 WebDriver 中提供了一个叫 Select 的方法,可以帮助我们完成这些事情:

# 导入 Select 类
from selenium.webdriver.support.ui import Select

# 找到 name 的选项卡
select = Select(driver.find_element_by_name('status'))

# 
select.select_by_index(1)
select.select_by_value("0")
select.select_by_visible_text(u"未审核")

以上是三种选择下拉框的方式,它可以根据索引来选择,可以根据值来选择,可以根据文字来选择。注意:

  • index 索引从 0 开始
  • value是option标签的一个属性值,并不是显示在下拉框中的值
  • visible_text是在option标签文本的值,是显示在下拉框的值

全部取消选择怎么办呢?很简单:

select.deselect_all()

(4) 弹窗处理

当你触发了某个事件之后,页面出现了弹窗提示,处理这个提示或者获取提示信息方法如下:

alert = driver.switch_to_alert()

(5) 页面切换

一个浏览器肯定会有很多窗口,所以我们肯定要有方法来实现窗口的切换。切换窗口的方法如下:

driver.switch_to.window("this is window name")

也可以使用 window_handles 方法来获取每个窗口的操作对象。例如:

for handle in driver.window_handles:
    driver.switch_to_window(handle)

(6) 页面前进和后退

操作页面的前进和后退功能:

driver.forward()     #前进
driver.back()       # 后退

(7) Cookies

获取页面每个Cookies值,用法如下

for cookie in driver.get_cookies():
    print "%s -> %s" % (cookie['name'], cookie['value'])

删除Cookies,用法如下

# By name
driver.delete_cookie("CookieName")

# all
driver.delete_all_cookies()

四、JavaScript执行器

这一节我们来讨论在Python Selenium WebDriver中如何使用JavaScript来单击或对Web元素执行操作。

使用JavaScript的潜在操作:

  • 获取元素文本或属性
  • 找到一个元素
  • 对元素做一些操作,比如 click()
  • 更改元素的属性
  • 滚动到网页上的元素或位置
  • 等到页面加载完毕

(1)如何在WebDriver中使用JavaScript

Python Selenium WebDriver提供了一个内置方法:

driver.execute_script("some javascript code here");

我们可以通过两种方式在浏览器中执行JavaScript。

方法1:在文档根级别执行JavaScript

在这种情况下,我们使用JavaScript提供的方法捕获我们想要使用的元素,然后在其上声明一些操作并使用WebDriver执行此JavaScript。执行时,WebDriver会将JavaScript语句注入浏览器,脚本将执行该任务。例如:

jS = "document.getElementsByName('username')[0].click();"driver.execute_script(javaScript)

第1步:我们正在使用JavaScript检查并通过属性“名称”获取元素。(另外,可以使用’id’和’class’属性。)

第2步:使用JavaScript声明并对元素执行单击操作。

第3步:调用execute_script()方法并将我们创建的JavaScript作为字符串值传递。

方法2:在元素级别执行JavaScript

在这种情况下,我们使用WebDriver捕获我们想要使用的元素,然后使用JavaScript在其上声明一些操作,并通过将web元素作为参数传递给JavaScript来使用WebDriver执行此JavaScript。

userName = driver.find_element_by_xpath("//button[@name='username']")
driver.execute_script("arguments[0].click();", userName)

第1步:使用WebDriver提供的方法检查和捕获元素:find_element_by_xpath
第2步:使用JavaScript声明并对元素执行单击操作:arguments[0].click() 第3步:execute_script()
第3步:execute_script() 使用我们创建的JavaScript语句作为字符串值调用方法,并使用WebDriver作为参数捕获Web元素:driver.execute_script(“arguments[0].click();”,
userName)

上面两行代码可以缩短为下面的格式,我们使用WebDriver找到一个元素,声明一些JavaScript函数,并使用WebDriver执行JavaScript。

driver.execute_script("arguments[0].click();",driver.find_element_by_xpath("//button[@name='username']"))

此外,您的语句中可以有多个JavaScript操作:

userName = driver.find_element_by_xpath("//button[@name='username']")
password = driver.find_element_by_xpath("//button[@name='password']")
driver.execute_script("arguments[0].click();arguments[1].click();", userName, password)
#driver.execute_script("arguments[1].click();arguments[0].click();", userName, password)

在这种情况下,web元素的顺序的使用很重要。

实战:
from selenium import webdriver

driver = webdriver.PhantomJS()
driver.get("https://www.baidu.com/")

# 将搜索输入框标红
js = "var q=document.getElementById(\"kw\");q.style.border=\"2px solid red\";"
driver.execute_script(js)

#隐藏百度图片
img = driver.find_element_by_xpath("//*[@id='lg']/img")
driver.execute_script('$(arguments[0]).fadeOut()',img)

# 向下滚动到页面底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

# 向下滚动10000像素
js = "document.body.scrollTop=10000"
#js="var q=document.documentElement.scrollTop=10000"
driver.execute_script(js)

#从Web元素中获取值
print driver.execute_script('return document.getElementById("fsr").innerText')

driver.quit()

在使用driver.execute_script从Web元素中获取值报出WebDriver异常:
selenium.common.exceptions.WebDriverException: Message: unknown error: Cannot read property ‘innerText’ of null
解决方法:JavaScript不能找到要操作的元素,检查元素是否存在。


五、页面等待

现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个元素完全加载出来了。如果实际页面等待时间过长导致某个dom元素还没出来,但是你的代码直接使用了这个WebElement,那么就会抛出NullPointer的异常。

为了避免这种元素定位困难而且会提高产生 ElementNotVisibleException 的概率。所以 Selenium 提供了两种等待方式,一种是隐式等待,一种是显式等待。

隐式等待是等待特定的时间,显式等待是指定某一条件直到这个条件成立时继续执行。

A. 隐式等待

隐式等待比较简单,就是简单地设置一个等待时间,单位为秒。

from selenium import webdriver

driver = webdriver.Chrome()
driver.implicitly_wait(10) # seconds
driver.get("http://www.xxxxx.com/loading")
myDynamicElement = driver.find_element_by_id("myDynamicElement")

当然如果不设置,默认等待时间为0。

B. 显式等待

显式等待指定某个条件,然后设置最长等待时间。如果在这个时间还没有找到元素,那么便会抛出异常了。

from selenium import webdriver
from selenium.webdriver.common.by import By
# WebDriverWait 库,负责循环等待
from selenium.webdriver.support.ui import WebDriverWait
# expected_conditions 类,负责条件出发
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("http://www.xxxxx.com/loading")
try:
    # 页面一直循环,直到 id="myDynamicElement" 出现
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "myDynamicElement"))
    )
finally:
    driver.quit()

如果不写参数,程序默认会 0.5s 调用一次来查看元素是否已经生成,如果本来元素就是存在的,那么会立即返回。

下面是一些内置的等待条件,你可以直接调用这些条件,而不用自己写某些等待条件了。

title_is
title_contains
presence_of_element_located
visibility_of_element_located
visibility_of
presence_of_all_elements_located
text_to_be_present_in_element
text_to_be_present_in_element_value
frame_to_be_available_and_switch_to_it
invisibility_of_element_located
element_to_be_clickable – it is Displayed and Enabled.
staleness_of
element_to_be_selected
element_located_to_be_selected
element_selection_state_to_be
element_located_selection_state_to_be
alert_is_present

四、实战演示

登陆斗鱼(演示网站模拟登录):

#coding=utf-8
import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

class Douyu():
    def __init__(self):
        self.url = "https://www.douyu.com/"
        self.driver = webdriver.PhantomJS()

    def log_in(self):
        self.driver.get(self.url)
        time.sleep(3)#睡3分钟,等待页面加载
        self.driver.save_screenshot("0.jpg")
        #输入账号
        self.driver.find_element_by_xpath('//*[@id="form_email"]').send_keys("[email protected]")
        #输入密码
        self.driver.find_element_by_xpath('//*[@id="form_password"]').send_keys("xxxx")
        #点击登陆
        self.driver.find_element_by_class_name("bn-submit").click()
        time.sleep(2)
        self.driver.save_screenshot("douyu.jpg")
        #输出登陆之后的cookies
        print(self.driver.get_cookies())

    def __del__(self):
        '''调用内建的稀构方法,在程序退出的时候自动调用
        类似的还可以在文件打开的时候调用close,数据库链接的断开
        '''
        self.driver.quit()

if __name__ == "__main__":
    douyu = Douyu() #实例化
    douyu.log_in()  #之后调用登陆方法

爬取斗鱼直播平台的所有房间信息(演示动态页面模拟点击):

#coding=utf-8
from selenium import webdriver
import json
import time

class Douyu:
    # 1.发送首页的请求
    def __init__(self):
        self.driver = webdriver.PhantomJS()
        self.driver.get("https://www.douyu.com/directory/all") #请求首页

    #获取没页面内容
    def get_content(self):
        time.sleep(3) #每次发送完请求等待三秒,等待页面加载完成
        li_list = self.driver.find_elements_by_xpath('//ul[@id="live-list-contentbox"]/li')
        contents = []
        for i in li_list: #遍历房间列表
            item = {}
            item["img"] = i.find_element_by_xpath("./a//img").get_attribute("src") #获取房间图片
            item["title"] = i.find_element_by_xpath("./a").get_attribute("title") #获取房间名字
            item["category"] = i.find_element_by_xpath("./a/div[@class='mes']/div/span").text #获取房间分类
            item["name"] = i.find_element_by_xpath("./a/div[@class='mes']/p/span[1]").text #获取主播名字
            item["watch_num"] = i.find_element_by_xpath("./a/div[@class='mes']/p/span[2]").text #获取观看人数
            print(item)
            contents.append(item)
        return contents
    #保存本地
    def save_content(self,contents):
        f = open("douyu.txt","a")
        for content in contents:
            json.dump(content,f,ensure_ascii=False,indent=2)
            f.write("\n")
        f.close()

    def run(self):
        #1.发送首页的请求
        #2.获取第一页的信息
        contents = self.get_content()
            #保存内容
        self.save_content(contents)
        #3.循环  点击下一页按钮,直到下一页对应的class名字不再是"shark-pager-next"
        while self.driver.find_element_by_class_name("shark-pager-next"): #判断有没有下一页
            #点击下一页的按钮
            self.driver.find_element_by_class_name("shark-pager-next").click() #
            # 4.继续获取下一页的内容
            contents = self.get_content()
            #4.1.保存内容
            self.save_content(contents)

if __name__ == "__main__":
    douyu = Douyu()
    douyu.run()

后期内容提要:

  • [Python爬虫] 九、机器视觉与机器图像识别之Tesseract
  • [Python爬虫] 十、Scrapy 框架

如果您有任何疑问或者好的建议,期待你的留言与评论!

发布了82 篇原创文章 · 获赞 540 · 访问量 73万+

猜你喜欢

转载自blog.csdn.net/deng_xj/article/details/104322155