爬蟲入門到放棄系列06:爬蟲實戰基金

前言

爬蟲的基本知識已經告一段落,這次就找個網站實戰一波。但是為什麼選擇了基金?這還要從我的故事講起。

我是一名韭零後,小白一枚,隨大流入基市一載,佛系持有,盈虧持平。看到年前白酒紅勝火,遂小投一筆,未曾想開市之後綠如藍,賺的本韭菜空喜歡,一周夢回解放前。

還記得那天的天台的風很涼,低頭往下看車來車往,有點恐高。想點一支煙烘托一下氣氛,才想起我不會抽煙。悲傷之際,突然想起一位名人曾說過:”只要你不跑,你就不是韭菜”。於是轉身回家,坐在電腦前寫下了這篇文章。

準備

  1. 明確爬取目標
    爬取各個板塊基金數據
  2. 尋找數據網站:天天基金網(fund.eastmoney.com)

天天基金網

  1. 確定網站入口:在首頁上點擊 投資工具 -> 主題基金 進入主題頁面,選擇 主題索引,如下圖:

主題分類

  1. 確定爬取內容點擊主題下的主題索引下的 白酒 進入白酒列表。

點擊招商中證白酒,進入詳情頁面。

根據自己的需求,從頁面上的內容確定要爬取的字段。這裡要爬取的字段除了圖中紅框部分,還有基金名稱、基金編碼、所屬主題字段。

  1. 明確頁面跳轉關係:主題頁面 -> 列表 -> 詳情頁,一共三層

網站分析

第一層:請求網站入口

F12或者右鍵選擇檢查,使用開發者工具找到基金分類的html元素。

右鍵html元素,複製xpath,當然你可以自己寫。

開發代碼獲取分類列表:
第一部分代碼
如圖,按理說使用我自己寫的xpath和拷貝的xpath,都可以獲取到分類的html元素,但結果結果卻為空。帶着疑問,去查看返回的網頁內容。

請求內容

如圖,爬蟲請求返回的網頁和從瀏覽器上看到的網頁元素不一樣,行業分類內容沒了!!剛接觸爬蟲的可能還在疑問為什麼,開發過爬蟲的已經開始搶答了:

嗯,什麼是動態加載? 這裡我就用我自己的理解說一下。

動態加載

我們用瀏覽器訪問一個網頁的時候,後台返回給瀏覽器html網頁、js、css等文件。瀏覽器內核(也稱渲染引擎)在加載網頁的同時,也會執行html中的js渲染網頁,然後將渲染後的網頁展示在瀏覽器上,即瀏覽器上的網頁內容是:原始HTML + 瀏覽器js渲染的結果。

js將數據渲染到網頁的過程方式就是動態加載。那麼,數據從哪來?

你輸入url請求網站時,其實js中定義的方法也偷偷地幫你發起了請求。最常見的是網頁上有一數據展示的部分,當我們點擊下一頁時,頁面沒有進行跳轉,只有展示數據部分刷新,這個就是ajax實現的局部刷新功能,也是最常見的動態加載之一。講講大致原理。

前端開發者在js中對下一頁按鈕添加了點擊監聽事件。點擊按鈕時,進入相應js函數,在函數中使用ajax對後台url進行請求,返回json或者其他格式的數據,然後選中數據展示區的html元素,清除其中已有的數據,插入新獲取的數據,就實現了數據刷新而不需要網頁跳轉的功能,也稱為異步請求、局部刷新。當然很多網站在網頁加載時,就使用ajax來獲取數據進行渲染。

但是爬蟲程序他沒有渲染引擎啊,無法執行js,所以只能獃獃地獲取後台返回的原始html。我們在瀏覽器中看到的網頁源碼,才是沒有經過js渲染的網頁,也是我們爬蟲最終獲取的網頁內容。

原始網頁

如圖,網頁源碼中也沒有分類元素。至此,我們可以得出結論:開發者工具看到的是js渲染後的html,網頁源碼是原始的html

這時候你應該有所考慮:我們解析網頁是為了什麼?獲取數據!但網頁中沒有數據,所以我們就不需要請求這個網頁的url了。我們只要找到js獲取數據的url,直接請求這個url,數據不直接就有了么

正常情況下,如何應對動態加載

找接口的url

在我看來,使用動態加載網頁獲取數據比普通網頁簡單的多,使用加密參數的除外。我們可以直接從接口獲取json或者其他文本格式的數據,而不需要解析網頁。我們的爬蟲開發也直接從面向網頁變成了面向數據。我們首先要做的就是找接口的url。

如何找到接口url?

  1. 打開開發者工具,刷新頁面,搜索關鍵字

根據返回數據中的關鍵字搜索,如圖,我們根據”白酒”找到了對應的響應內容。這裡先看看返回的內容,這裡記住BKCodeBkname兩個字段。

  1. 查看url,構造參數

我們來查看此響應的請求。如圖,我們找到了url,並且有兩個請求參數。

根據請求和響應來看,這個是一個JSONP的請求。這類請求的規律是:url中的callback由一個方法名+時間戳組成,_參數也是一個時間戳;響應內容格式為callback(json)。如果用興趣可以去了解一下JSONP,如果單純獲取數據只要了解他的規律即可。

第二層:解析列表頁

  1. 我們點擊進入”電子信息”的基金列表頁,如圖

  2. 按照分類頁面請求的方法,你會發現這個也是一個jsonp接口返回的數據,同樣,來尋找接口url。

這裡主要關注FCODE字段。從列表頁發現,一頁是十個基金,需要翻頁,所以在響應數據中末尾有TotalCount字段,用這個可以來計算一共有多少頁。

  1. 查看請求參數

這裡的tp字段就是BKCode,pageIndex傳入當前請求的頁數。

第三層:解析詳情頁

進入一個基金詳情頁,你會發現這個頁面就是傳統的靜態頁面,使用css或者xpath直接解析即可。通過url你會發現,從列表頁是通過Fcode字段來跳轉到每個基金的詳情頁。

程序開發

從上面的分析來看,分類頁和列表頁是動態加載,返回內容是類似於json的jsonp文本,我們可以去掉多餘的部分,直接用json解析。詳情頁是靜態頁面,用xpath即可。

代碼開發

import requests
import time
import datetime
import json
import pymysql
from lxml.html import etree

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15'
    , 'Referer': '//fund.eastmoney.com'
}

# 初始化數據庫連接
connection = pymysql.connect(host='47.102.219.86', user='root', password='1qaz@WSX', database='scrapy', port=3306, charset='utf8')
cursor = connection.cursor()

# 程序入口, 解析基金分類
def start_requests():
    timestamp = int(time.time() * 1000)
    callback = 'jQuery18306789193760800711_' + str(timestamp)
    start_url = f'//fundtest.eastmoney.com/dataapi1015/ztjj//GetBKListByBKType?callback={callback}&_={timestamp}'
    response = requests.get(start_url, headers=headers)
    # 將分類返回的數據掐頭去尾,格式化成json
    result = response.text.replace(callback, '')
    result = result[1: result.rfind(')')]
    data = json.loads(result)
    # 遍歷行業分類數據,獲取名稱和代號
    for item in data['Data']['hy'] :
        time.sleep(3)
        code = item['BKCode']
        category = item['BKName']
        print(code, category)
        parseFundList(code, category)
    # 遍歷概念分類數據
    for item in data['Data']['gn']:
        time.sleep(3)
        code = item['BKCode']
        category = item['BKName']
        print(code, category)
        parseFundList(code, category)

# 解析每個分類下的基金列表
def parseFundList(code, category):
    timestamp = int(time.time() * 1000)
    callback = 'jQuery1830316287740290561061_' + str(timestamp)
    index = 1
    url = f'//fundtest.eastmoney.com/dataapi1015/ztjj/GetBKRelTopicFund?callback={callback}&sort=SON_1N&sorttype=DESC&pageindex={index}&pagesize=10&tp={code}&isbuy=1&_={timestamp}'
    response = requests.get(url, headers=headers)
    result = response.text.replace(callback, '')
    result = result[1: result.rfind(')')]
    data = json.loads(result)
    totalCount = data['TotalCount']
    # 先根據totalCount計算出總頁數
    pages = int(int(totalCount) / 10) + 1
    # 解析出每頁基金的FCode
    for index in range(1, pages + 1):
        timestamp = int(time.time() * 1000)
        callback = 'jQuery1830316287740290561061_' + str(timestamp)
        url = f'//fundtest.eastmoney.com/dataapi1015/ztjj/GetBKRelTopicFund?callback={callback}&sort=SON_1N&sorttype=DESC&pageindex={index}&pagesize=10&tp={code}&isbuy=1&_={timestamp}'
        response = requests.get(url, headers=headers)
        result = response.text.replace(callback, '')
        result = result[1: result.rfind(')')]
        data = json.loads(result)
        for item in data['Data']:
            time.sleep(3)
            fundCode = item['FCODE']
            fundName = item['SHORTNAME']
            parse_info(fundCode, fundName, category)


def parse_info(fundCode, fundName, category):
    url = f'//fund.eastmoney.com/{fundCode}.html'
    response = requests.get(url, headers=headers)
    content = response.text.encode('ISO-8859-1').decode('UTF-8')
    html = etree.HTML(content)
    worth = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[2]/dd[1]/span[1]/text()')
    if worth:
        worth = worth[0]
    else:
        worth = 0
    scope = html.xpath('//div[@class="infoOfFund"]/table/tr[1]/td[2]/text()')[0].replace(':', '')
    manager = html.xpath('//div[@class="infoOfFund"]/table/tr[1]/td[3]/a/text()')[0]
    create_time = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[1]/text()')[0].replace(':', '')
    company = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[2]/a/text()')[0]
    level = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[3]/div/text()')
    if level:
        level = level[0]
    else:
        level = '暫無評級'
    month_1 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[1]/dd[2]/span[2]/text()')
    month_3 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[2]/dd[2]/span[2]/text()')
    month_6 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[3]/dd[2]/span[2]/text()')
    if month_1:
        month_1 = month_1[0]
    else:
        month_1 = ''

    if month_3:
        month_3 = month_3[0]
    else:
        month_3 = ''

    if month_6:
        month_6 = month_6[0]
    else:
        month_6 = ''
    print(fundName, fundCode, category, worth, scope, manager, create_time, company, level, month_1, month_3, month_6, sep='|')
    # 存儲到mysql
    today = datetime.date.today()
    sql = f"insert into fund_info values('{today}', '{fundName}', '{fundCode}', '{category}', '{worth}', '{scope}', '{manager}', '{create_time}', '{company}', '{level}', '{month_1}', '{month_3}', '{month_6}')"
    cursor.execute(sql)
    connection.commit()
# 開始爬取
start_requests()

聲明: 以上代碼僅限於學習使用,不得使用該程序對網站惡意請求造成破壞,否則後果自負。

程序如上,在解析動態加載的數據的時候明顯比解析網頁顯簡單,因為數據字段規範,根本不用考慮字段缺失的問題,而解析網頁就會有各種各樣的情況出現。

其次,程序還有很多可以優化的部分。例如

  1. 可以將冗餘代碼重構成一個方法,這裡為了直觀都是逐行寫的。
  2. 可以針對詳情頁不同結構多設置幾種解析方式。
  3. 對詳情頁每個字段進行if為空的判斷,然後設置缺省值,我這裡只判斷了三四個字段。

數據庫建表

CREATE TABLE `fund_info` (
  `op_time` varchar(20) DEFAULT NULL,
  `fundName` varchar(20) DEFAULT NULL,
  `fundCode` varchar(20) DEFAULT NULL,
  `category` varchar(20) DEFAULT NULL,
  `worth` varchar(20) DEFAULT NULL,
  `scope` varchar(20) DEFAULT NULL,
  `manager` varchar(20) DEFAULT NULL,
  `create_time` varchar(20) DEFAULT NULL,
  `company` varchar(20) DEFAULT NULL,
  `level` varchar(20) DEFAULT NULL,
  `month_1` varchar(20) DEFAULT NULL,
  `month_3` varchar(20) DEFAULT NULL,
  `month_6` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8

運行結果

控制台輸出:

數據庫查詢:

結語

3月6日確定題目開始着手寫,寫完已經是3月14日。也深刻體會到開發容易描述不易。本篇文章從分析網站、到開發爬蟲、存儲數據,以及穿插了部分動態加載的知識,全方面的講述了一個爬蟲開發的全過程,希望對你有所啟示。期待下一次相遇。


寫的都是日常工作中的親身實踐,置身自己的角度從0寫到1,保證能夠真正讓大家看懂。

文章會在公眾號 [入門到放棄之路] 首發,期待你的關注。

感謝每一份關注