1. 程式人生 > >Python爬蟲周記之案例篇——基金凈值Selenium動態爬蟲

Python爬蟲周記之案例篇——基金凈值Selenium動態爬蟲

模擬瀏覽器 mode flag sch quest expec clear until taf

在成功完成基金凈值爬蟲的爬蟲後,簡單了解爬蟲的一些原理以後,心中不免產生一點困惑——為什麽我們不能直接通過Request獲取網頁的源代碼,而是通過查找相關的js文件來爬取數據呢?

有時候我們在用requests抓取頁面的時候,得到的結果可能和瀏覽器中看到的不一樣:瀏覽器中可以看到正常顯示的頁面數據,但是使用requests得到的結果並沒有。

這是因為requests獲取的都是原始的HTML文檔,而瀏覽器中的頁面則是經過JavaScript處理數據後生成的結果,這些數據來源多種,可能是通過Ajax加載的,可能是包含在HTML文檔中的,也可能是經過JavaScript和特定算法計算後生成的。而依照目前Web發展的趨勢,網頁的原始HTML文檔不會包含任何數據,都是通過Ajax等方法統一加載後再呈現出來,這樣在Web開發上可以做到前後端分離,而且降低服務器直接渲染頁面帶來的。通常,我們把這種網頁稱為動態渲染頁面。

之前的基金凈值數據爬蟲就是通過直接向服務器獲取數據接口,即找到包含數據的js文件,向服務器發送相關的請求,才獲取文件。

那麽,有沒有什麽辦法可以直接獲取網頁的動態渲染數據呢?答案是有的。

我們還可以直接使用模擬瀏覽器運行的方式來實現動態網頁的抓取,這樣就可以做到在瀏覽器中看倒是什麽樣,抓取的源碼就是什麽樣,即實現——可見即可爬。

Python提供了許多模擬瀏覽器運行的庫,如:Selenium、Splash、PyV8、Ghost等。本文將繼續以基金凈值爬蟲為例,Selenium對其進行動態頁面爬蟲。

環境

tools

1、Chrome及其developer tools

2、python3.7

3、PyCharm

python3.7中使用的庫

1、Selenium

2、pandas

3、random

4、 time

5、os

系統

Mac OS 10.13.2

Selenium基本功能及使用

準備工作

  • Chrome瀏覽器
  • Selenium庫
    • 可直接通過pip安裝,執行如下命令即可:
    • pip install selenium
  • ChromDriver配置
    • Selenium庫是一個自動化測試工具,需要瀏覽器來配合使用,我們主要介紹Chrome瀏覽器及ChromeDriver驅動的配置,只有安裝了ChromeDriver並配置好對應環境,才能驅動Chrome瀏覽器完成相應的操作。Windows和Mac下的安裝配置方法略有不同,具體可通過網上查閱資料得知,在此暫時不做贅述。

基本使用

首先,我們先來了解Selenium的一些功能,以及它能做些什麽:

Selenium是一個自動化測試工具,利用它可以驅動遊覽器執行特定的動作,如點擊、下拉等操作,同時還可以獲取瀏覽器當前呈現的頁面的源代碼,做到可見即可爬。對於一些動態渲染的頁面來說,此種抓取方式非常有效。它的基本功能實現也十分的方便,下面我們來看一些簡單的代碼:

 1 from selenium import webdriver
 2 from selenium.webdriver.common.by import By
 3 from selenium.webdriver.common.keys import Keys
 4 from selenium.webdriver.support import expected_conditions as EC
 5 from selenium.webdriver.support.wait import WebDriverWait
 6 
 7 
 8 browser = webdriver.Chrome()  # 聲明瀏覽器對象
 9 try:
10     browser.get(https://www.baidu.com)  # 傳入鏈接URL請求網頁
11     query = browser.find_element_by_id(kw)   # 查找節點
12     query.send_keys(Python)  # 輸入文字
13     query.send_keys(Keys.ENTER)  # 回車跳轉頁面
14     wait = WebDriverWait(browser, 10)  # 設置最長加載等待時間
15     print(browser.current_url)  # 獲取當前URL
16     print(browser.get_cookies())  # 獲取當前Cookies
17     print(browser.page_source)  # 獲取源代碼
18 finally:
19     browser.close()  # 關閉瀏覽器

運行代碼後,會自動彈出一個Chrome瀏覽器。瀏覽器會跳轉到百度,然後在搜索框中輸入Python→回車→跳轉到搜索結果頁,在獲取結果後關閉瀏覽器。這相當於模擬了一個我們上百度搜索Python的全套動作,有木有覺得很神奇!!

技術分享圖片

在過程中,當網頁結果加載出來後,控制臺會分別輸出當前的URL、當前的Cookies和網頁源代碼:

技術分享圖片

可以看到,我們得到的內容都是瀏覽器中真實的內容,由此可以看出,用Selenium來驅動瀏覽器加載網頁可以直接拿到JavaScript渲染的結果。接下來,我們也將主要利用Selenium來進行基金凈值的爬取~

註:Selenium更多詳細用法和功能可以通過官網查閱(https://selenium-python.readthedocs.io/api.html)

基金凈值數據爬蟲

通過之前的爬蟲,我們會發現數據接口的分析相對來說較為繁瑣,需要分析相關的參數,而如果直接用Selenium來模擬瀏覽器的話,可以不再關註這些接口參數,只要能直接在瀏覽器頁面裏看到的內容,都可以爬取,下面我們就來試一試該如何完成我們的目標——基金凈值數據爬蟲。

頁面分析

本次爬蟲的目標是單個基金的凈值數據,抓取的URL為:http://fundf10.eastmoney.com/jjjz_519961.html(以單個基金519961為例),URL的構造規律很明顯,當我們在瀏覽器中輸入訪問鏈接後,呈現的就是最新的基金凈值數據的第一頁結果:

技術分享圖片

在數據的下方,有一個分頁導航,其中既包括前五頁的連接,也包括最後一頁和下一頁的連接,同時還有一個輸入任意頁碼跳轉的鏈接:

技術分享圖片

如果我們想獲取第二頁及以後的數據,則需要跳轉到對應頁數。因此,如果我們需要獲取所有的歷史凈值數據,只需要將所有頁碼遍歷即可,可以直接在頁面跳轉文本框中輸入要跳轉的頁碼,然後點擊“確定”按鈕即可跳轉到頁碼對應的頁面。

此處不直接點擊“下一頁”的原因是:一旦爬蟲過程中出現異常退出,比如到50頁退出了,此時點擊“下一頁”時,無法快速切換到對應的後續頁面。此外,在爬蟲過程中也需要記錄當前的爬蟲進度,能夠及時做異常檢測,檢測問題是出在第幾頁。整個過程相對較為復雜,用直接跳轉的方式來爬取網頁較為合理。

當我們成功加載出某一頁的凈值數據時,利用Selenium即可獲取頁面源代碼,定位到特定的節點後進行操作即可獲取目標的HTML內容,再對其進行相應的解析即可獲取我們的目標數據。下面,我們用代碼來實現整個抓取過程。

獲取基金凈值列表

首先,需要構造目標URL,這裏的URL構成規律十分明顯,為http://fundf10.eastmoney.com/jjjz_基金代碼.html,我們可以通過規律來構造自己想要爬取的基金對象。這裏,我們將以基金519961為例進行抓取。

 1 browser = webdriver.Chrome()
 2 wait = WebDriverWait(browser, 10)
 3 fundcode=519961
 4 
 5 def index_page(page):
 6     ‘‘‘
 7     抓取基金索引頁
 8     :param page: 頁碼
 9     :param fundcode: 基金代碼
10     ‘‘‘
11     print(正在爬取基金%s第%d頁 % (fundcode, page))
12     try:
13         url = http://fundf10.eastmoney.com/jjjz_%s.html % fundcode
14         browser.get(url)
15         if page>1:
16             input_page = wait.until(
17                 EC.presence_of_element_located((By.CSS_SELECTOR, #pagebar input.pnum)))
18             submit = wait.until(
19                 EC.element_to_be_clickable((By.CSS_SELECTOR, #pagebar input.pgo)))
20             input_page.clear()
21             input_page.send_keys(str(page))
22             submit.click()
23         wait.until(
24         EC.text_to_be_present_in_element((By.CSS_SELECTOR, #pagebar label.cur), 
25                                          str(page)))
26         wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, #jztable)))
27         get_jjjz()
28     except TimeoutException:
29         index_page(page)

這裏,首相構造一個WebDriver對象,即聲明瀏覽器對象,使用的瀏覽器為Chrome,然後指定一個基金代碼(519961),接著定義了index_page()方法,用於抓取基金凈值數據列表。

在該方法裏,我們首先訪問了搜索基金的鏈接,然後判斷了當前的頁碼,如果大於1,就進行跳頁操作,否則等頁面加載完成。

在等待加載時,我們使用了WebDriverWait對象,它可以指定等待條件,同時制定一個最長等待時間,這裏指定為最長10秒。如果在這個時間內匹配了等待條件,也就是說頁面元素成功加載出來,就立即返回相應結果並繼續向下執行,否則到了最大等待時間還沒有加載出來時,就直接拋出超市異常。

比如,我們最終需要等待歷史凈值信息加載出來就指定presence_of_element_located這個條件,然後傳入了CSS選擇器的對應條件#jztable,而這個選擇器對應的頁面內容就是每一頁基金凈值數據的信息快,可以到網頁裏面查看一下:

技術分享圖片

註:這裏講一個小技巧,如果同學們對CSS選擇器的語法不是很了解的話,可以直接在選定的節點點擊右鍵→拷貝→拷貝選擇器,可以直接獲取對應的選擇器:

技術分享圖片

關於CSS選擇器的語法可以參考CSS選擇器參考手冊(http://www.w3school.com.cn/cssref/css_selectors.asp)。

當加載成功後,機會秩序後續的get_jjjz()方法,提取歷史凈值信息。

關於翻頁操作,這裏首先獲取頁碼輸入框,賦值為input_page,然後獲取“確定”按鈕,賦值為submit:

技術分享圖片

首先,我們情況輸入框(無論輸入框是否有頁碼數據),此時調用clear()方法即可。隨後,調用send_keys()方法將頁碼填充到輸入框中,然後點擊“確定”按鈕即可,聽起來似乎和我們常規操作的方法一樣。

那麽,怎樣知道有沒有跳轉到對應的頁碼呢?我們可以註意到,跳轉到當前頁的時候,頁碼都會高亮顯示:

技術分享圖片

我們只需要判斷當前高亮的頁碼數是當前的頁碼數即可,左移這裏使用了另一個等待條件text_to_be_present_in_element,它會等待指定的文本出現在某一個節點裏時即返回成功,這裏我們將高亮的頁碼對應的CSS選擇器和當前要跳轉到 頁碼通過參數傳遞給這個等待條件,這樣它就會檢測當前高亮的頁碼節點是不是我們傳過來的頁碼數,如果是,就證明頁面成功跳轉到了這一頁,頁面跳轉成功。

這樣,剛從實現的index_page()方法就可以傳入對應的頁碼,待加載出對應頁碼的商品列表後,再去調用get_jjjz()方法進行頁面解析。

解析歷史凈值數據列表

接下來,我們就可以實現get_jjjz()方法來解析歷史凈值數據列表了。這裏,我們通過查找所有歷史凈值數據節點來獲取對應的HTML內容

技術分享圖片

並進行對應解析,實現如下:

 1 def get_jjjz():
 2     ‘‘‘
 3     提取基金凈值數據
 4     ‘‘‘
 5     lsjz = pd.DataFrame()
 6     html_list = browser.find_elements_by_css_selector(#jztable tbody tr)
 7     for html in html_list:
 8         data = html.text.split( )
 9         datas = {
10             凈值日期: data[0],
11             單位凈值: data[1],
12             累計凈值: data[2],
13             日增長率: data[3],
14             申購狀態: data[4],
15             贖回狀態: data[5],
16         }
17         lsjz = lsjz.append(datas, ignore_index=True)
18     save_to_csv(lsjz)

首先,調用find_elements_by_css_selector來獲取所有存儲歷史凈值數據的節點,此時使用的CSS選擇器是#jztable tbody tr,它會匹配所有基金凈值節點,輸出的是一個封裝為list的HTML。利用for循環對list進行遍歷,用text方法提取每個html裏面的文本內容,獲得的輸出是用空格隔開的字符串數據,為了方便後續處理,我們可以用split方法將數據切割,以一個新的list形式存儲,再將其轉化為dict形式。

最後,為了方便處理,我們將遍歷的數據存儲為一個DataFrame再用save_to_csv()方法進行存儲為csv文件。

保存為本地csv文件

接下來,我們將獲取的基金歷史凈值數據保存為本地的csv文件中,實現代碼如下:

 1 def save_to_csv(lsjz):
 2     ‘‘‘
 3     保存為csv文件
 4     : param result: 歷史凈值
 5     ‘‘‘
 6     file_path = lsjz_%s.csv % fundcode
 7     try:
 8         if not os.path.isfile(file_path):  # 判斷當前目錄下是否已存在該csv文件,若不存在,則直接存儲
 9             lsjz.to_csv(file_path, index=False)
10         else:  # 若已存在,則追加存儲,並設置header參數為False,防止列名重復存儲
11             lsjz.to_csv(file_path, mode=a, index=False, header=False)
12         print(存儲成功)
13     except Exception as e:
14         print(存儲失敗)

此處,result變量就是get_jjjz()方法裏傳來的歷史凈值數據。

遍歷每一頁

我們之前定義的get_index()方法需要接受參數page,page代表頁碼。這裏,由於不同基金的數據頁數並不相同,而為了遍歷所有頁我們需要獲取最大頁數,當然,我們也可以用一些巧辦法來解決這個問題,頁碼遍歷代碼如下:

 1 def main():
 2     ‘‘‘
 3     遍歷每一頁
 4     ‘‘‘
 5     flag = True
 6     page = 1
 7     while flag:
 8         try:
 9             index_page(page)
10             time.sleep(random.randint(1, 5))
11             page += 1
12         except:
13             flag = False
14             print(似乎是最後一頁了呢)

其實現方法結合了try...except和while方法,逐個遍歷下一頁內容,當頁碼超過,即不存在時,index_page()的運行就會出現報錯,此時可以將flag變為False,則下一次while循環不會繼續,這樣,我們便可遍歷所有的頁碼了。

由此,我們的基金凈值數據爬蟲已經基本完成,最後直接調用main()方法即可運行。

技術分享圖片

總結

在本文中,我們用Selenium演示了基金凈值頁面的抓取,有興趣的同學可以嘗試利用其它的條件來爬取基金數據,如設置數據的起始和結束日期:

技術分享圖片

利用日期來爬取內容可以方便日後的數據更新,此外,如果覺得瀏覽器的彈出較為惱人,可以嘗試Chrome Headless模式或者利用PhantomJS來抓取。

至此,基金凈值爬蟲的分析正式完結,撒花~

技術分享圖片

Python爬蟲周記之案例篇——基金凈值Selenium動態爬蟲