用Python爬取金融市場數據
一、寫在前面
由于在平時的工作中,需要對某信托網的信托在售和資管在售數據進行統計分析,但是一條一條的輸入,顯然太過耗時耗力,于是萌生了寫個爬蟲的想法。
一門計算機語言,可以當做是在模仿人的目的或意圖來進行一系列行為或動作,所以在寫代碼之前,首先要弄清楚你要干什么,如果是你,你每一步的動作是什么,然后將這一步步的動作通過代碼傳遞給計算機,讓計算機高效的幫你完成即可。
本文結合正則表達式和比較流行的beautifulsoup(bs4),對網頁進行解析并提取數據,因此在正式進行之前,有必要簡單介紹下正則表達式和bs4.
二、基礎知識
1、正則表達式
具體的詳細介紹可自行去網上補知識,這里只介紹一些規則和常用的用法。
- # 正則表達式
- 規則:
- 單字符:
- . : 除換行以外所有字符
- [] : 匹配集合中任意一個字符
- \d : 數字
- \D : 非數字
- \w : 數字、字母、下劃線、中文
- \W : 非數字、字母、下劃線、中文
- \s : 空格
- \S : 非空格
- 數量修飾:
- * : 任意多次
- + : 至少1次
- ?: 非貪婪方式,可有可無
- {m} : 固定m次
- {m+} : 至少m次
- {m,n} : m到n次
- 起始:
- ^ : 以啥啥開頭
- $ : 以啥啥結尾
- 常用組合和函數:
- .* : 貪婪方式任意字符任意次數
- .*? : 非貪婪方式任意字符任意次數
- r = re.compile(r'正則表達式',re.S) :
- 最常用:將規則傳遞給某個參數以便反復使用
- re.match\re.search\(字符串)
- re.findall(字符串)
- re.sub(正則表達式,替換內容,字符串)
2、bs4
同樣,詳細知識自行補,這里只介紹常用的用法:select結合選擇器的用法。
- # bs4用法
- 首先加載里面的BeautifulSoup:
- from bs4 import BeautifulSoup
- soup = BeautifulSoup('網頁響應回來的東西')
主要有以下幾種提取規則:
- 1、獲取標簽
- soup.a 獲取a標簽(***個)
- 2、獲取屬性
- soup.a.attrs 獲取a標簽下所有的屬性和值,返回的是字典
- soup.a['name'] 獲取a標簽下的name屬性
- 3、獲取內容
- soup.a.string()
- soup.a.text() 建議使用這個
- 4、find用法
- soup.find('a') 找到***個a
- soup.find('a',title='') 附加條件的查找
- 5、find_all用法
- soup.find_all('a') 找到所有a
- soup.find_all(['a','b']) 找到所有a和b
- soup.find_all('a',limit=5) 找到前5個a
- 6、select用法——重點
- 結合選擇器使用,常用的選擇器如下:
- 標簽選擇器:如div表示為div
- 類選擇器:.表示,如class = 'you'表示為.you
- id選擇器:#表示,如id = 'me'表示為#me
- 組合選擇器:如div,.you,#me
- 層級選擇器:如div .you #me表示選取div標簽下的you類下的id為me的內容
- 再如div > .you > #me,> 則表示只能是下面一級
三、開始實戰——爬取某信托網的信托在售數據
1、爬取前的準備工作——梳理好代碼的邏輯
正如前面所說,寫代碼之前,首先要清楚你想要干什么,如果是你,你是什么樣的動作來達到你的這個目的或意圖。
***,你的目的或意圖是什么,對于本例而言,我需要獲取任意某頁至某頁信托在售產品的下面數據:產品名稱、發行機構、發行時間、***收益、產品期限、投資行業、發行地、收益分配方式、發行規模、***收益、***收益和利率等級劃分情況這12個數據。
第二,如果是人,需要哪些動作來達到這個目的。我們來看下網頁。動作就清晰了:
輸入網址/搜索關鍵字 > 進入網站 > 點擊紅色框框里的信托產品和在售 > 錄入下面綠色框框里的相關信息 > 發現信息不全,再點擊這個產品,在詳情頁(再下一張圖)繼續錄入。
2、開始爬取
既然動作清晰了,那就可以讓計算機來模擬人的這個動作進行爬取了。
然后就是寫代碼的邏輯了。我們用做數學題常用的倒推法來梳理這個過程。
要想獲取數據 < 你得解析網頁給你的響應 < 你得有個響應 < 你得發送請求 < 你得有個請求request < 你得有個url。
然后我們再正過來解題:獲取url > 構建request > 發送請求 > 獲取響應 > 解析響應 > 獲取所需數據 > 保存數據。
所以按照這個步驟,我們可以先做出一個大框架,然后在框架的基礎上補充血肉。大框架,就是定義個主函數。
值得注意的是,本例中,每個產品的信息獲取,我們都有二次點擊的動作,即***頁數據不全,我們再點擊進入詳情頁進行剩余數據的獲取,因此,本例是有兩層的數據獲取過程的。***層使用正則表達式,第二層使用bs4。
① 定義主函數
如下是這個主函數,前面的寫入相關數據你可以先不管,這都是在***步的獲取url時,后補過來的。
回到前面的目的:提取任意某頁至任意某頁的數據,所以寫個循環是必須的,然后在循環下方,兩層網頁的數據獲取框架就出來了。(由于第二層網頁的url是根據***層網頁的某個數據拼接出來的,而***層網頁是一下子提取整個頁面所有產品的信息,所以第二層網頁的提取也設置了個循環,對***層網頁的所有產品,一個一個點進去進行提取)
- # 定義一個主函數
- def main():
- # 寫入相關數據
- url_1 = 'http://www.某信托網.com/Action/ProductAJAX.ashx?'
- url_2 = 'http://www.某信托網/Product/Detail.aspx?'
- size = input('請輸入每頁顯示數量:')
- start_page = int(input('請輸入起始頁碼:'))
- end_page = int(input('請輸入結束頁碼'))
- type = input('請輸入產品類型(1代表信托,2代表資管):')
- items = [] # 定義一個空列表用來存儲數據
- # 寫循環爬取每一頁
- for page in range(start_page, end_page + 1):
- # ***層網頁的爬取流程
- print('第{}頁開始爬取'.format(page))
- # 1、拼接url——可定義一個分函數1:joint
- url_new = joint(url_1 ,size=size ,page=page ,type=type)
- # 2、發起請求,獲取響應——可定義一個分函數2:que_res
- response = que_res(url_new)
- # 3、解析內容,獲取所需數據——可定義一個分函數3:parse_content_1
- contents = parse_content_1(response)
- # 4、休眠2秒
- time.sleep(2)
- # 第二層網頁的爬取流程
- for content in contents:
- print(' 第{}頁{}開始下載'.format(page ,content[0]))
- # 1、拼接url
- id = content[0]
- url_2_new = joint(url_2 ,id=id) # joint為前面定義的第1個函數
- # 2、發起請求,獲取響應
- response_2 = que_res(url_2_new) # que_res為前面定義的第2個函數
- # 3、解析內容,獲取所需數據——可定義一個分函數4:parse_content_2,直接返回字典格式的數據
- item = parse_content_2(response_2 ,content)
- # 存儲數據
- items.append(item)
- print(' 第{}頁{}結束下載'.format(page ,content[0]))
- # 休眠5秒
- time.sleep(5)
- print('第{}頁結束爬取'.format(page))
- # 保存數據為dataframe格式CSV文件
- df = pd.DataFrame(items)
- df.to_csv('data.csv' ,index=False ,sep=',' ,encoding='utf-8-sig')
- print('*'*30)
- print('全部爬取結束')
- if __name__ == '__main__':
- main()
② 獲取url —— ***層和第二層通用
由于我們需要訪問兩層的數據,所以希望定義一個函數,能對兩層的URL都可以進行拼接。
如下圖為***層頁面的內容和源碼,由第二個紅框中的內容(X-Requested-With:XMLHttpRequest),可知這是一個AJAX get請求,且攜帶者第三個紅框中的數據,而第三個紅框中的數據,又恰好是***個紅框中的url的一部分,即為:
http://www.某信托網.com/Action/ProductAJAX.ashx?加上第三個紅框中的數據。
第三個框框中包括幾個可變的數據:pageSize(表示一頁顯示多少產品);pageIndex(表示第幾頁);conditionStr(定義產品類型,1表示信托,2表示資管),其余的數據都是固定的(這其中有個_:1544925791285這種下劃線帶一串數字的東西,像是個隨機數,去掉也沒影響,我就給去掉了)。
下圖為第二層頁面的內容和源碼,可見只是一個簡單的get請求,且網址很簡單,就是一個http://www.某信托網.com/Product/Detail.aspx?加上一個id,而這個id又來自哪里呢,答案就在***層網頁的響應數據中(見再下面一幅圖的紅色框)。
通過上面的分析,***層網頁的請求url由一個固定的部分加上一些數據,第二層網頁的url依賴于***層的數據,我們先在主函數中將url_1、url_2和一些可變的數據寫入(見上面的主函數),然后定義一個函數用來拼接兩層的url即可,因為***層網頁url的固定部分長度為47,第二層的為43,這里使用一個長度條件來判斷是拼接***層還是拼接第二層。
- # 定義第1個分函數joint,用來拼接url
- def joint(url,size=None,page=None,type=None,id=None):
- if len(url) > 45:
- condition = 'producttype:' + type + '|status:在售'
- data = {
- 'mode': 'statistics',
- 'pageSize': size,
- 'pageIndex': str(page),
- 'conditionStr': condition,
- 'start_released': '',
- 'end_released': '',
- 'orderStr': '1',
- 'ascStr': 'ulup'
- }
- joint_str = urllib.parse.urlencode(data)
- url_new = url + joint_str
- else:
- data = {
- 'id':id
- }
- joint_str = urllib.parse.urlencode(data)
- url_new = url + joint_str
- return url_new
③ 構建request + 獲取response一條龍 —— ***層和第二層通用
獲取url后,接下來就是構建request用來發送請求獲取響應了,此處定義一個函數實現一條龍服務。
這里為了提防反爬,user_agent在多個里隨機選,并使用了代理池(雖然不多),并且我電腦端也進行了局域網ip代理。
- # 定義第2個函數que_res,用來構建request發送請求,并返回響應response
- def que_res(url):
- # 構建request的***步——構建頭部:headers
- USER_AGENTS = [
- "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
- "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
- "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
- ]
- user_agent = random.choice(USER_AGENTS)
- headers = {
- 'Accept-Language': 'zh-CN,zh;q=0.8',
- 'Connection': 'keep-alive',
- 'Host': 'www.某信托網.com',
- 'Referer': 'http://www.某信托網.com/Product/Index.aspx',
- 'User-Agent': user_agent,
- 'X-Requested-With': 'XMLHttpRequest'
- }
- # 構建request的第二步——構建request
- request = urllib.request.Request(url=url, headers=headers)
- # 發起請求的***步——構建代理池
- proxy_list = [
- {'http':'125.40.29.100:8118'},
- {'http':'14.118.135.10:808'}
- ]
- proxy = random.choice(proxy_list)
- # 發起請求的第二步——創建handler和opener
- handler = urllib.request.ProxyHandler(proxy)
- opener = urllib.request.build_opener(handler)
- # 發起請求的第三步——發起請求,獲取響應內容并解碼
- response = opener.open(request).read().decode()
- # 返回值
- return response
④ 解析***層網頁的內容
獲取響應之后就是解析并提取數據了,***層使用正則表達式的方法來進行。
獲取的response如下如:
因此可寫出如下正則,從左到右分配匹配出ID、產品名稱、發行機構、發行時間、產品期限、投資行業、首頁收益。
- # 定義第3個函數parse_content_1,用來解析并匹配***層網頁內容,此處使用正則表達式方法
- def parse_content_1(response):
- # 寫正則進行所需數據的匹配
- re_1 = re.compile(
- r'{"ROWID".*?"ID":"(.*?)","Title":"(.*?)","producttype".*?"issuers":"(.*?)","released":"(.*?) 0:00:00","PeriodTo":(.*?),"StartPrice".*?"moneyinto":"(.*?)","EstimatedRatio1":(.*?),"status":.*?"}')
- contents = re_1.findall(response)
- return contents
⑤ 解析第二層網頁的內容并輸出數據
第二層使用bs4中的select+選擇器的方法來進行。除了***層所提取的數據外,還需要發行地、收益分配方式、發行規模、***收益、***收益和利率等級分布情況。
網頁如下,可見,我們所需要的信息隱藏在一個又一個tr標簽里,而這個tr標簽處于id=“procon1”下的一個table標簽里(此處有個坑,就是從網頁來看,table下還有個tbody標簽,而實際得到的響應里并沒有)。
由于我們不是所有的信息都要,所以我們可以一個一個的提取,最終輸出個數據。代碼如下(這中間用到了前面提到的選擇器知識和一些字符串處理方法):
- # 定義第4個函數parse_content_2,用來解析并匹配第二層網頁內容,并輸出數據,此處使用BeautifulSoup方法
- def parse_content_2(response,content):
- # 使用bs4進行爬取第二層信息
- soup = BeautifulSoup(response)
- # 爬取發行地和收益分配方式,該信息位于id為procon1下的table下的第4個tr里
- tr_3 = soup.select('#procon1 > table > tr')[3]
- address = tr_3.select('.pro-textcolor')[0].text
- r_style = tr_3.select('.pro-textcolor')[1].text
- # 爬取發行規模,該信息位于id為procon1下的table下的第5個tr里
- tr_4 = soup.select('#procon1 > table > tr')[4]
- guimo = tr_4.select('.pro-textcolor')[1].text
- re_2 = re.compile(r'.*?(\d+).*?', re.S)
- scale = re_2.findall(guimo)[0]
- # 爬取收益率,該信息位于id為procon1下的table下的第8個tr里
- tr_7 = soup.select('#procon1 > table > tr')[7]
- rate = tr_7.select('.pro-textcolor')[0].text[:(-1)]
- r = rate.split('至')
- r_min = r[0]
- r_max = r[1]
- # 提取利率等級
- tr_11 = soup.select('#procon1 > table > tr')[11]
- r_grade = tr_11.select('p')[0].text
- # 保存數據到一個字典中
- item = {
- '產品名稱':content[1],
- '發行機構':content[2],
- '發行時間':content[3],
- '產品期限':content[4],
- '投資行業':content[5],
- '首頁收益':content[6],
- '發行地': address,
- '收益分配方式': r_style,
- '發行規模': scale,
- '***收益': r_min,
- '***收益': r_max,
- '利率等級': r_grade
- }
- # 返回數據
- return item
⑥ 保存數據到本地(以dataframe格式保存到本地CSV格式)
- # 保存數據為dataframe格式CSV文件
- df = pd.DataFrame(items)
- df.to_csv('data.csv',index=False,sep=',',encoding='utf-8-sig')
- 好了,現在就大功告成了,***不要只讓自己爽,也要讓對方的服務器別太難過,在一些地方休眠幾秒,完整代碼如下。
- import urllib.request
- import urllib.parse
- import re
- import random
- from bs4 import BeautifulSoup
- import pandas as pd
- import time
- # 定義第1個分函數joint,用來拼接url
- def joint(url,size=None,page=None,type=None,id=None):
- if len(url) > 45:
- condition = 'producttype:' + type + '|status:在售'
- data = {
- 'mode': 'statistics',
- 'pageSize': size,
- 'pageIndex': str(page),
- 'conditionStr': condition,
- 'start_released': '',
- 'end_released': '',
- 'orderStr': '1',
- 'ascStr': 'ulup'
- }
- joint_str = urllib.parse.urlencode(data)
- url_new = url + joint_str
- else:
- data = {
- 'id':id
- }
- joint_str = urllib.parse.urlencode(data)
- url_new = url + joint_str
- return url_new
- # 定義第2個函數que_res,用來構建request發送請求,并返回響應response
- def que_res(url):
- # 構建request的***步——構建頭部:headers
- USER_AGENTS = [
- "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
- "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
- "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
- "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
- ]
- user_agent = random.choice(USER_AGENTS)
- headers = {
- 'Accept-Language': 'zh-CN,zh;q=0.8',
- 'Connection': 'keep-alive',
- 'Host': 'www.某信托網.com',
- 'Referer': 'http://www.某信托網.com/Product/Index.aspx',
- 'User-Agent': user_agent,
- 'X-Requested-With': 'XMLHttpRequest'
- }
- # 構建request的第二步——構建request
- request = urllib.request.Request(url=url, headers=headers)
- # 發起請求的***步——構建代理池
- proxy_list = [
- {'http':'125.40.29.100:8118'},
- {'http':'14.118.135.10:808'}
- ]
- proxy = random.choice(proxy_list)
- # 發起請求的第二步——創建handler和opener
- handler = urllib.request.ProxyHandler(proxy)
- opener = urllib.request.build_opener(handler)
- # 發起請求的第三步——發起請求,獲取響應內容并解碼
- response = opener.open(request).read().decode()
- # 返回值
- return response
- # 定義第3個函數parse_content_1,用來解析并匹配***層網頁內容,此處使用正則表達式方法
- def parse_content_1(response):
- # 寫正則進行所需數據的匹配
- re_1 = re.compile(
- r'{"ROWID".*?"ID":"(.*?)","Title":"(.*?)","producttype".*?"issuers":"(.*?)","released":"(.*?) 0:00:00","PeriodTo":(.*?),"StartPrice".*?"moneyinto":"(.*?)","EstimatedRatio1":(.*?),"status":.*?"}')
- contents = re_1.findall(response)
- return contents
- # 定義第4個函數parse_content_2,用來解析并匹配第二層網頁內容,并輸出數據,此處使用BeautifulSoup方法
- def parse_content_2(response,content):
- # 使用bs4進行爬取第二層信息
- soup = BeautifulSoup(response)
- # 爬取發行地和收益分配方式,該信息位于id為procon1下的table下的第4個tr里
- tr_3 = soup.select('#procon1 > table > tr')[3] #select到第四個目標tr
- address = tr_3.select('.pro-textcolor')[0].text #select到該tr下的class為pro-textcolor的***個內容(發行地)
- r_style = tr_3.select('.pro-textcolor')[1].text #select到該tr下的class為pro-textcolor的第二個內容(收益分配方式)
- # 爬取發行規模,該信息位于id為procon1下的table下的第5個tr里
- tr_4 = soup.select('#procon1 > table > tr')[4] #select到第五個目標tr
- guimo = tr_4.select('.pro-textcolor')[1].text #select到該tr下的class為pro-textcolor的第二個內容(發行規模:至***萬)
- re_2 = re.compile(r'.*?(\d+).*?', re.S) #設立一個正則表達式,將純數字提取出來
- scale = re_2.findall(guimo)[0] #提取出純數字的發行規模
- # 爬取收益率,該信息位于id為procon1下的table下的第8個tr里
- tr_7 = soup.select('#procon1 > table > tr')[7] #select到第八個目標tr
- rate = tr_7.select('.pro-textcolor')[0].text[:(-1)] #select到該tr下的class為pro-textcolor的***個內容(且通過下標[-1]將末尾的 % 去除)
- r = rate.split('至') #此處用來提取***收益和***收益
- r_min = r[0]
- r_max = r[1]
- # 提取利率等級
- tr_11 = soup.select('#procon1 > table > tr')[11] #select到第十二個目標tr
- r_grade = tr_11.select('p')[0].text #select到該tr下的p下的***個內容(即利率等級)
- # 保存數據到一個字典中
- item = {
- '產品名稱':content[1],
- '發行機構':content[2],
- '發行時間':content[3],
- '產品期限':content[4],
- '投資行業':content[5],
- '首頁收益':content[6],
- '發行地': address,
- '收益分配方式': r_style,
- '發行規模': scale,
- '***收益': r_min,
- '***收益': r_max,
- '利率等級': r_grade
- }
- # 返回數據
- return item
- # 定義一個主函數
- def main():
- # 寫入相關數據
- url_1 = 'http://www.某信托網.com/Action/ProductAJAX.ashx?'
- url_2 = 'http://www.某信托網.com/Product/Detail.aspx?'
- size = input('請輸入每頁顯示數量:')
- start_page = int(input('請輸入起始頁碼:'))
- end_page = int(input('請輸入結束頁碼'))
- type = input('請輸入產品類型(1代表信托,2代表資管):')
- items = [] # 定義一個空列表用來存儲數據
- # 寫循環爬取每一頁
- for page in range(start_page, end_page + 1):
- # ***層網頁的爬取流程
- print('第{}頁開始爬取'.format(page))
- # 1、拼接url——可定義一個分函數1:joint
- url_new = joint(url_1,size=size,page=page,type=type)
- # 2、發起請求,獲取響應——可定義一個分函數2:que_res
- response = que_res(url_new)
- # 3、解析內容,獲取所需數據——可定義一個分函數3:parse_content_1
- contents = parse_content_1(response)
- # 4、休眠2秒
- time.sleep(2)
- # 第二層網頁的爬取流程
- for content in contents:
- print(' 第{}頁{}開始下載'.format(page,content[0]))
- # 1、拼接url
- id = content[0]
- url_2_new = joint(url_2,id=id) # joint為前面定義的第1個函數
- # 2、發起請求,獲取響應
- response_2 = que_res(url_2_new) # que_res為前面定義的第2個函數
- # 3、解析內容,獲取所需數據——可定義一個分函數4:parse_content_2,直接返回字典格式的數據
- item = parse_content_2(response_2,content)
- # 存儲數據
- items.append(item)
- print(' 第{}頁{}結束下載'.format(page,content[0]))
- # 休眠5秒
- time.sleep(5)
- print('第{}頁結束爬取'.format(page))
- # 保存數據為dataframe格式CSV文件
- df = pd.DataFrame(items)
- df.to_csv('data.csv',index=False,sep=',',encoding='utf-8-sig')
- print('*'*30)
- print('全部爬取結束')
- if __name__ == '__main__':
- main()
3、爬取結果
運行代碼,這里以每頁顯示4個產品,爬取前3頁的信托在售為例,運行結果如下:
然后打開存到本地的CSV文件如下:結果是美好的。
這種兩層網頁的數據抓取,可以用在非常非常非常多的地方呦。