<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 9693字,需瀏覽 20分鐘

           ·

          2020-09-20 13:13

          最近有盆友需要幫忙寫個爬蟲腳本,爬取雪球網(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元)


          瀏覽 27
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产ts视频 | 黄色视频在线观看18 | 性爱18p | 欧美淫秽视频 | 欧美色一级 |