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的使用
Selenium是一个自动化测试工具,利用它可以驱动浏览器执行特定的动作,如点击、下拉等操作,同时还可以获取浏览器当前呈现的页面的源代码,做到可见即可爬。对于一些JavaScript动态渲染的页面来说,此时抓取方式非常有效。
先看一个示例:
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和页面源代码:
DevTools listening on ws://127.0.0.1:52552/devtools/browser/a5ff675d-c41a-4809-a71f-67814a67b8a1
[7052:6392:1020/093003.297:ERROR:gpu_process_transport_factory.cc(980)] Lost UI shared context.
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=Python&rsv_pq=c63cb3760003ea74&rsv_t=69f18QWYwjSN%2FD%2BHYr4wWuMN%2FV8XR0tVGonVPWvzDmEy%2FDN5Iyhp0qul8GU&rqlang=cn&rsv_enter=1&rsv_sug3=6&rsv_sug2=0&inputT=281&rsv_sug4=281
[{'domain': '.baidu.com', 'httpOnly': False, 'name': 'H_PS_PSSID', 'path': '/', 'secure': False, 'value': '1448_21114_22158'}, {'domain': '.baidu.com', 'httpOnly': False, 'name': 'delPer', 'path': '/', 'secure': False, 'value': '0'}, {'domain': '.baidu.com', 'expiry': 3687482651.658103, 'httpOnly': False, 'name': 'BAIDUID', 'path': '/', 'secure': False, 'value': '8B75792045F67DD6D8F9BEA0100E2997:FG=1'}, {'domain': '.baidu.com', 'expiry': 3687482651.658243, 'httpOnly': False, 'name': 'BIDUPSID', 'path': '/', 'secure': False, 'value': '8B75792045F67DD6D8F9BEA0100E2997'}, {'domain': '.baidu.com', 'expiry': 3687482651.658326, 'httpOnly': False, 'name': 'PSTM', 'path': '/', 'secure': False, 'value': '1539999003'}, {'domain': 'www.baidu.com', 'httpOnly': False, 'name': 'BD_HOME', 'path': '/', 'secure': False, 'value': '0'}, {'domain': '.baidu.com', 'expiry': 1540085407.545845, 'httpOnly': False, 'name': 'BDORZ', 'path': '/', 'secure': False, 'value': 'B490B5EBF6F3CD402E515D22BCDA1598'}, {'domain': 'www.baidu.com', 'expiry': 1540863005, 'httpOnly': False, 'name': 'BD_UPN', 'path': '/', 'secure': False, 'value': '12314753'}, {'domain': 'www.baidu.com', 'httpOnly': False, 'name': 'BD_CK_SAM', 'path': '/', 'secure': False, 'value': '1'}, {'domain': '.baidu.com', 'httpOnly': False, 'name': 'PSINO', 'path': '/', 'secure': False, 'value': '2'}, {'domain': 'www.baidu.com', 'expiry': 1540001599, 'httpOnly': False, 'name': 'H_PS_645EC', 'path': '/', 'secure': False, 'value': '0df4UB%2Fe5SQx6iVzGA%2Bv3o6QiQa40d5JF8pbaMuuARqGJUMq1KLPVl0A86k'}, {'domain': 'www.baidu.com', 'httpOnly': False, 'name': 'BDSVRTM', 'path': '/', 'secure': False, 'value': '143'}]
<!DOCTYPE html><!--STATUS OK-->...</html>
可以看到,我们得到的当前URL、Cookies和源代码都是浏览器中的真实内容。
所以说,如果用Selenium来驱动浏览器加载页面的话,就可以直接拿到JavaScript渲染的结果了,不用担心使用的是什么加密系统。
声明浏览器对象
Selenium支持非常多的浏览器,如Chrome、FireFox、Edge等,还有Android、BlackBerry等手机端的浏览器。另外,也支持无界面浏览器PhantomJS。
此外,我们可以用如下方式初始化:
from selenium import webdriver
browser = webdriver.Chrome()
browser = webdriver.Firefox()
browser = webdriver.Edge()
browser = webdriver.PhantomJS()
browser = webdriver.Safari()
这样就完成了浏览器对象的初始化并将其赋值为browser对象。接下来,我们要做的就是调用browser对象,让其执行各个动作以模拟浏览器操作。
访问页面
我们可以 用get()方法来请求网页,参数传入链接URL即可。比如,这里用get()方法访问淘宝,然后打印出源代码,代码如下:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.taobao.com')
print(browser.page_source)
browser.close()
运行后发现,弹出了Chrome浏览器并自动访问了淘宝,然后控制台输出了淘宝页面的源代码,随后浏览器关闭。
通过这几行简单的代码,我们可以实现浏览器的驱动并获取网页源码,非常便捷。
查找节点
Selenium可以驱动浏览器完成各种操作,比如填充表单、模拟点击等。比如,我们想要完成向某个输入框输入文字的操作,总需要知道这个输出框在哪里吧?而Selenium提供了一系列查找节点的方法,我们可以用这些方法来获取想要的节点,以便下一步执行一些动作或者提取信息。
单个节点
比如,想要从淘宝页面中提取搜索框这个节点,首先要观察它的源代码,可以发现,它的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获取,它们返回的结果完全一致。运行结果如下:
<selenium.webdriver.remote.webelement.WebElement (session="a75be51dfe8994a873b8f8eb2650f74c", element="0.9106846181007426-1")>
<selenium.webdriver.remote.webelement.WebElement (session="a75be51dfe8994a873b8f8eb2650f74c", element="0.9106846181007426-1")>
<selenium.webdriver.remote.webelement.WebElement (session="a75be51dfe8994a873b8f8eb2650f74c", element="0.9106846181007426-1")>
可以看到,这3个节点都是WebElement类型,是完全一致的。
这里列出所有获取单个节点方法:
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()
实际上,这种查找方式的功能和上面列举的查找函数完全一致,不过参数更加灵活。
多个节点
如果查找的目标在网页中只有一个,那么完全可以用find_element()方法。但如果有多个节点,再用find_element()方法查找,就只能得到第一个节点了。如果要查找所有满足条件的节点,需要用find_elements()这样的方法。
对于淘宝左侧的导航栏:
可以使用如下方法实现:
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="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-1")>, <selenium.webdriver.remote.webelement.WebElement (session="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-2")>, <selenium.webdriver.remote.webelement.WebElement (session="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-3")>, <selenium.webdriver.remote.webelement.WebElement (session="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-4")>, <selenium.webdriver.remote.webelement.WebElement (session="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-5")>, <selenium.webdriver.remote.webelement.WebElement (session="92843601f318210e7fb8bf174cf4a1cf", element="0.18315580906738105-6")>]
可以看到,得到的内容变成了列表,列表中的每个及诶单都是WebElement类型。
节点交互
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('iPhone')
time.sleep(1)
input.clear()
input.send_keys('iPad')
button = browser.find_element_by_class_name('btn-search')
button.click()
这里首先驱动浏览器打开淘宝,然后用find_element_by_id()方法获取输入框,然后用send_keys()方法输入iPhone文字,等待一秒后用clear()方法清空输入框,再次调用send_keys()方法输入iPad文字,之后再用find_element_by_class_name()方法获取搜索按钮,最后调用click()方法完成搜索动作。