在簡單完成了基金凈值爬取以后,我們對中間的過程可能產生了很多疑惑,即使完成了目標,也僅僅是知其然而不知其所以然,而為了以后爬蟲任務的順利進行,對爬蟲過程中所涉及的原理進行掌握是十分有必要的。
本文將會針對之前爬蟲過程中所涉及到的幾個爬蟲原理進行簡單的闡述。
url究竟是什么?它的構成有什么規律可循?
URL和URI
在訪問任何一個網頁時,我們都需要一個網頁鏈接(如百度: www.baidu.com),這就相當於網頁的“家庭地址”一樣,只有在知道了這個“地址”,我們才能看到“這戶人家”長得什么樣。而這個“地址”在大部分時候也被稱為URL,全稱為Universal Resource Locator,即統一資源定位符。
除了URL,還有一個極少聽到的名詞——URI,全稱為Uniform Resource Identifier,即統一資源標志符。
以獲取基金代碼列表時用到的鏈接為例——
http://fund.eastmoney.com/js/fundcode_search.js是天天基金網基金代碼的數據存儲鏈接,它是一個URL,也是一個URI。即有這樣的數據資源,我們用URL/URl來唯一指定了它的訪問方式,這其中包括了訪問協議http、訪問路徑(/即根目錄)和資源名稱fundcode_search.js。通過這樣的一個鏈接,我們便可以從互聯網上找到這個資源,這就是URL/URI。
URL是URI的自己,也就是說每個URL都是URI,但不是每一個URI都是URL,URI的子集中還包括URN,它在目前的互聯網中用得非常少,幾乎所有的URI都是URL,因此,一般的網頁鏈接我們都可以直接、也慣稱為URL。
URL的解析
在爬取基金代碼和基金凈值數據時,仔細觀察相關的URL,我們可以發現它們的構成並非是無規律可循的。而事實上,URL的構成也確實存在一套統一的標准。
protocol://domain[:port]/path/[?parameters]#fragment
- protocol 協議:標明了請求需要使用的協議,通常使用的是
HTTP
協議或者安全協議HTTPS
.其他協議還有mailto:
用戶打開郵箱的客戶端,和ftp:
用來做文件的轉換,file
用來獲取文件,data
獲取外部資源等 - domain 域名:標明了需要請求的服務器的地址,一個URL中也可以使用IP地址作為域名使用
- port 端口:標明了獲取服務器資源的入口端口號用於區分服務的端口,一台擁有IP地址的服務器可以提供許多服務,比如
Web
服務、FTP
服務、SMTP
服務等。那么,服務器的資源通過“IP地址+端口號”來區分不同的服務。如果把服務器比作房子,端口號可以看做是通向不同服務的門。端口不是一個URL必須的部分,如果省略端口部分,將采用默認端口,一般為80。 - path 路徑:表示服務器上資源的路徑,從域名后的最后一個“/”開始到“?”為止,是文件名部分,如果沒有“?”,則是從域名后的最后一個“/”開始到“#”為止,是文件部分,如果沒有“?”和“#”,那么從域名后的最后一個“/”開始到結束,都是文件名部分。過去這樣的路徑標記的是服務器上文件的物理路徑,但是現在,路徑表示的只是一個抽象地址,並不指代任何物理地址。文件名部分也不是一個URL必須的部分,如果省略該部分,則使用默認的文件名。
- parameter 參數:從“?”開始到“#”為止之間的部分為參數部分,又稱搜索部分、查詢部分。這些參數是以鍵值對的形式,通過
&
符號分隔開來,服務器可以通過這些參數進行相應的個性化處理。 - fragment 片段:可以理解為資源內部的
書簽,
用來想服務器指明展示的內容所在的書簽
的點。例如對於HTML
文件來說,瀏覽器會滾動到特定的或者上次瀏覽過的位置,對於音頻或者視頻資源來說,瀏覽器又會跳轉到對應的時間節點。錨部分也不是一個URL必須的部分。
requests.get()中的headers和params參數又是什么?
當我們嘗試獲取網頁內容時,我們會用到requests.get()訪問網站的服務器,然后獲取想到得到的網頁內容。
params參數
requests.get()中的params參數就是為了將一些特別長,且明顯有規律的URL,如:
http://api.fund.eastmoney.com/f10/lsjz?callback=jQuery18303213780505917203_1548395296124&fundCode=000001&pageIndex=1&pageSize=20&startDate=&endDate=&_=1548395296139
以參數化的方式傳入,讓其URL組合更為簡潔和格式化。
headers參數
而我們在獲取基金凈值數據時發現,直接用URL訪問並不能獲得我們想要的內容,而是加上參數headers才成功。
這是因為對一些網頁進行訪問時,在你發送請求給服務器的過程中,需要使用一些附加信息,在獲得服務器的識別和准許后,才能返回給你你所想要的內容。這就像我們平時到某個小區去看望朋友時,保安會在需要確認你的信息后才會放你同行一樣。
常用頭信息
Accept |
請求報頭域,用於指定客戶端可接受哪些類型的信息 |
Accept-Language |
指定客戶端可接受的語言類型 |
Accept-Encoding |
指定客戶端接受的內容編碼 |
Host | 用於指定請求資源的主機IP和端口號,其內容為請求URL的原始服務器或網關的位置 |
Cookie | 也常用復數形式Cookies,這是網站為了辨別用戶進行會話跟蹤而存儲在用戶本地的數據,它的主要功能是維持當前訪問會話 |
Referer | 此內容用來標識這個請求是從哪個頁面發過來的,服務器可以拿到這一信息並做相應的處理,如做來源統計、防盜鏈處理等 |
User-Agent | 特殊的字符串頭,可以使服務器識別客戶使用的操作系統及版本、瀏覽器及版本等信息。 |
Content-Type | 在HTTP協議消息頭中,它用來表示具體請求中的媒體類型信息 |
是否只有get一種訪問形式?
每一次訪問網頁都是一次向服務器發出請求的過程。更具體的說,在瀏覽器中輸入URL,回車之后會在瀏覽器觀察到頁面內容,這個過程就是瀏覽器向網站所在的服務器發送一個請求,網站服務器接收到這個請求后進行處理和解析,然后返回對應的響應,接着傳回給瀏覽器。響應里包含了頁面的源代碼等內容,瀏覽器再對其進行解析,便將網頁呈現出來,也就是最終我們在瀏覽器上所看到的效果。
請求由客戶端(手機或PC瀏覽器)向服務器發出,可分為四部分:請求方法、請求的網址、請求頭和請求體。而get則是一種請求方法。
請求方法
常見的請求方法:GET和POST。
基金凈值數據的訪問,就是一種GET請求,鏈接為——
http://api.fund.eastmoney.com/f10/lsjz?callback=jQuery18303213780505917203_1548395296124&fundCode=000001&pageIndex=1&pageSize=20&startDate=&endDate=&_=1548395296139
其中URL中包含了請求的參數信息。
POST請求大多在表單提交時發起,常見於登錄過程中發送的登錄表單,在輸入用戶名和密碼后點擊登錄,通常便會發起一個POST請求,其數據通常以表單形式傳輸,不會體現在URL中。
其他請求方法:
- GET:請求頁面,並返回頁面的內容
- HEAD:類似於GET請求,只不過返回的響應中沒有具體的內容,用於獲取報頭
- POST:大多用於提交表單或上傳文件,數據包含在請求體中
- PUT:從客戶端向服務器傳送的數據取代指定文檔中的內容
- DELETE:請求服務器刪除指定的頁面
- CONNECT:把服務器當做跳板,讓服務器代替客戶端訪問其他網頁
- OPTIONS:允許客戶端查看服務器的性能
- TRACE:回顯服務器收到的請求,主要用於測試或診斷
請求的網址
即URL
請求頭
用來說明服務器要使用的附加信息,比如:Cookie、Referer、User-Agent等。
請求體
一般承載的內容是POST請求中的表單數據,而對於GET請求,請求體則為空。
在爬蟲中,如果要構造POST請求,需要使用正確的Content-Type,並了解各種請求庫的各個參數設置時使用的是哪種Content-Type,不然可能會導致POST提交后無法正常響應
當我們嘗試直接用循環去爬取所有內容的時候,是否會遭遇反爬?又該如何解決反爬?
如果直接用循環去爬取網頁內容時,經常,通常會由於被判別為爬蟲而被禁止,也就是常說的反爬蟲。
這種現象出現的原因往往是因為網址采取的反爬蟲措施,比如,服務器會檢測某個IP在單位時間內的請求次數,如果超過了這個閾值,就會直接拒絕服務,返回一些錯誤信息,統稱這種情況被稱為封IP。
遭遇反爬蟲時,通常由兩種思路:
1. 延長訪問間隔時間:在每次循環訪問時,用time.sleep方法,設置訪問的間隔時間,但這種方法會降低爬蟲的效率。
2. 代理IP:既然封IP是由於同一IP訪問次數太多,那么我們如果借助代理IP的方式讓服務器以為是不同的IP在訪問服務器,就能有效的防止被封IP,但有效的代理IP通常也並不容易找到,需要付費購買。
因此,如果你的爬蟲項目密度不大,可以采取第一種方式來語法反爬取,如果初始的爬蟲量大,可以選擇分批次爬取,整理后的代碼如下:

1 def get_fundcode(): 2 ''' 3 獲取fundcode列表 4 :return: 將獲取的DataFrame以csv格式存入本地 5 ''' 6 url = 'http://fund.eastmoney.com/js/fundcode_search.js' 7 r = requests.get(url) 8 cont = re.findall('var r = (.*])', r.text)[0] # 提取list 9 ls = json.loads(cont) # 將字符串個事的list轉化為list格式 10 fundcode = pd.DataFrame(ls, columns=['fundcode', 'fundsx', 'name', 'category', 'fundpy']) # list轉為DataFrame 11 fundcode = fundcode.loc[:, ['fundcode', 'name', 'category']] 12 fundcode.to_csv('./案例/基金凈值爬取/fundcode.csv', index=False) 13 14 15 def get_one_page(fundcode, pageIndex=1): 16 ''' 17 獲取基金凈值某一頁的html 18 :param fundcode: str格式,基金代碼 19 :param pageIndex: int格式,頁碼數 20 :return: str格式,獲取網頁內容 21 ''' 22 url = 'http://api.fund.eastmoney.com/f10/lsjz' 23 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' 24 headers = { 25 'Cookie': cookie, 26 'Host': 'api.fund.eastmoney.com', 27 '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', 28 'Referer': 'http://fundf10.eastmoney.com/jjjz_%s.html' % fundcode, 29 } 30 params = { 31 'callback': 'jQuery18307633215694564663_1548321266367', 32 'fundCode': fundcode, 33 'pageIndex': pageIndex, 34 'pageSize': 20, 35 } 36 try: 37 r = requests.get(url=url, headers=headers, params=params) 38 if r.status_code == 200: 39 return r.text 40 return None 41 except RequestException: 42 return None 43 44 45 def parse_one_page(html): 46 ''' 47 解析網頁內容 48 :param html: str格式,html內容 49 :return: dict格式,獲取歷史凈值和訪問頁數 50 ''' 51 if html is not None: # 判斷內容是否為None 52 content = re.findall('\((.*?)\)', html)[0] # 提取網頁文本內容中的數據部分 53 lsjz_list = json.loads(content)['Data']['LSJZList'] # 獲取歷史凈值列表 54 total_count = json.loads(content)['TotalCount'] # 獲取數據量 55 total_page = math.ceil(total_count / 20) # 56 lsjz = pd.DataFrame(lsjz_list) 57 info = {'lsjz': lsjz, 58 'total_page': total_page} 59 return info 60 return None 61 62 63 def main(fundcode): 64 ''' 65 將爬取的基金凈值數據儲存至本地csv文件 66 ''' 67 html = get_one_page(fundcode) 68 info = parse_one_page(html) 69 total_page = info['total_page'] 70 lsjz = info['lsjz'] 71 lsjz.to_csv('./案例/基金凈值爬取/%s_lsjz.csv' % fundcode, index=False) # 將基金歷史凈值以csv格式儲存 72 page = 1 73 while page < total_page: 74 page += 1 75 print(lsjz) 76 html = get_one_page(fundcode, pageIndex=page) 77 info = parse_one_page(html) 78 if info is None: 79 break 80 lsjz = info['lsjz'] 81 lsjz.to_csv('./案例/基金凈值爬取/%s_lsjz.csv' % fundcode, mode='a', index=False, header=False) # 追加存儲 82 time.sleep(random.randint(5, 10)) 83 84 85 if __name__=='__main__': 86 # 獲取所有基金代碼 87 get_fundcode() 88 # fundcode = '519961' 89 fundcodes = pd.read_csv('./案例/基金凈值爬取/fundcode.csv', converters={'fundcode': str}) 90 # 獲取所有基金凈值數據 91 for fundcode in fundcodes['fundcode']: 92 print(fundcode) 93 main(fundcode) 94 time.sleep(random.randint(5, 10))
上述代碼雖然提供了基金凈值數據爬蟲的完整代碼,但在具體項目的實施中,還需要根據項目的更新需求和更新周期來增加更新機制,這時,一個較為完整的爬蟲小程序才算完成~