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