實(shí)戰(zhàn) | PyQt5制作雪球網(wǎng)股票數(shù)據(jù)爬蟲工具

最近有盆友需要幫忙寫個爬蟲腳本,爬取雪球網(wǎng)一些上市公司的財(cái)務(wù)數(shù)據(jù)。盆友希望可以根據(jù)他自己的選擇進(jìn)行自由的抓取,所以簡單給一份腳本交給盆友,盆友還需要自己搭建python環(huán)境,更需要去熟悉一些參數(shù)修改的操作,想來也是太麻煩了。
于是,結(jié)合之前做過的匯率計(jì)算器小工具,我這邊決定使用PyQt5給朋友制作一個爬蟲小工具,方便他的操作可視化。
一、效果演示

二、功能說明
可以自由選擇證券市場類型:A股、美股和港股 可以自由選擇上市公司:單選或全選 可以自由選擇財(cái)務(wù)數(shù)據(jù)類型:單選或全選(主要指標(biāo)、利潤表、資產(chǎn)負(fù)債表、現(xiàn)金流表) 可以導(dǎo)出數(shù)據(jù)存儲為excel表格文件 支持同一家上市公司同類型財(cái)務(wù)數(shù)據(jù)追加
三、制作過程
首先引入需要的庫
import?sys
from?PyQt5?import?QtCore,?QtGui,?QtWidgets
from?PyQt5.QtWidgets?import?QApplication,?QMainWindow,QFileDialog
import?os
import?requests
from?fake_useragent?import?UserAgent
import?json
import??logging
import?time
import?pandas?as?pd
from?openpyxl?import?load_workbook
雪球網(wǎng)頁拆解
這一步的目的是獲取需要爬取的數(shù)據(jù)的真正URL地址規(guī)律。
當(dāng)我選中某只股票查看財(cái)務(wù)數(shù)據(jù)某類型數(shù)據(jù)報(bào)告時,點(diǎn)擊下一頁,網(wǎng)站地址沒有變化,基本可以知道這是動態(tài)加載的數(shù)據(jù),對于這類數(shù)據(jù)可以使用F12打開開發(fā)者模式。

在開發(fā)者模式下,選到Network—>XHR可以查看到真正的數(shù)據(jù)獲取地址URL及請求方式(General里是請求URL和請求方式說明,Request Headers有請求頭信息,如cookie,Query String Parameters就是可變參數(shù)項(xiàng),一般來說數(shù)據(jù)源URL就是由基礎(chǔ)URL和這里的可變參數(shù)組合而成)

我們分析這段URL,可以發(fā)現(xiàn)其基本結(jié)構(gòu)如下:

基于上述結(jié)構(gòu),我們拆分最終的組合URL地址如下
#基礎(chǔ)網(wǎng)站
base_url?=?f'https://stock.xueqiu.com/v5/stock/finance/{ABtype}'
#組合url地址
url?=?f'{base_url}/{data_type}.json?symbol={ipo_code}&type=all&is_detail=true&count={count_num}×tamp={start_time}'
操作界面設(shè)計(jì)
操作界面設(shè)計(jì)使用的是PyQt5,這里不做更詳細(xì)的介紹,我們在后續(xù)中對PyQt5的使用再專題講解。
使用QT designer對操作界面進(jìn)行可視化設(shè)計(jì),參考如下:

雪球網(wǎng)數(shù)據(jù)提取.ui中各個組件的相關(guān)設(shè)置,參考如下:

.ui文件可以使用pyuic5指令進(jìn)行編譯生成對應(yīng)的.py文件,或者我們也可以在vscode里直接轉(zhuǎn)譯(這里也不做更詳細(xì)的介紹,具體見后續(xù)專題講解)。
本文沒有將操作界面定義文件單獨(dú)使用,而是將全部代碼集中在同一個.py文件,因此其轉(zhuǎn)譯后的代碼備用即可。
獲取cookie及基礎(chǔ)參數(shù)
獲取cookie
為了便于小工具拿來即可使用,我們需要自動獲取cookie地址并附加在請求頭中,而不是人為打開網(wǎng)頁在開發(fā)者模式下獲取cookie后填入。
自動獲取cookie,這里使用到的requests庫的session會話對象。
requests庫的session會話對象可以跨請求保持某些參數(shù),簡單來說,就是比如你使用session成功的登錄了某個網(wǎng)站,則在再次使用該session對象請求該網(wǎng)站的其他網(wǎng)頁都會默認(rèn)使用該session之前使用的cookie等參數(shù)
import?requests
from?fake_useragent?import?UserAgent
url?=?'https://xueqiu.com'
session?=?requests.Session()
headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random}
?
session.get(url,?headers=headers)
???
#獲取當(dāng)前的Cookie
Cookie=?dict(session.cookies)
基礎(chǔ)參數(shù)
基礎(chǔ)參數(shù)是用于財(cái)務(wù)數(shù)據(jù)請求時原始網(wǎng)址構(gòu)成參數(shù)選擇,我們在可視化操作工具中需要對財(cái)務(wù)數(shù)據(jù)類型進(jìn)行選擇,因此這里需要構(gòu)建財(cái)務(wù)數(shù)據(jù)類型字典。
#原始網(wǎng)址
original_url?=?'https://xueqiu.com'
#財(cái)務(wù)數(shù)據(jù)類型字典
dataType?=?{'全選':'all',
???????????'主要指標(biāo)':'indicator',
???????????'利潤表':'income',
???????????'資產(chǎn)負(fù)債表':'balance',
???????????'現(xiàn)金流量表':'cash_flow'}
獲取獲取各證券市場上市名錄
因?yàn)槲覀冊诳梢暬僮鞴ぞ呱鲜沁x定股票代碼后抓取相關(guān)數(shù)據(jù)并導(dǎo)出,對導(dǎo)出的文件名稱希望是以股票代碼+公司名稱的形式(SH600000 浦發(fā)銀行)存儲,所以我們需要獲取股票代碼及名稱對應(yīng)關(guān)系的字典表。
這其實(shí)就是一個簡單的網(wǎng)絡(luò)爬蟲及數(shù)據(jù)格式調(diào)整的過程,實(shí)現(xiàn)代碼如下:
?1import?requests
?2import?pandas?as?pd
?3import?json
?4from?fake_useragent?import?UserAgent?
?5#請求頭設(shè)置
?6headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random}
?7#股票清單列表地址解析(通過設(shè)置參數(shù)size為9999可以只使用1個靜態(tài)地址,全部股票數(shù)量不足5000)
?8url?=?'https://xueqiu.com/service/v5/stock/screener/quote/list?page=1&size=9999&order=desc&orderby=percent&order_by=percent&market=CN&type=sh_sz'
?9#請求原始數(shù)據(jù)
10response?=?requests.get(url,headers?=?headers)
11#獲取股票列表數(shù)據(jù)
12df?=?response.text
13#數(shù)據(jù)格式轉(zhuǎn)化
14data?=?json.loads(df)
15#獲取所需要的股票代碼及股票名稱數(shù)據(jù)
16data?=?data['data']['list']
17#將數(shù)據(jù)轉(zhuǎn)化為dataframe格式,并進(jìn)行相關(guān)調(diào)整
18data?=?pd.DataFrame(data)
19data?=?data[['symbol','name']]
20data['name']?=?data['symbol']+'?'+data['name']
21data.sort_values(by?=?['symbol'],inplace=True)
22data?=?data.set_index(data['symbol'])['name']
23#將股票列表轉(zhuǎn)化為字典,鍵為股票代碼,值為股票代碼和股票名稱的組合
24ipoCodecn?=?data.to_dict()
A股股票代碼及公司名稱字典如下:

獲取上市公司財(cái)務(wù)數(shù)據(jù)并導(dǎo)出
根據(jù)在可視化操作界面選擇的 財(cái)務(wù)報(bào)告時間區(qū)間、財(cái)務(wù)報(bào)告數(shù)據(jù)類型、所選證券市場類型以及所輸入的股票代碼后,需要先根據(jù)這些參數(shù)組成我們需要進(jìn)行數(shù)據(jù)請求的網(wǎng)址,然后進(jìn)行數(shù)據(jù)請求。
由于請求后的數(shù)據(jù)是json格式,因此可以直接進(jìn)行轉(zhuǎn)化為dataframe類型,然后進(jìn)行導(dǎo)出。在數(shù)據(jù)導(dǎo)出的時候,我們需要判斷該數(shù)據(jù)文件是否存在,如果存在則追加,如果不存在則新建。
獲取上市公司財(cái)務(wù)數(shù)據(jù)
通過選定的參數(shù)生成財(cái)務(wù)數(shù)據(jù)網(wǎng)址,然后根據(jù)是否全選決定后續(xù)數(shù)據(jù)請求的操作,因此可以拆分為獲取數(shù)據(jù)網(wǎng)址和請求詳情數(shù)據(jù)兩部分。
獲取數(shù)據(jù)網(wǎng)址
數(shù)據(jù)網(wǎng)址是根據(jù)證券市場類型、財(cái)務(wù)數(shù)據(jù)類型、股票代碼、單頁數(shù)量及起始時間戳決定,而這些參數(shù)都是通過可視化操作界面進(jìn)行設(shè)置。
證券市場類型 控件 是radioButton,可以通過你 ischecked() 方法判斷是否選中,然后用if-else進(jìn)行參數(shù)設(shè)定;
財(cái)務(wù)數(shù)據(jù)類型 和 股票代碼 因?yàn)橹С?全選,需要先進(jìn)行全選判定(全選條件下是需要循環(huán)獲取數(shù)據(jù)網(wǎng)址,否則是單一獲取即可),因此這部分需要再做拆分;
單頁數(shù)量 考慮到每年有4份財(cái)務(wù)報(bào)告,因此這里默認(rèn)為年份差*4;
時間戳 是 根據(jù)起始時間中的 結(jié)束時間 計(jì)算得出,由于可視化界面輸入的 是 整數(shù)年份,我們可以通過 mktime() 方法獲取時間戳。
?1def?Get_url(self,name,ipo_code):
?2???#獲取開始結(jié)束時間戳(開始和結(jié)束時間手動輸入)
?3???inputstartTime?=?str(self.start_dateEdit.date().toPyDate().year)
?4???inputendTime?=?str(self.end_dateEdit.date().toPyDate().year)
?5???endTime?=?f'{inputendTime}-12-31?00:00:00'
?6???timeArray?=?time.strptime(endTime,?"%Y-%m-%d?%H:%M:%S")
?7
?8???#獲取指定的數(shù)據(jù)類型及股票代碼
?9???filename?=?ipo_code
10???data_type?=dataType[name]
11???#計(jì)算需要采集的數(shù)據(jù)量(一年以四個算)
12???count_num?=?(int(inputendTime)?-?int(inputstartTime)?+1)?*?4
13???start_time?=??f'{int(time.mktime(timeArray))}001'
14
15???#證券市場類型
16???if?(self.radioButtonCN.isChecked()):
17???????ABtype?=?'cn'
18???????num?=?3
19???elif?(self.radioButtonUS.isChecked()):
20???????ABtype?=?'us'
21???????num?=?6
22???elif?(self.radioButtonHK.isChecked()):
23???????ABtype?=?'hk'
24???????num?=?6
25???else:
26???????ABtype?=?'cn'
27???????num?=?3
28
29???#基礎(chǔ)網(wǎng)站
30???base_url?=?f'https://stock.xueqiu.com/v5/stock/finance/{ABtype}'
31
32???#組合url地址
33???url?=?f'{base_url}/{data_type}.json?symbol={ipo_code}&type=all&is_detail=true&count={count_num}×tamp={start_time}'
34
35???return?url,num
請求詳情數(shù)據(jù)
需要根據(jù)用戶輸入決定數(shù)據(jù)采集方式,代碼中主要是根據(jù)用戶輸入做判斷然后再進(jìn)行詳情數(shù)據(jù)請求。
?1#根據(jù)用戶輸入決定數(shù)據(jù)采集方式
?2def?Get_data(self):
?3???#name為財(cái)務(wù)報(bào)告數(shù)據(jù)類型(全選或單個)
?4???name?=?self.Typelist_comboBox.currentText()
?5???#股票代碼(全選或單個)
?6???ipo_code?=?self.lineEditCode.text()
?7???#判斷證券市場類型
?8???if?(self.radioButtonCN.isChecked()):
?9???????ipoCodex=ipoCodecn
10???elif?(self.radioButtonUS.isChecked()):
11???????ipoCodex=ipoCodeus
12???elif?(self.radioButtonHK.isChecked()):
13???????ipoCodex=ipoCodehk
14???else:
15???????ipoCodex=ipoCodecn
16#根據(jù)財(cái)務(wù)報(bào)告數(shù)據(jù)類型和股票代碼類型決定數(shù)據(jù)采集的方式
17???if?name?==?'全選'?and?ipo_code?==?'全選':
18???????for?ipo_code?in?list(ipoCodex.keys()):
19???????????for?name?in?list(dataType.keys())[1:]:
20???????????????self.re_data(name,ipo_code)
21???elif?name?==?'全選'?and?ipo_code?!=?'全選':
22???????????for?name?in?list(dataType.keys())[1:]:
23???????????????self.re_data(name,ipo_code)
24???elif?ipo_code?==?'全選'?and?name?!=?'全選':
25???????for?ipo_code?in?list(ipoCodex.keys()):
26???????????self.re_data(name,ipo_code)????????????
27???else:
28???????self.re_data(name,ipo_code)
29
30#數(shù)據(jù)采集,需要調(diào)用數(shù)據(jù)網(wǎng)址(Get.url(name,ipo_code)????
31def?re_data(self,name,ipo_code):
32???name?=?name
33???#獲取url和num(url為詳情數(shù)據(jù)網(wǎng)址,num是詳情數(shù)據(jù)中根據(jù)不同證券市場類型決定的需要提取的數(shù)據(jù)起始位置)
34???url,num?=?self.Get_url(name,ipo_code)
35???#請求頭
36???headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random}
37???#請求數(shù)據(jù)
38???df?=?requests.get(url,headers?=?headers,cookies?=?cookies)
39
40???df?=?df.text
41try:
42??????data?=?json.loads(df)
43??pd_df?=?pd.DataFrame(data['data']['list'])
44??to_xlsx(num,pd_df)
45???except?KeyError:
46???????log?=?'該股票此類型報(bào)告不存在,請重新選擇股票代碼或數(shù)據(jù)類型'
47???????self.rizhi_textBrowser.append(log)??
財(cái)務(wù)數(shù)據(jù)處理并導(dǎo)出
單純的數(shù)據(jù)導(dǎo)出是比較簡單的操作,直接to_excel() 即可。但是考慮到同一個上市公司的財(cái)務(wù)數(shù)據(jù)類型有四種,我們希望都保存在同一個文件下,且對于同類型的數(shù)據(jù)可能存在分批導(dǎo)出的情況希望能追加。因此,需要進(jìn)行特殊的處理,用pd.ExcelWriter()方法操作。
?1#數(shù)據(jù)處理并導(dǎo)出
?2def?to_xlsx(self,num,data):
?3???pd_df?=?data
?4???#獲取可視化操作界面輸入的導(dǎo)出文件保存文件夾目錄
?5???filepath?=?self.filepath_lineEdit.text()
?6???#獲取文件名
?7???filename?=?ipoCode[ipo_code]??
?8???#組合成文件詳情(地址+文件名+文件類型)
?9???path?=?f'{filepath}\{filename}.xlsx'
10???#獲取原始數(shù)據(jù)列字段
11???cols?=?pd_df.columns.tolist()
12???#創(chuàng)建空dataframe類型用于存儲
13???data?=?pd.DataFrame()????
14???#創(chuàng)建報(bào)告名稱字段????????????
15???data['報(bào)告名稱']?=?pd_df['report_name']
16???#由于不同證券市場類型下各股票財(cái)務(wù)報(bào)告詳情頁數(shù)據(jù)從不同的列才是需要的數(shù)據(jù),因此需要用num作為起點(diǎn)
17???for?i?in?range(num,len(cols)):
18???????col?=?cols[i]
19???????try:
20???????????#每列數(shù)據(jù)中是列表形式,第一個是值,第二個是同比
21???????????data[col]?=?pd_df[col].apply(lambda?x:x[0])
22???????#?data[f'{col}_同比']?=?pd_df[col].apply(lambda?x:x[1])
23???????except?TypeError:
24???????????pass
25???data?=?data.set_index('報(bào)告名稱')??????
26???log?=?f'{filename}的{name}數(shù)據(jù)已經(jīng)爬取成功'
27???self.rizhi_textBrowser.append(log)
28???#由于存儲的數(shù)據(jù)行索引為數(shù)據(jù)指標(biāo),所以需要對采集的數(shù)據(jù)進(jìn)行轉(zhuǎn)T處理
29???dataT?=?data.T
30???dataT.rename(index?=?eval(f'_{name}'),inplace=True)
31???#以下為判斷數(shù)據(jù)報(bào)告文件是否存在,若存在則追加,不存在則重新創(chuàng)建
32???try:
33???????if?os.path.exists(path):
34???????????#讀取文件全部頁簽
35???????????df_dic?=?pd.read_excel(path,None)
36???????????if?name?not?in?list(df_dic.keys()):
37???????????????log?=?f'{filename}的{name}數(shù)據(jù)頁簽不存在,創(chuàng)建新頁簽'
38???????????????self.rizhi_textBrowser.append(log)
39???????????????#追加新的頁簽
40???????????????with?pd.ExcelWriter(path,mode='a')?as?writer:
41???????????????????book?=?load_workbook(path)????
42???????????????????writer.book?=?book????
43???????????????????dataT.to_excel(writer,sheet_name=name)
44???????????????????writer.save()
45???????????else:
46???????????????log?=?f'{filename}的{name}數(shù)據(jù)頁簽已存在,合并中'
47???????????????self.rizhi_textBrowser.append(log)
48???????????????df?=?pd.read_excel(path,sheet_name?=?name,index_col=0)
49???????????????d_?=?list(set(list(dataT.columns))?-?set(list(df.columns)))
50#使用merge()進(jìn)行數(shù)據(jù)合并
51???????????????dataT?=?pd.merge(df,dataT[d_],how='outer',left_index=True,right_index=True)
52???????????????dataT.sort_index(axis=1,ascending=False,inplace=True)
53???????????????#頁簽中追加數(shù)據(jù)不影響其他頁簽
54???????????????with?pd.ExcelWriter(path,engine='openpyxl')?as?writer:??
55???????????????????book?=?load_workbook(path)????
56???????????????????writer.book?=?book
57???????????????????idx?=?writer.book.sheetnames.index(name)
58???????????????????#刪除同名的,然后重新創(chuàng)建一個同名的
59???????????????????writer.book.remove(writer.book.worksheets[idx])
60???????????????????writer.book.create_sheet(name,?idx)
61???????????????????writer.sheets?=?{ws.title:ws?for?ws?in?writer.book.worksheets}????????
62
63???????????????????dataT.to_excel(writer,sheet_name=name,startcol=0)
64???????????????????writer.save()
65???????else:
66???????????dataT.to_excel(path,sheet_name=name)
67
68???????log?=?f'{filename}的{name}數(shù)據(jù)已經(jīng)保存成功'
69???????self.rizhi_textBrowser.append(log)
70
71???except?FileNotFoundError:
72???????log?=?'未設(shè)置存儲目錄或存儲目錄不存在,請重新選擇文件夾'
73???????self.rizhi_textBrowser.append(log)
上面就是制作過程講解與關(guān)鍵代碼,由于源代碼內(nèi)容較多,就不全量展示了,可在后臺回復(fù)“XQ”可獲取完整源代碼文件!
-END-
wen
mo
song
shu
文末推薦一本書Python編程從入門到實(shí)踐(豆瓣評分9.1,最經(jīng)典編程入門教材的"蟒蛇書"第2版,全面修訂升級!本書是針對所有層次 Python 讀者而作的 Python 入門書。
本書內(nèi)容分為“基礎(chǔ)知識”和“項(xiàng)目”兩部分。讀完本書,讀者不僅能快速掌握編程基礎(chǔ)知識,還能編寫出解決實(shí)際問題的代碼并開發(fā)復(fù)雜的項(xiàng)目。聽說圖靈社區(qū)提前解鎖了本書的搶讀版,每周解鎖新章節(jié),原價?130元的紙質(zhì)書+電子書,10月11日前,購買搶讀版僅需78元)

