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