PyQt5制作一个爬虫小工具,爬取雪球网上市公司的财务数据
本文的文字及圖片來源于網絡,僅供學習、交流使用,不具有任何商業用途,如有問題請及時聯系我們以作處理。
以下文章來源于可以叫我才哥 ,作者:可以叫我才哥
?
最近有朋友需要幫忙寫個爬蟲腳本,爬取雪球網一些上市公司的財務數據。盆友希望可以根據他自己的選擇進行自由的抓取,所以簡單給一份腳本交給盆友,盆友還需要自己搭建python環境,更需要去熟悉一些參數修改的操作,想來也是太麻煩了。
于是,結合之前做過的匯率計算器小工具,我這邊決定使用PyQt5給朋友制作一個爬蟲小工具,方便他的操作可視化。
一、效果演示
?
二、功能說明
- 可以自由選擇證券市場類型:A股、美股和港股
- 可以自由選擇上市公司:單選或全選
- 可以自由選擇財務數據類型:單選或全選(主要指標、利潤表、資產負債表、現金流表)
- 可以導出數據存儲為excel表格文件
- 支持同一家上市公司同類型財務數據追加
三、制作過程
首先引入需要的庫
import?sys from?PyQt5?import?QtCore,?QtGui,?QtWidgets from?PyQt5.QtWidgets?import?QApplication,?QMainWindow,QFileDialogimport?os import?requests from?fake_useragent?import?UserAgent import?json import??logging import?time import?pandas?as?pd from?openpyxl?import?load_workbook雪球網頁拆解
這一步的目的是獲取需要爬取的數據的真正URL地址規律。
當我選中某只股票查看財務數據某類型數據報告時,點擊下一頁,網站地址沒有變化,基本可以知道這是動態加載的數據,對于這類數據可以使用F12打開開發者模式。
?
在開發者模式下,選到Network—>XHR可以查看到真正的數據獲取地址URL及請求方式(General里是請求URL和請求方式說明,Request Headers有請求頭信息,如cookie,Query String Parameters就是可變參數項,一般來說數據源URL就是由基礎URL和這里的可變參數組合而成)
?
我們分析這段URL,可以發現其基本結構如下:
?
基于上述結構,我們拆分最終的組合URL地址如下
#基礎網站 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}'操作界面設計
操作界面設計使用的是PyQt5,這里不做更詳細的介紹,我們在后續中對PyQt5的使用再專題講解。
使用QT designer對操作界面進行可視化設計,參考如下:
?
雪球網數據提取.ui中各個組件的相關設置,參考如下:
?
.ui文件可以使用pyuic5指令進行編譯生成對應的.py文件,或者我們也可以在vscode里直接轉譯(這里也不做更詳細的介紹,具體見后續專題講解)。
本文沒有將操作界面定義文件單獨使用,而是將全部代碼集中在同一個.py文件,因此其轉譯后的代碼備用即可。
獲取cookie及基礎參數
獲取cookie
為了便于小工具拿來即可使用,我們需要自動獲取cookie地址并附加在請求頭中,而不是人為打開網頁在開發者模式下獲取cookie后填入。
自動獲取cookie,這里使用到的requests庫的session會話對象。
requests庫的session會話對象可以跨請求保持某些參數,簡單來說,就是比如你使用session成功的登錄了某個網站,則在再次使用該session對象請求該網站的其他網頁都會默認使用該session之前使用的cookie等參數
import?requests from?fake_useragent?import?UserAgenturl?=?'https://xueqiu.com'session?=?requests.Session() headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random}session.get(url,?headers=headers)#獲取當前的Cookie Cookie=?dict(session.cookies)基礎參數
基礎參數是用于財務數據請求時原始網址構成參數選擇,我們在可視化操作工具中需要對財務數據類型進行選擇,因此這里需要構建財務數據類型字典。
#原始網址 original_url?=?'https://xueqiu.com' #財務數據類型字典 dataType?=?{'全選':'all','主要指標':'indicator','利潤表':'income','資產負債表':'balance','現金流量表':'cash_flow'}獲取獲取各證券市場上市名錄
因為我們在可視化操作工具上是選定股票代碼后抓取相關數據并導出,對導出的文件名稱希望是以股票代碼+公司名稱的形式(SH600000 浦發銀行)存儲,所以我們需要獲取股票代碼及名稱對應關系的字典表。
這其實就是一個簡單的網絡爬蟲及數據格式調整的過程,實現代碼如下:
?1import?requests2import?pandas?as?pd3import?json4from?fake_useragent?import?UserAgent?5#請求頭設置6headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random}7#股票清單列表地址解析(通過設置參數size為9999可以只使用1個靜態地址,全部股票數量不足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#請求原始數據 10response?=?requests.get(url,headers?=?headers) 11#獲取股票列表數據 12df?=?response.text 13#數據格式轉化 14data?=?json.loads(df) 15#獲取所需要的股票代碼及股票名稱數據 16data?=?data['data']['list'] 17#將數據轉化為dataframe格式,并進行相關調整 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#將股票列表轉化為字典,鍵為股票代碼,值為股票代碼和股票名稱的組合 24ipoCodecn?=?data.to_dict()A股股票代碼及公司名稱字典如下:
?
獲取上市公司財務數據并導出
根據在可視化操作界面選擇的 財務報告時間區間、財務報告數據類型、所選證券市場類型以及所輸入的股票代碼后,需要先根據這些參數組成我們需要進行數據請求的網址,然后進行數據請求。
由于請求后的數據是json格式,因此可以直接進行轉化為dataframe類型,然后進行導出。在數據導出的時候,我們需要判斷該數據文件是否存在,如果存在則追加,如果不存在則新建。
獲取上市公司財務數據
通過選定的參數生成財務數據網址,然后根據是否全選決定后續數據請求的操作,因此可以拆分為獲取數據網址和請求詳情數據兩部分。
獲取數據網址
數據網址是根據證券市場類型、財務數據類型、股票代碼、單頁數量及起始時間戳決定,而這些參數都是通過可視化操作界面進行設置。
證券市場類型 控件 是radioButton,可以通過你?ischecked()?方法判斷是否選中,然后用if-else進行參數設定;
財務數據類型 和 股票代碼 因為支持 全選,需要先進行全選判定(全選條件下是需要循環獲取數據網址,否則是單一獲取即可),因此這部分需要再做拆分;
單頁數量 考慮到每年有4份財務報告,因此這里默認為年份差*4;
時間戳 是 根據起始時間中的 結束時間 計算得出,由于可視化界面輸入的 是 整數年份,我們可以通過?mktime()?方法獲取時間戳。
?1def?Get_url(self,name,ipo_code):2???#獲取開始結束時間戳(開始和結束時間手動輸入)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")78???#獲取指定的數據類型及股票代碼9???filename?=?ipo_code 10???data_type?=dataType[name] 11???#計算需要采集的數據量(一年以四個算) 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???#基礎網站 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請求詳情數據
需要根據用戶輸入決定數據采集方式,代碼中主要是根據用戶輸入做判斷然后再進行詳情數據請求。
?1#根據用戶輸入決定數據采集方式2def?Get_data(self):3???#name為財務報告數據類型(全選或單個)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#根據財務報告數據類型和股票代碼類型決定數據采集的方式 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#數據采集,需要調用數據網址(Get.url(name,ipo_code)???? 31def?re_data(self,name,ipo_code): 32???name?=?name 33???#獲取url和num(url為詳情數據網址,num是詳情數據中根據不同證券市場類型決定的需要提取的數據起始位置) 34???url,num?=?self.Get_url(name,ipo_code) 35???#請求頭 36???headers?=?{"User-Agent":?UserAgent(verify_ssl=False).random} 37???#請求數據 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?=?'<font?color=\"#FF0000\">該股票此類型報告不存在,請重新選擇股票代碼或數據類型</font>' 47???????self.rizhi_textBrowser.append(log)??財務數據處理并導出
單純的數據導出是比較簡單的操作,直接to_excel()?即可。但是考慮到同一個上市公司的財務數據類型有四種,我們希望都保存在同一個文件下,且對于同類型的數據可能存在分批導出的情況希望能追加。因此,需要進行特殊的處理,用pd.ExcelWriter()方法操作。
?1 #數據處理并導出2def?to_xlsx(self,num,data):3???pd_df?=?data4???#獲取可視化操作界面輸入的導出文件保存文件夾目錄5???filepath?=?self.filepath_lineEdit.text()6???#獲取文件名7???filename?=?ipoCode[ipo_code]??8???#組合成文件詳情(地址+文件名+文件類型)9???path?=?f'{filepath}\{filename}.xlsx' 10???#獲取原始數據列字段 11???cols?=?pd_df.columns.tolist() 12???#創建空dataframe類型用于存儲 13???data?=?pd.DataFrame()???? 14???#創建報告名稱字段???????????? 15???data['報告名稱']?=?pd_df['report_name'] 16???#由于不同證券市場類型下各股票財務報告詳情頁數據從不同的列才是需要的數據,因此需要用num作為點 17???for?i?in?range(num,len(cols)): 18???????col?=?cols[i] 19???????try: 20???????????#每列數據中是列表形式,第一個是值,第二個是同比 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('報告名稱')?????? 26???log?=?f'{filename}的{name}數據已經爬取成功' 27???self.rizhi_textBrowser.append(log) 28???#由于存儲的數據行索引為數據指標,所以需要對采集的數據進行轉T處理 29???dataT?=?data.T 30???dataT.rename(index?=?eval(f'_{name}'),inplace=True) 31???#以下為判斷數據報告文件是否存在,若存在則追加,不存在則重新創建 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}數據頁簽不存在,創建新頁簽' 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}數據頁簽已存在,合并中' 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()進行數據合并 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???????????????#頁簽中追加數據不影響其他頁簽 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???????????????????#刪除同名的,然后重新創建一個同名的 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'<font?color=\"#00CD00\">{filename}的{name}數據已經保存成功</font>' 69???????self.rizhi_textBrowser.append(log) 70 71???except?FileNotFoundError: 72???????log?=?'<font?color=\"#FF0000\">未設置存儲目錄或存儲目錄不存在,請重新選擇文件夾</font>' 73???????self.rizhi_textBrowser.append(log)總結
以上是生活随笔為你收集整理的PyQt5制作一个爬虫小工具,爬取雪球网上市公司的财务数据的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HEX文件解析C语言源代码
- 下一篇: excel if in函数_【Excel