原來那些賺錢的投資者都在用這個辦法! 1. 獲取數據 2. 分析數據 One more thing:優化

近半年多,基金又變得異常火爆,很多小朋友開始投資基金,但是基金的選擇是個頭疼的問題,網上衆多up主,各自心懷鬼胎、衆說紛紜。之前火爆的“坤坤”,一夜之間也跌回解放前。所以,想賺錢,還得靠自己,任何人都不會對你的選擇負責!

最近很流行的一句話:“你只能賺到自己認知範圍內的錢。”

那麼,自己如何選擇?

孫子曾經曰過:**知己知彼…… **

先看看某寶的“金選好基”:


再看看已經跌下神壇的坤坤的基金在某寶上是個什麼狀態:


金選!

金牌經理!

驚不驚喜?意不意外?

所以,平臺推薦的你敢買嗎?

毛爺爺說的好:自己動手,豐衣足食。 基金好不好,我們自己來判斷。

自己來分析基金的好壞,其實步驟也很簡單,類似把大象放進冰箱裏:

  1. 打開冰箱——獲取數據
  2. 把大象塞進冰箱——分析數據
  3. 關上冰箱門——買入

1. 獲取數據

按照國際慣例,我們以東方財富的天天基金網作爲數據源:

網上其實很多教我們如何用python爬天天基金的數據的文章,很顯然這些文章幫助了很多人,但也給天天基金網造成了不少的困擾,所以他們經常修改一些參數名或者id名。

好老師應該授人予漁,而非授人予魚,文章會有點長,我把小白可能遇到的坑以及解題思路都會盡量詳細的分享出來,目的就是讓小白真正掌握獲取數據的能力,不管別人的網站再如何變化,我們都能找到應對的辦法。

數據獲取方式

通常來說,公開網站的數據獲取方式有兩種:

  1. 爬蟲——最簡單粗暴
  2. Web API——最優雅

1.1 爬蟲

爬蟲方式是網上文章最多的,雖然看上去簡單,但很多經常被爬的網站都會設計一些反爬機制,小白在實際操作時卻會遇到無數的坑。坑有很多種:

  1. 反DDOS
  2. 異步加載數據
  3. 用戶真實性驗證
    我們先把坑的事情放一邊,回到爬蟲本身,看看如何展開接下來的工作。

想爬數據,第一步就是分析頁面的代碼,打開瀏覽器的開發者模式,選擇頁面中數據的部分,然後分析其HTML代碼的特徵:


以上圖爲例,所有的數據都在 <table id="dbtable">...</table> 標籤中。id在一個html頁面中是唯一的,因此通過代碼只要提取出id=“dbtable”的對象,理論上就可以得到table中的所有數據了。

但是,衆所周知,事情總不會一帆風順,不過,我先上一段代碼吧:

import requests
url = 'http://fund.eastmoney.com/data/fundranking.html#tgp;c0;r;s1nzf;pn50;ddesc;qsd20200715;qed20210715;qdii;zq;gg;gzbd;gzfs;bbzt;sfbb'
# 獲取url的結果
response = requests.get(url)
# 看看我們都拿到了什麼東西
print(response.content)

以下是結果的局部:

b'\xef\xbb\xbf\r\n\r\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r\n<html xmlns="http://www.w3.org/1999/xhtml">\r\n<head>\r\n    <title>\xe5\xbc\x80\xe6\x94\xbe\xe5\xbc\x8f\xe5\x9f\xba\xe9\x87\x91\xe6\x8e\x92\xe8\xa1\x8c _ \xe5\xa4\xa9\xe5\xa4\xa9\xe5\x9f\xba\xe9\x87\x91\xe7\xbd\x91</title>\r\n

是不是看不懂?不要內疚,我也看不懂,因爲這東西本來就有問題, \xe5\xbc\x80\xe6\x94\xbe\xe5\xbc\x8f\xe5\x9f\這些有規律但不知所云的東西其實是中文,只不過request庫默認的編碼方式是ISO-8859-1,在ISO-8859-1的字符集是不包涵中文的,所以就把原始信息直接丟了出來。這是小白們遇到的第一個小坑——中文字符編碼問題。如何解決也非常的簡單,只要做一個decode操作,把字符集設置成utf-8,那麼一切都回來了。不信你就試試看:

print(response.content.decode('utf8'))

# 以下是部分結果:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>開放式基金排行 _ 天天基金網</title>
    <meta name="keywords" content="基金排行,開放式基金排行,創新基金排行,貨幣基金排行,漲幅排行,基金排行查詢,漲幅分佈,自定義基金排行,股票型基金,混合型基金,債券型基金,指數型基金,保本型基金,QDII,LOF,按基金公司篩選" />
    <meta name="description" content="天天基金網每日及時更新開放式基金收益率排行。" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="mobile-agent" content="format=html5; url=https://m.1234567.com.cn/?page=jjph&tab=qb" />
    <meta http-equiv="Content-Language" content="zh-CN" />
    <meta http-equiv="Cache-Control" content="no-cache" />
    <meta http-equiv="Expires" content="-1" />

很明顯,中文正常顯示了,連換行也正常了。

接下來的問題就是想辦法從這堆html代碼中把<table id="dbtable">...</table> 的內容過濾出來。還記得前面截圖中的XPath嗎?

什麼是XPath?不知道的看這裏

按照最簡單的做法,看看我們能得到什麼,先看代碼:

from lxml import etree
tree = etree.HTML(response.content.decode('utf8'))
dbtab = tree.xpath('//*[@id=\"dbtable\"]')
print(dbtab[0].text)

但是結果什麼都沒有。也許這是我們遇到的第二個坑!回去翻翻print(response.content.decode('utf8')),找到<table id="dbtable">發現:

<table id="dbtable">
                <thead>
                    <tr>
                        <th>比較</th>
                        <th>序號</th>
                        <th col="dm" class="tworow"><a><span class="ades">基金<br />
                            代碼</span><span class="showway"></span></a></th>
                        <th col="jc"><a>基金簡稱</a></th>
                        <th col="jzrq"><a>日期</a></th>
                        <th col="dwjz"><a>單位淨值</a></th>
                        <th col="ljjz"><a>累計淨值</a></th>
                        <th col="rzdf"><a>日增長率</a></th>
                        <th col="zzf"><a>近1周</a></th>
                        <th col="1yzf"><a>近1月</a></th>
                        <th col="3yzf"><a>近3月</a></th>
                        <th col="6yzf"><a>近6月</a></th>
                        <th col="1nzf"><a>近1年</a></th>
                        <th col="2nzf"><a>近2年</a></th>
                        <th col="3nzf"><a>近3年</a></th>
                        <th col="jnzf"><a>今年來</a></th>
                        <th col="lnzf" style="white-space: nowrap;"><a>成立來</a></th>
                        <th col="qjzf" id="sortclass" class="datespan">
                            <div style="position: relative" col="qjzf">
                                <a onmouseover="show_tip();">自定義</a>
                                <b class="cal" id="calen"></b>
                            </div>
                        </th>
                        <th class="yh_head">手續費</th>
                        <th>
                            <label style="text-align: center; vertical-align: middle">
                                <input id="onlysale" style="margin-top: 0px" type="radio" value="1" name="showsale" class="md" checked="checked" /><span class="md">可購</span></label><br />
                            <label style="text-align: center; vertical-align: middle;">
                                <input style="margin-top: 0px" type="radio" value="0" name="showsale" class="md" id="allfund" checked="" /><span class="md">全部</span>
                            </label>
                        </th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>

只有表頭,tbody竟然是空的,果然,我們遇到了第二個坑,數據異步加載。

雖然這是個坑,但有好也有壞,通常我們有兩個解題思路:

  1. 順着爬蟲思路往下走,我們需要一個瀏覽器的模擬器,拿到瀏覽器渲染完成的數據。
  2. 既然是異步,那麼一定有web API,這樣我們就不需要用爬蟲爬去數據了,省去了後面繁瑣的數據整理工作。

1.1.1 瀏覽器模擬器

本文不作爲重點,這裏只簡單介紹一下。python生態的瀏覽器模擬器最有名的就是selinum,它可以調用本地的瀏覽器驅動,完整的模擬瀏覽器的操作,包括點擊事件等,非常強大,在一些需要通過交互過程才能獲取到數據的場景下非常有用。

1.2 Web API

既然我們確定了有Web API,那麼,應該去哪找它呢?
首先,還是去看瀏覽器的開發者工具:


打開Network選項卡。如果你的選項卡中有一大堆東西,可以點擊左上角第二排的第二個按鈕清理一下。接下來按照下圖的示意,隨便點擊表格上方的分類按鈕,如圖中所示,我點擊的是混合型按鈕:

在下方開發者工具的列表中多了兩個請求,點開最長的那個,看到這個請求的Response中剛好是表格中的內容!看來,這應該就是我們要找的東西了。複製這個請求地址:http://fund.eastmoney.com/data/rankhandler.aspx?op=ph&dt=kf&ft=hh&rs=&gs=0&sc=1nzf&st=desc&sd=2020-07-15&ed=2021-07-15&qdii=&tabSubtype=,,,,,&pi=1&pn=50&dx=1&v=0.726345617727655 我們單獨打開看看:

var rankData ={ErrCode:-999,Data:"無訪問權限"}

爲什麼會這樣?我們也不是註冊用戶,打開網頁就能正常看到數據了,哪來的訪問權限?

其實這個就是網站設計的典型反爬機制,也就是前面說的第三個坑,驗證用戶的真實性。我簡單解釋一下背後的邏輯:

一個正常人類去訪問這個頁面,這個url地址的真正訪問者應該是http://fund.eastmoney.com/data/fundranking.html 這個頁面中的JavaScript代碼,所以它可以通過驗證流量來源的身份來確認是否是一個正常訪問請求。

這個可以僞裝嗎?答案是肯定的。爲了保持用戶和服務器端的交互連續性,用戶和服務器之前存在一個叫做會話的東西,在http協議中叫做request(請求)。request中可以存儲許多與用戶相關的信息,有一些是協議默認的,我們還可以在程序裏面自定義添加一些特殊信息,實現從瀏覽器向服務器傳遞數據。

注意:通過URL傳遞參數的方式是明文的,請勿傳遞敏感信息。這種傳輸方式也叫GET請求。敏感信息可以採用POST非明文方式,具體方法本文不展開,感興趣的同學請自行搜索。

說到Request,我們不妨去瀏覽器的開發者工具中看看剛纔的url請求中都有些什麼東西:



在請求的Headers裏面,我們可以找到Request Headers,我們注意看裏面的Host和Referer這兩個字段,它倆其實就是前面說的web API去驗證請求來源的依據,我們只要把和兩個東西塞到我們的Request Header中,應該就能拿到數據了。來看看代碼:

headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
        'Host': 'fund.eastmoney.com',
        'Accept-Encoding': 'gzip, deflate',
        'Referer': 'http://fund.eastmoney.com/data/fundranking.html'
    }
response = requests.get(url, headers=headers)

我們增加了一個叫做headers的字典,把剛纔瀏覽器的Request Headers中的關鍵內容塞進去就行,User-Agent和Cookie也可以塞進去,一般這種API還會去校驗這兩個東西。
僞裝的Header做好了以後,我們在requests.get方法中傳入剛纔做好的headers,這樣就能拿到真正的數據了:

var rankData = {datas:["000209,信誠新興產業混合,XCXXCYHH,2021-07-16,4.6530,4.6530,-1.81,1.35,35.38,68.71,53.97,153.16,301.12,277.07,60.84,365.30,2013-07-17,1,142.1564,1.50%,0.15%,1,0.15%,1,157.36",
"002190,農銀新能源主題,NYXNYZT,2021-07-16,3.8259,3.8259,-3.01,0.54,25.48,53.12,36.05,140.99,352.02,288.26,37.18,282.59,2016-03-29,1,138.3444,1.50%,0.15%,1,0.15%,1,268.34",
"400015,東方新能源汽車混合,DFXNYQCHH,2021-07-16,4.1490,4.6090,-3.93,-0.55,33.22,70.17,42.06,135.85,278.83,221.85,47.79,453.21,2011-12-28,1,136.1310,1.50%,0.15%,1,0.15%,1,164.20",
"001951,金鷹改革紅利混合,JYGGHLHH,2021-07-16,3.3520,3.3520,-3.12,0.06,23.37,48.85,44.05,125.57,224.18,243.79,52.36,235.20,2015-12-02,1,119.8221,1.50%,0.15%,1,0.15%,1,209.80",
……
"700003,平安策略先鋒混合,PACLXFHH,2021-07-16,5.3350,5.4350,0.40,2.18,33.98,61.28,37.01,82.77,208.03,233.65,42.91,483.74,2012-05-29,1,71.7518,1.50%,0.15%,1,0.15%,1,180.64"],
allRecords:4476,
pageIndex:1,
pageNum:50,
allPages:90,
allNum:8252,
gpNum:1610,
hhNum:4476,
zqNum:1983,
zsNum:1164,
bbNum:0,
qdiiNum:183,
etfNum:0,
lofNum:330,
fofNum:169
};

看起來這是我們要的數據了,但這個數據還不能被python直接識別到,因爲前面有一段 *var rankData = * 的鬼東西。這個東西是js代碼,對他們網站的前端游泳,但是對我們毫無意義。仔細分析一下拿到的數據發現,我們需要的其實是datas後面的 [ …… ] 裏面的東西。數據的最後面還有一段allRecords:4476,pageIndex:1,pageNum:50,allPages:90,allNum:8252,gpNum:1610,hhNum:4476,zqNum:1983,zsNum:1164,bbNum:0,qdiiNum:183,etfNum:0,lofNum:330,fofNum:169。這些是一些參數值,後面我們都用得着,所以我們乾脆把 {……}都弄出來,這個結構和python中的dict結構是一樣的。所以,我們只需要把前面的 “var rankData =”去掉就好了,來看代碼:

# 截取第14個字符到倒數第一個字符之間的所有內容,注意,最後還有一個;號也要去掉
js_data = response.text[14:-1]

# 把得到的{···}轉成dict對象。這裏我們用到了第三方庫execjs,沒有的同學自己pip install一下
dt = execjs.eval(js_data)

到這裏,我們纔算真正白嫖到了網站的數據。那麼有了數據,接下來就是分析了。Python以爬蟲和數據分析著稱,而說到數據分析,必須提到的就是無所不能的pandas庫。那麼接下來就來教大家如何用pandas進行數據分析。


2. 分析數據

前文我們已經拿到了網站數據並且放到了一個叫做dt的字典中。但是真正我們需要的數據在dt的一個叫做datas的字段中,這個數據是一個數組,我們只需要把它取出來丟給pandas就可以了,代碼也非常的簡單:

import pandas as pd
em_data = pd.DataFrame(dt['datas'])

來看看我們得到的DataFrame是什麼樣的


很奇怪,數據的行數沒問題,但是隻有一列,這是什麼原因?先回去看看拿到的數據:

"000209,信誠新興產業混合,XCXXCYHH,2021-07-16,4.6530,4.6530,-1.81,1.35,35.38,68.71,53.97,153.16,301.12,277.07,60.84,365.30,2013-07-17,1,142.1564,1.50%,0.15%,1,0.15%,1,157.36",
"002190,農銀新能源主題,NYXNYZT,2021-07-16,3.8259,3.8259,-3.01,0.54,25.48,53.12,36.05,140.99,352.02,288.26,37.18,282.59,2016-03-29,1,138.3444,1.50%,0.15%,1,0.15%,1,268.34"

爲了方便,我截取了前面兩條數據,並且做了換行處理。我們看到,每條數據都被一對“”引號包起來了,這是一個二維數組結構,但是pandas無法直接識別出來,不過也不麻煩,我們只需要做一個小小的處理就可以——對這個字段做分隔符的拆分即可:

# DataFrame的str可以自動對每行的數據進行處理
em_data = pd.DataFrame(dt['datas'])[0].str.split(',', expand = True)

# 字段拆分完畢後,爲了便於分析,我們需要給它加上字段名,以下是我不辭辛苦一個個對出來的,拿走不謝~  
cols=['代碼', '名稱', '簡稱', '日期','單位淨值','累計淨值','日增長率','近1周','近1月','近3月','近6月','近1年','近2年','近3年','今年來','成立來', '成立日期','自定義', 'A', 'B', '手續費', 'C', 'D', 'E', 'F']
em_data.columns = cols

這樣我們就得到了一個完美的DataFrame:

至於後面如何進行分析,就完全是個人主觀的事情了,這個必須自己去學習金融知識和統計學知識,一點點積累,技術部分自己去看pandas的文檔即可,你能知道的計算方法pandas都有現成的函數可以直接使用,非常的強大。

不過,在拿到一個數據的初期,我喜歡先對數據進行一個整體的瞭解,做一個overview,這裏強烈安利一個基於pandas的庫,可以幫助我們進行數據探索——pandas_profiling,使用也非常非常簡單:

import pandas_profiling
pandas_profiling.ProfileReport(df)

如果你使用Jupyter Notebook編寫代碼,那麼經過短暫的等待,你就會看到一個非常驚人的交互式報告:




One more thing:優化

到這裏,基本技能已經介紹完了,但細心的同學會發現,裏面隱藏了一個小問題,就是數據量的問題。我們從開發者工具中拿到的那個url只返回了50條數據,但是一共有幾千個基金,這麼多數據我們應該怎麼取?如果每次只能取50條,5000條數據我們就要取100次,不光速度慢,而且很有可能被對方的安全設備識別爲DDOS攻擊。那麼,有沒有更佳優雅的解決方案呢?

還記得我們前面提過的Request參數傳遞的事情嗎?我們來分析一下這個接口的URL地址,看看能有什麼發現:

# 我把它簡單處理一下,在&符號後面加上換行,得到了這個東西,:
http://fund.eastmoney.com/data/rankhandler.aspx?
op=ph&
dt=kf&
ft=hh&
rs=&
gs=0&
sc=1nzf&
st=desc&
sd=2020-07-15&
ed=2021-07-15&
qdii=&
tabSubtype=,,,,,&
pi=1&
pn=50&
dx=1&
v=0.726345617727655

簡單解釋一下,url中的“?”號後面是請求的參數,多個參數用&符號進行連接。
根據參數值我們可以簡單得到以下猜測:

  • sd:start date 開始日期
  • ed:end date 結束日期
  • pi:page index 所在分頁的頁數
  • pn:page number 每頁的數量
    剩下的參數感興趣的同學可以自己去研究,pi和pn應該就是我們可以利用的地方。如果pn沒有上限限制,那麼我們就可以一次性拿到所有的數據了,我測試了4476,也就是所有的混合基金的數量,順利拿到了所有的數據。
    既然,我們摸清楚了這個接口的一些參數,是不是可以把它封裝成一個python的function呢?說幹就幹,以下是完整的代碼:
import pandas as pd
import requests
import execjs

def get_eastMoney(start_date='2020-07-15', end_date='2020-07-15', page_index=1, page_number=50, fund_type='all', sc='6yzf'):
    """

    :param start_date:
    :param end_date:
    :param page_index:
    :param page_number:
    :param fund_type: 基金類型,hh爲混合,all爲全部
    :param sc: 排序規則,6yzf爲近六個月,1nzf爲近1年
    :return:
    """
    url = 'http://fund.eastmoney.com/data/rankhandler.aspx?op=ph&dt=kf&rs=&gs=0&st=desc&qdii=&tabSubtype=,,,,,&dx=1&v=0.5137439179039982'
    cols=['代碼', '名稱', '簡稱', '日期','單位淨值','累計淨值','日增長率','近1周','近1月','近3月','近6月','近1年','近2年','近3年','今年來','成立來', '成立日期','自定義', 'A', 'B', '手續費', 'C', 'D', 'E', 'F']
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
        'Host': 'fund.eastmoney.com',
        'Accept-Encoding': 'gzip, deflate',
        'Referer': 'http://fund.eastmoney.com/data/fundranking.html'
    }
    
    params = {
        'ft': fund_type,
        'sd': start_date,
        'ed': end_date,
        'pi': page_index,
        'pn': page_number,
        'sc': sc
    }
    
    response = requests.get(url, headers=headers, params=params)
    
    js_data = response.text[14:-1]
    dt = execjs.eval(js_data)
    print('allRecords: ', dt['allRecords'])
    em_data = pd.DataFrame(dt['datas'])[0].str.split(',', expand = True)
    em_data.columns = cols
    return em_data

df = get_eastMoney(fund_type='hh', page_number=500)

經過一個簡單的封裝,我們就有了自己的python的函數,以後我們只要通過一行代碼就可以得到想要的數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章