相信很多金融類的從業(yè)者和學者都比較偏好于爬取金融類數(shù)據(jù),比如博主我?(? ? ??)
在完成了簡單的環(huán)境配置后,博主我安耐不住雞凍的心情,打算先爬個基金數(shù)據(jù)以解手癢,順便通過這個案例簡單了解一下其中涉及到的一些爬蟲原理
1、Chrome及其developer tools
2、python3.7
3、PyCharm
1、requests
2、re——正則表達式
3、json
4、pandas
5、math
6、sqlalchemy——博主選擇用萬能的SQLAlchemy完成數(shù)據(jù)庫的存儲,小規(guī)模爬蟲的盆友可以直接保存到本地的csv文件
Mac OS 10.13.2
在此,博主選擇的是爬蟲對象是一個叫天天基金的boy,爬蟲的目標是獲得基金的凈值數(shù)據(jù)。
在正式爬取每個基金的數(shù)據(jù)之前,我們應先獲取一個基金列表,再根據(jù)列表里面所列出來的基金逐一進行爬取。由此,我們的爬蟲步驟將分為以下兩步:
1、獲取基金代碼列表
2、爬取基金凈值的數(shù)據(jù)
用Chrome瀏覽器登錄天天基金的網(wǎng)頁,找到基金代碼列表的對應網(wǎng)址,用瀏覽器強大的developer tool來觀察網(wǎng)頁的內(nèi)容,檢查js文件后,發(fā)現(xiàn)了一個可疑的對象——fundcode_search.js:
雙擊點開這個js文件,我們驚奇的發(fā)現(xiàn)——OMG! 基金代碼居然都在里面?。?!
我們嘗試用requests.get直接獲取網(wǎng)頁內(nèi)容試試
1 import requests2 r = requests.get('http://fund.eastmoney.com/js/fundcode_search.js')3 r.text
我們獲取的內(nèi)容為
很顯然,我們需要獲取的基金代碼是以list的形式存儲的,為了獲取這段字符串中的list,我們可以直接通過正則表達式提取list。
但是,直接通過正則表達式獲取的list是以字符串形式呈現(xiàn)的,為了將其轉(zhuǎn)為list格式,這里我們則需要用到json.loads()函數(shù)
1 cont = re.findall('var r = (.*])', r.text)[0] # 提取list2 ls = json.loads(cont) # 將字符串個事的list轉(zhuǎn)化為list格式3 all_fundCode = pd.DataFrame(ls, columns=['基金代碼', '基金名稱縮寫', '基金名稱', '基金類型', '基金名稱拼音']) # list轉(zhuǎn)為DataFrame
這樣,通過一段簡單的代碼,我們便獲取到了所有的基金代碼。
那么,我們獲取了所有基金代碼的列表后,這些數(shù)據(jù)對我們接下來爬取基金凈值又有什么用呢?我們來隨機選取一個基金(以000001華夏成長為例),點擊進入網(wǎng)頁進行觀察
觀察其url:http://fund.eastmoney.com/000001.html,不難發(fā)現(xiàn)這個網(wǎng)頁是由“固定組成+基金代碼”的形式構(gòu)成的,我們再進入凈值入口觀察其url構(gòu)成
顯然,這也是一段規(guī)律性極強的url,我們可以大膽的猜測,所有基金凈值的url形式均是形如“http://fundf10.eastmoney.com/jjjz_基金代碼.html”。
由此,我們之前獲取基金代碼的作用就顯而易見了。
下面,我們還需要尋找基金歷史凈值的網(wǎng)頁信息,看到Network里面密密麻麻一大堆網(wǎng)頁文件,真的讓人很暈+_+,難道我們只能把這些文件一個個點開來么?
倔強的我當然是不會屈服的【其實是懶】。
既然懶得找,不如直接搜一搜試試?于是根據(jù)之前基金網(wǎng)頁的url構(gòu)成經(jīng)驗,我大膽的猜測凈值數(shù)據(jù)的文件名,于是乎……還真被我找到了?。 ? ^皿^)っHiahia…
繼續(xù)用Chrome的開發(fā)者工具觀察該網(wǎng)頁文件的Headers信息,我們可以得到“請求頭文件(Request Headers)”和“URL訪問的構(gòu)成參數(shù)(Query String Parameters)”的信息
其中,觀察Query String Parameters的信息,對比發(fā)出請求的URL,不難發(fā)現(xiàn)URL的構(gòu)成中各個參數(shù)的意義:
callback:調(diào)回函數(shù),是JavaScript中的一種高級函數(shù),一種被作為參數(shù)傳遞給另一個函數(shù)的高級函數(shù),申請調(diào)用的函數(shù)就是jQuery函數(shù)
fundCode:基金代碼
pageIndex:對應基金歷史凈值明細的頁數(shù)
pageSize:每頁返回的數(shù)據(jù)個數(shù)
startDate/endDate:歷史凈值明細中起始和截止日期的篩選
_:訪問的時間戳
那么,如果我們和之前一樣,直接雙擊這個js文件會怎么樣呢?
我們可以看到,之前在developer tool里看見的數(shù)據(jù)都消失了,這是為什么呢?
先別著急,我們先用requests.get()試一下,看看得到的內(nèi)容是否也是這樣。
url = 'http://api.fund.eastmoney.com/f10/lsjz?callback=jQuery18303213780505917203_1548395296124&fundCode=000001&pageIndex=1&pageSize=20&startDate=&endDate=&_=1548395296139'r = requests.get(url=url) r.text >>> 'jQuery18303213780505917203_1548395296124({"Data":"","ErrCode":-999,"ErrMsg":"","TotalCount":0,"Expansion":null,"PageSize":0,"PageIndex":0})'
果然,直接對該url進行訪問的結(jié)果和之前在瀏覽器內(nèi)直接打開該鏈接所得到的結(jié)果是一樣的,為了找到其原因,我們繼續(xù)利用開發(fā)者工具觀察該網(wǎng)頁的信息
對比后不難發(fā)現(xiàn),它的Request Headers內(nèi)少了Refer,再回頭看看Refer的內(nèi)容——“http://fundf10.eastmoney.com/jjjz_000001.html”,正是我們訪問基金凈值的源網(wǎng)頁。
由此,我們可以大膽的推理,存儲基金凈值數(shù)據(jù)的js文件是在客戶端發(fā)出訪問請求時,是會通過識別Refer這一信息來判斷是否返回數(shù)據(jù)的。因此,我們在發(fā)出請求時,必須要把Refer這一信息帶上才行。
根據(jù)這一結(jié)論,我們來更新一下自己的代碼
1 fundCode = '000001' 2 pageIndex = 1 3 url = 'http://api.fund.eastmoney.com/f10/lsjz' 4 5 # 參數(shù)化訪問鏈接,以dict方式存儲 6 params = { 7 'callback': 'jQuery18307633215694564663_1548321266367', 8 'fundCode': fundCode, 9 'pageIndex': pageIndex,10 'pageSize': 20,11 }12 # 存儲cookie內(nèi)容13 cookie = 'EMFUND1=null; EMFUND2=null; EMFUND3=null; EMFUND4=null; EMFUND5=null; EMFUND6=null; EMFUND7=null; EMFUND8=null; EMFUND0=null; EMFUND9=01-24 17:11:50@#$%u957F%u4FE1%u5229%u5E7F%u6DF7%u5408A@%23%24519961; st_pvi=27838598767214; st_si=11887649835514'14 # 裝飾頭文件15 headers = {16 'Cookie': cookie,17 'Host': 'api.fund.eastmoney.com',18 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',19 'Referer': 'http://fundf10.eastmoney.com/jjjz_%s.html' % fundCode,20 }21 r = requests.get(url=url, headers=headers, params=params) # 發(fā)送請求22 23 r.text
運行代碼后成功獲取了歷史凈值是數(shù)據(jù),其內(nèi)容是嵌套在jQuery內(nèi)的一個dict,我們可以和之前一樣,用正則表達的方法提取出dict,并用json.loads()函數(shù)將這段string格式的dict轉(zhuǎn)為dict格式,以獲取目標數(shù)據(jù)
1 text = re.findall('\((.*?)\)', r.text)[0] # 提取dict2 LSJZList = json.loads(text)['Data']['LSJZList'] # 獲取歷史凈值數(shù)據(jù)3 TotalCount = json.loads(text)['TotalCount'] # 轉(zhuǎn)化為dict4 LSJZ = pd.DataFrame(LSJZList) # 轉(zhuǎn)化為DataFrame格式5 LSJZ['fundCode'] = fundCode # 新增一列fundCode
上述代碼獲得的結(jié)果如下:
在成功爬取了一頁的歷史凈值后,下一個我們需要解決的問題是——爬多少頁?
此時,之前獲取的網(wǎng)頁內(nèi)容中,有一個叫TotalCount的數(shù)據(jù)引起了我的注意
在這個數(shù)據(jù)的幫助下,如果我們想要確定爬取的頁數(shù),只要將TotalCount?PageSize,然后取整數(shù),就可以搞定了~
1 total_page = math.ceil(total_count / 20)
在確定了需要爬取的頁數(shù)后,只要寫一個簡單的循環(huán),便可以遍歷所有的數(shù)據(jù)了~
以上,我們完成了一個簡單的基金歷史凈值爬取的目標,和核心代碼的部分展示,但在過程中,其實還有很多細節(jié)上的疑問和問題沒有得到解決,比如:
在講到參數(shù)化訪問鏈接時,我們會想知道url究竟是什么?它的構(gòu)成有什么規(guī)律可循?
進行requests.get()訪問時,其中的headers和params參數(shù)又是什么?還有什么別的參數(shù)設(shè)置么?
是否只有g(shù)et一種訪問形式?
Request Headers中列出的內(nèi)容又分別代表什么?
當我們嘗試直接用循環(huán)去爬取所有內(nèi)容的時候,真的安全么?是否會遭遇反爬?又該如何解決反爬?
在具體項目實施中,我們還需要考慮到數(shù)據(jù)的如何更新?
這些問題有些涉及到爬蟲原理,有些涉及到項目實操,針對這些問題的解答和完整代碼的演示,則由下篇來解答~
聯(lián)系客服