Ajax分析和抓取方式,是JavaScript动态渲染页面的一种情形,可使用 requests 或 urllib 爬取数据。JavaScript动态渲染的页面不是只有Ajax一种,比如中国青年网 http://news.youth.cn/gn/ 的分页部分由JavaScript生成的,不是原始的HTML代码,但是不包含Ajax请求。又比如ECharts的官方实例 http://echarts.baidu.com/demo.html#bar-negative ,其图形都是经过JavaScript计算后生成的。另外的淘宝页面,有Ajax获取的数据,但是Ajax接口含有很多加密参数,不容易找出规律,很难直接分析Ajax来获取。
这些问题可以通过使用模拟浏览器运行的方式来实现,这样在浏览器中看到什么样,抓取的源码就是什么样,也就是可见即可爬。不用管网页内部的JavaScript用的什么算法渲染页面,也不用管网页后台的Ajax接口到底有哪些参数。
Python有许多模拟浏览器运行的库,如Selenium、Splash、PyV8、Ghost等。下面了解下Selenium和Splash的用法,以应对动态渲染的页面。
一、 Selenium的使用
Selenium是自动化测试工具,它可以驱动浏览器执行特定的动作,如点击、下拉等操作,同时还可以获取浏览器当前呈现的页面的源代码,做到可见即可爬。对于JavaScript动态渲染的页面,这种抓取方法非常有效。
下面以Chrome为例说明Selenium的用法。首先要正确安装Chrome浏览器并配置好ChromeDriver。还要安装好Python的Selenium库。
1、 开始使用
首先看下Selenium大致有哪些功能。例如下面代码所示:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
browser = webdriver.Chrome()
try:
browser.get('https://www.baidu.com')
input = browser.find_element_by_id('kw')
input.send_keys('Python')
input.send_keys(Keys.ENTER)
wait = WebDriverWait(browser, 10)
wait.until(EC.presence_of_element_located((By.ID, 'content_left')))
print(browser.current_url)
print(browser.get_cookies())
print(browser.page_source)
finally:
browser.close()
运行这段代码后会自动弹出一个Chrome浏览器,浏览器自动跳转到百度首页,然后在搜索框中输入Python,接着跳转到搜索结果页。搜索结果加载出来后,控制台分别会输出当前的URL、当前的Cookies和网页源代码。Cookies以字典列表形式输出。
这就是使用Selenium驱动浏览器加载网页拿到JavaScript渲染的结果,不必担心是什么加密系统。
2、 声明浏览器对象
Selenium支持的浏览器非常多,如Chrome、Firefox、Edge等,还有Android、BlackBerry等手机端浏览器。还支持无界面浏览器PhangtomJS。可用下面这些方式初始这些浏览器对象:
from selenium import webdriver
browser = webdriver.Chrome()
browser = webdriver.Firefox()
browser = webdriver.Edge()
browser = webdriver.PhantomJS()
browser = webdriver.Safari()
这就是在初始化浏览对象并将其赋值为browser对象。接下来可以调用browser对象,让其执行各个动作以模拟浏览器操作。
3、 访问页面
使用前面创建的浏览器对象的 get() 方法,参数是要访问的链接URL。例如访问淘宝首页并输出源代码,示例如下:
from selenium import webdriver
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless') # 无界面模式
browser = webdriver.Chrome(chrome_options=chrome_options)
browser.get('https://www.taobao.com')
print(browser.current_url)
print(browser.page_source)
browser.close()
运行代码后不弹出Chrome浏览器并自动访问淘宝,然后在控制台输出淘宝的网址和页面的源代码,随后关闭浏览器。这几行简单的代码就实现了浏览器的驱动并获取网页源代码,非常便捷。
4、 查找节点
Selenium还可以驱动浏览器完成各种操作,如填充表单、模拟点击等。比如要完成向某个输入框输入文字的操作,首先要找到输入框。Selenium有提供一系列查找节点的方法,可用这些方法获取想要的节点,以便下一步执行一些动作或者提取信息。
4.1、 提取单个节点
例如要提取淘宝页面中的搜索框节点,需要先观察它的源代码。如图1-1所示。
图1-1 搜索框源代码
从源代码可以看到,搜索框节点的id是q,name也是q。另外还有许多其他属性,此时可用多种方式获取它。比如,find_element_by_name()是根据name值获取,find_element_by_id()是根据id获取。还有根据 XPath、CSS 选择器获取的方式。代码示例如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.taobao.com')
input_first = browser.find_element_by_id('q')
input_second = browser.find_element_by_css_selector('#q')
input_third = browser.find_element_by_xpath('//*[@id="q"]')
print(input_first, input_second, input_third)
browser.close()
这里用了3种方式获取输入框,分别是根据ID、CSS选择器和XPath获取,3种方式返回的结果完全一致,并且都是WebElement类型。输出如下所示:
<selenium.webdriver.remote.webelement.WebElement (session="5c88916914b54ea71fd04dc64adf2bc1", element="0.056290961173190324-1")>
<selenium.webdriver.remote.webelement.WebElement (session="5c88916914b54ea71fd04dc64adf2bc1", element="0.056290961173190324-1")>
<selenium.webdriver.remote.webelement.WebElement (session="5c88916914b54ea71fd04dc64adf2bc1", element="0.056290961173190324-1")>
获取单个节点的方法有下面这些:
find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector
Selenium的通用方法 find_element(),需传入两个参数:查找方式 By和值。是find_element_by_id()方法的通用函数版本。如find_element_by_id(id)等价于find_element(By.ID, id),两种方法得到的结果是一样的。示例如下:
from selenium import webdriver
from selenium.webdriver.common.by import By
browser = webdriver.Chrome()
browser.get("https://www.taobao.com")
input_first = browser.find_element(By.ID, 'q')
print(input_first)
browser.close()
这种查找方式的参数更灵活,功能与前面列举的是一样的。
4.2、 多个节点
find_element() 方法只能查找单个节点,就算有多个节点,也只能得到第一个节点。节点类型是:WebElement。
find_elements() 方法可以查找所有满足条件的节点。结果是列表类型,每个节点类型是:WebElement
例如查找淘宝左侧导航条的所有条目,通过源代码分析可知,每一个导航条都是用 li 标签包起来的,这些导航条都有一个共同的父标签
ul,ul标签有class属性,其属性值是service-bd。可先根据class属性值找到ul标签,继而找到下面的子标签即可找到左侧导航条
的所有节点。代码如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get("https://www.taobao.com")
lis = browser.find_elements_by_css_selector(".service-bd li")
print(lis)
browser.close()
输出如下所示:
[<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-1")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-2")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-3")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-4")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-5")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-6")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-7")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-8")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-9")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-10")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-11")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-12")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-13")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-14")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-15")>,
<selenium.webdriver.remote.webelement.WebElement (session="4b1d6b1ad2469591d0fa83017d2aa992", element="0.7683288407073237-16")>]
输出结果是列表类型,列表中的每个节点都是WebElement类型。获取多个节点的所有方法如下:
find_elements_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_elements()
使用通用方法find_elements()方法选择时,可这样写:
lis = browser.find_elements(By.CSS_SELECTOR, '.service-bd li')
5、 节点交互
Selenium可以让浏览器模拟执行一些动作。常见用法有:输入文字用 send_keys() 方法,清空文字用 clear() 方法,点击按钮用 click()方法。基本用法如下:
from selenium import webdriver
import time
browser = webdriver.Chrome()
browser.get("https://www.taobao.com")
input = browser.find_element_by_id('q') # 获取输入框
input.send_keys("Mate20") # 输入Mate20
time.sleep(1)
input.clear() # 等待1秒后清空输入框
input.send_keys("P20") # 重新输入P20
button = browser.find_element_by_class_name('btn-search') # 获取搜索按钮
button.click() # 点击搜索
上面代码执行过程:首先驱动浏览器打开淘宝网站,然后用find_element_by_id()方法获取输入框,接着用send_keys()方法输入Mate20文字,等待1秒后用clear()方法清空输入框,再次调用send_keys()方法输入P20,之后再用find_element_by_class_name()方法获取搜索按钮,最后调用click()方法完成搜索动作。
官方文档的交互动作介绍:
http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement
6、 动作链
前面的交互动作是针对某个节点执行的。例如,对于输入框,可调用它的输入文字和清空文字方法;对于按钮,可调用它的点击方法。有一些操作,它们没有特定的执行对象,比如鼠标拖曳、键盘按键等,这些动作用另一种方式来执行,就是动作链。
例如要实现一个节点的拖曳操作,将某个节点从一处拖曳到另一处,可像下面这样实现:
from selenium import webdriver
from selenium.webdriver import ActionChains
browser = webdriver.Chrome()
url = 'http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
browser.get(url)
browser.switch_to.frame('iframeResult')
source = browser.find_element_by_css_selector('#draggable')
target = browser.find_element_by_css_selector('#droppable')
actions = ActionChains(browser)
actions.drag_and_drop(source, target)
actions.perform()
运行这段代码,首先打开网页一个拖曳实例,接着选中要拖曳的节点和拖曳到的目标节点,再接着声明ActionChains对象并将其赋值为actions变量,然后通过调用actions变量的drag_and_drop()方法,再调用perform()方法执行动作,此时就完成拖曳操作。
动作链接官方文档:
http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains
7、 执行JavaScript
某些操作,SeleniumAPI 没有提供。比如,下拉进度条操作就没有,它可以使用 execute_script() 方法直接模拟运行JavaScript来实现。示例如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
browser.execute_script('alert("To Bottom")')
代码中利用execute_script()方法将进度条下拉到最底部,然后弹出alert提示框。有了这个方法,基本上API没有提供的所有功能都可以用执行JavaScript的方式来实现。
8、 获取节点信息
page_source属性可获取网页源代码,解析库(有正则表达式、Beautiful Soup、pyquery等)用来提取信息。Selenium有提供节点
选择方法,返回的是WebElement类型,对应也有相关的方法和属性直接提取节点信息,如属性、文本等。
8.1、 获取属性
get_attribute()方法获取节点属性,需要先选中节点,代码示例如下:
from selenium import webdriver
browser = webdriver.Chrome()
url = 'https://www.zhihu.com/explore'
browser.get(url)
logo = browser.find_element_by_id('zh-top-link-logo')
print(logo)
print(logo.get_attribute('class'))
运行程序,驱动浏览器打开知乎页面,然后获取知乎的logo节点,最后打印出class。输出信息如下所示:
<selenium.webdriver.remote.webelement.WebElement (session="db40cefb1cf4ac278c6832791fe74b26", element="0.46332722296830897-1")>
zu-top-link-logo
这样通过get_attribute()方法传入属性名参数就可获取到属性值。
8.2、 获取文本值
每个WebElement节点都有text属性,调用该属性可获取节点内部的文本信息。相当于Beautiful Soup的get_text()方法、pyquery的text()方法,示例如下:
from selenium import webdriver
browser = webdriver.Chrome() # 驱动打开浏览器
url = 'https://www.zhihu.com/explore'
browser.get(url) # 打开知乎页面
input = browser.find_element_by_class_name('zu-top-add-question') # 获取提问节点
print(input.text) # 输出:提问
8.3、 获取id、位置、标签名和大小
WebElement节点的其它属性如下:
id属性:获取节点id
location属性:获取该节点在页面中的相对位置
tag_name属性:获取标签名称
size属性:获取节点的大小,也是宽高
这几个属性在某些时候很有用的。
from selenium import webdriver
browser = webdriver.Chrome() # 驱动打开浏览器
url = 'https://www.zhihu.com/explore'
browser.get(url) # 打开知乎页面
input = browser.find_element_by_class_name('zu-top-add-question') # 获取提问节点
print(input.id) # 获取节点id
print(input.location) # 节点在页面中的相对位置
print(input.tag_name) # 标签名称
print(input.size) # 标签的宽高
输出如下所示:
0.46332722296830897-2
{'x': 758, 'y': 7}
button
{'height': 32, 'width': 66}
9、 切换Frame
网页中有一种节点叫作 iframe,也是子Frame,相当于页面的子页面,子页面结构与外部网页结构完全一致。Selenium打开页面默认是在父级Frame里面操作,页面中如果有子Frame,它是不能获取到子Frame里面的节点。这时可使用switch_to.frame()方法可切换frame。示例如下:
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
browser = webdriver.Chrome()
url = 'http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
browser.get(url)
browser.switch_to.frame('iframeResult') # 切换到子frame
try:
# 获取父级Frame的logo节点,不成功就抛出NoSuchElementException异常
logo = browser.find_element_by_class_name('logo')
except NoSuchElementException:
print('NO LOGO')
browser.switch_to.parent_frame() # 切换回父级Frame
logo = browser.find_element_by_class_name('logo') # 获取logo节点
print(logo)
print(logo.text) # 输出父级logo节点的文本
输出如下所示:
NO LOGO
<selenium.webdriver.remote.webelement.WebElement (session="6e2ef8fd8e5d576d31cf86557ad39b67", element="0.4057476934894335-2")>
RUNOOB.COM
代码中switch_to.frame()方法切换到子Frame,接着find_element_by_class_name('logo')获取父级Frame的logo节点,未能获取就抛出异常NoSuchElementException。切换回父级Frame,重新获取logo节点,可以成功获取。如果页面中有子Frame时,要获取子Frame的节点,要先调用switch_to.frame()方法切换到对应的Frame后再进行操作。
10、 延时等待
在Selenium中,get()方法在网页框架加载结束后结束执行,此时获取page_source,并不是浏览器完全加载完成的页面,如果有额外的Ajax请求,在网页源代码中也不一定能成功获取到。所以需要延时等待一定时间,确保节点已经加载出来。
延时等待有两种方式:隐式等待;显式等待。
10.1、 隐式等待,implicitly_wait()
使用隐式等待测试时,如果Selenium没有在DOM中找到节点,将继续等待,超出设定时间后,就抛出找不到节点的异常。也就是说,在查找节点时节点没有立即出现时,隐式等待将等待一段时间再查找DOM,默认等待时间是0。示例如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.implicitly_wait(10) # 调用隐式等待,等待10秒
browser.get('https://www.zhihu.com/explore')
input = browser.find_element_by_class_name('zu-top-add-question')
print(input)
10.2、 显式等待,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
browser = webdriver.Chrome()
browser.get('https://www.taobao.com/')
wait = WebDriverWait(browser, 10) # 参数:等待对象及时长
# 在等待时间内获取输入框节点,通过ID查找
input = wait.until(EC.presence_of_element_located((By.ID, 'q')))
# 在等待时间内获取点击按钮节点,通过CSS选择器查找
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-search')))
print(input, button)
在代码中引入WebDriverWait对象,指定最长等待时间为10秒,接着调用它的 until() 方法,传入要等待条件 expected_conditions。这里传入了 presence_of_element_located 这个条件,表示节点出现的意思,其参数是节点的定位元组,也就是ID为q的节点搜索框。在10秒内ID为q的节点(即搜索框)成功加载,就返回该节点;如果超过10秒还没有加载出来,就抛出异常。
按钮的等待条件是 element_to_be_clickable,也就是可点击。参数(By.CSS_SELECTOR, '.btn-search')意思是查找按钮时查找CSS选择器为 .btn-search 的按钮,如果10秒内它是可点击,就成功加载出来并返回这个按钮节点;如果10秒还不可点击,就是没有加载出来,则抛出异常。运行这段代码,在网速好的情况下可正常加载出来,并且输出如下:
<selenium.webdriver.remote.webelement.WebElement (session="12fc5fa8bc80295340f5fd22433c6ec1", element="0.5824054028692756-1")>
<selenium.webdriver.remote.webelement.WebElement (session="12fc5fa8bc80295340f5fd22433c6ec1", element="0.5824054028692756-2")>
从输出可知,输出了两个节点,都是WebElement类型。如果网络有问题就抛出异常。在这段代码用到了两个等待条件,这些等待条件还
有很多,比如判断标题内容,判断某个节点内是否出现某文字等。表1-1是所有的等待条件。
表1-1 等待条件及其含义
更多等待条件参数及用法,参考官方文档:
http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.expected_conditions
11、 前进和后退
在使用浏览器时有前进和后退功能,Selenium 也可完成这个操作。使用 back() 方法后退,使用 forward() 方法前进。
import time
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.taobao.com/') # 连续访问3个页面
browser.get('https://www.baidu.com/')
browser.get('https://www.sina.com.cn/')
browser.back() # 后退到百度页面
time.sleep(1)
browser.forward() # 前进到sina页面
time.sleep(3)
browser.close()
这段代码连续访问3个页面后调用back()方法回到第二个页面,接下来调用forward()方法又前进到第三个页面。
12、 对Cookies的操作
对Cookies进行操作方法主要有:获取、添加、删除等。
get_cookies()方法:获取所有Cookies。
add_cookie(字典参数):添加cookie,参数是字典。
delete_all_cookies():删除所有的cookies。
示例如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
print("第一次cookies:", browser.get_cookies()) # 获取cookies,接着下面添加cookies
browser.add_cookie({'name': 'name', 'domain': 'www.zhihu.com', 'value': 'michael'})
print("第二次cookies:", browser.get_cookies()) # 再次获取cookies,核实是否添加成功
browser.delete_all_cookies() # 删除所有cookies
print("第三次cookies:", browser.get_cookies()) # 核实是否完全删除cookies
browser.close()
输出如下,第二次输出的cookies包含了添加的cookie:
第一次cookies: [{'domain': '.zhihu.com', 'httpOnly': False, ...}, ......]
第二次cookies: [{'domain': '.zhihu.com', 'httpOnly': False, ...}, ......, {'domain': 'www.zhihu.com', 'name': 'name', 'value': 'michael'}]
第三次cookies: []
13、 Selenium模拟开启选项卡
比如第一个选项卡打开百度网页,第二个选项卡打开淘宝网页。这些操作也可用Selenium来对选项卡进行操作。
window.open()是JavaScript语句的开启一个选项卡。
execute_script('window.open()') 执行JavaScript语句开启一个选项卡。
window_handles获取当前开启的选项卡,结果是选项卡代号列表。window_handles[0]是指选项卡列表中的第1个选项卡。
switch_to.window(选项卡参数):切换到选项卡参数指定的选项卡。
代码示例如下:
import time
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.execute_script('window.open()') # 开启一个新选项卡
print(browser.window_handles) # 输出当前开启的选项卡
browser.switch_to.window(browser.window_handles[1]) # 切换到新选项卡,也是第2个选项卡
browser.get('https://www.taobao.com') # 在第2个选项卡中打开淘宝页面
time.sleep(1)
browser.switch_to.window(browser.window_handles[0]) # 切换到第1个选项卡
browser.get('https://www.sina.com.cn') # 在第1个选项卡打开新浪页面
browser.close() # 关闭当前选项卡,也是第1个选项卡
输出如下所示:
['CDwindow-AAC4839C9E18D601645AC4D868050F5D', 'CDwindow-37138AB8D7A2E7501E48014FB506977C']
14、 异常处理
使用Selenium的时候,可能会遇到访问超时异常、节点未找到异常等情况。出现异常程序就中断运行。为了避免程序中断执行,可使用try except语句捕获各种异常。
使用Selenium时,常遇到的异常是:TimeoutException(超时异常),NoSuchElementException(节点未找到异常),此外还可用
WebDriverException异常捕获所有由Selenium产生的异常。异常模块所在位置是:selenium.common.exceptions。
导入WebDriverException的命令:
from selenium.common.exceptions import WebDriverException
Selenium的异常类官方文档参考:
http://selenium-python.readthedocs.io/api.html#module-selenium.common-exceptions