在成功完成基金凈值爬蟲的爬蟲后,簡單了解爬蟲的一些原理以后,心中不免產生一點困惑——為什么我們不能直接通過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來抓取。
至此,基金凈值爬蟲的分析正式完結,撒花~