A股上市公司年报,是我们分析上市公司财务状况和经营状况,并进行股票估值和行情预测的重要依据。那么,我们应该如何爬取这些上市公司的财报数据,并用于数据分析和可视化呢?今天我们以东方财富网为例,来介绍一下基本的步骤。
一、操作环境
1. 浏览器:Safari浏览器(版本:17.31)
2. Python版本:Python3.12
3. 开发环境:Pycharm 2023.3(Community Edition)
4. 操作系统:MacOS 14.3(Sonoma)
二、网页解析
1. 点击如下网址,进入数据页面
2. 页面空白处“点击右键”——“检查元素”,进入开发者工具。
选择”网络“标签页,然后刷新网页,获取所有网页加载项。
3. 对列表中的所有加载项依次进行预览,寻找数据源地址。
以本次操作为例,数据源的网址为(数据源网址是动态生成的):
4. 分析数据源网址,找出真正的数据源接口
一般网址的基本结构为:
协议://域名(IP:端口)/资源路径?查询条件(结构:key1=value1&key2=value2)
据此分析上面的数据源网址,可以发现,真正的数据接口,其实是:
https://datacenter-web.eastmoney.com/api/data/v1/get
但是,要想获取到指定数据,“查询条件”部分也是必不可少的。这个我们后面再讲。
5. 分析数据源网址的代码结构,确定爬取方案
通过“预览”,我们可以发现,数据源网址对应的,其实是一个JSON格式的数据。但这并不是标准的JSON格式数据,还多了一个“jQuery112307880322881038256_1709033016691()”。这个多出来的部分是回调函数,对应数据源网址中的"callback"部分,我们可直接在网址中删除。
三、编写代码
1. 导入工具包
# coding:utf-8
import requests
import pandas as pd
import json
2. 创建DataFrame表格。注意:变量名要与数据对应
list=pd.DataFrame(columns=['股票代码','股票名称','所在行业','股市类型','交易市场','每股收益','每股净收益','营业总收入','营业收入同比增长','净利润','净利润同比增长','每股净资产','净资产收益率','每股经营现金流量','销售毛利率','利润分配','股息率'])
3. 建立循环,以爬取所有页面的数据。循环范围根据数据的页面数量决定。
4. 使用requests爬取数据
(1)确定爬取数据的url网址,如下:
url='https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=UPDATE_DATE%2CSECURITY_CODE&sortTypes=-1%2C-1&pageSize=50&pageNumber='+str(i)+'&reportName=RPT_LICO_FN_CPD&columns=ALL&filter=(REPORTDATE%3D%272022-12-31%27)'
其中”?"之后的”查询条件“部分,分别设为:
sortColumns=UPDATE_DATE%2CSECURITY_CODE #排序变量#
sortTypes=-1%2C-1 #排序格式#
pageSize=50 #每页的数据量,此处默认为50#
pageNumber=str(i) # "i"对应循环数,循环爬取各页数据#
reportName=RPT_LICO_FN_CPD #报告名#
columns=ALL #查询所有变量#
filter=(REPORTDATE%3D%272021-12-31%27) #筛选报告时间,此处默认2022-12-31#
(2)创建表头headers,如下:
header={'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15',
'Cookie': 'st_asi=delete; st_inirUrl=https%3A%2F%2Fbaidu.com%2F; st_psi=20240227192236777-113300301066-4864084221; st_pvi=11934193275781; st_sn=13; st_sp=2024-02-26%2015%3A19%3A30; JSESSIONID=37A0895F7D4E1F2005EF79C5B041FC90; st_si=63431626210633; HAList=ty-1-000001-%u4E0A%u8BC1%u6307%u6570; qgqp_b_id=b06ad5d3eaab94f81f12355ecebb1842'}
cookie可以随意复制一个,不影响最终结果。
(3)爬取网页
money=requests.get(url=url,headers=header)
money.encoding='utf8'
5. 解析爬取的JSON数据
data=json.loads(money.text)
chart=data["result"]['data']
6. 将爬取到的数据依次写入DataFrame表格中
k=0
for j in range(50):
try:
chartj=chart[j]
print(chartj)
code=chartj['SECURITY_CODE']
abbr=chartj['SECURITY_NAME_ABBR']
market=chartj['TRADE_MARKET']
type=chartj['SECURITY_TYPE']
eps=chartj['BASIC_EPS']
epsde=chartj['DEDUCT_BASIC_EPS']
income=chartj['TOTAL_OPERATE_INCOME']
try:
incomegrtht=float(chartj['YSTZ'])/100
except:
incomegrtht = chartj['YSTZ']
profit=chartj['PARENT_NETPROFIT']
try:
profitgrtht=float(chartj['SJLTZ'])/100
except:
profitgrtht = chartj['SJLTZ']
bps=chartj['BPS']
try:
roe=float(chartj['WEIGHTAVG_ROE'])/100
except:
roe=chartj['WEIGHTAVG_ROE']
mgjy=chartj['MGJYXJJE']
try:
xsml=float(chartj['XSMLL'])/100
except:
xsml=chartj['XSMLL']
lrfp=chartj['ASSIGNDSCRPT']
hy=chartj['PUBLISHNAME']
try:
gxl=float(chartj['ZXGXL'])/100
except:
gxl=chartj['ZXGXL']
list.loc[k]=[code,abbr,hy,type,market,eps,epsde,income,incomegrtht,profit,profitgrtht,bps,roe,mgjy,xsml,lrfp,gxl]
k=k+1
except:
continue
其中,k=0需要在整个爬虫代码最前端进行声明。
同时,导入变量数据时,要与开头设定的DataFrame表格的变量顺序一致。
7. 把DataFrame表格保存为excel文件
list.to_excel('list.xlsx',index=False)
四、运行代码
运行代码后,在项目所在目录中会生成表格“list.xlsx”。
代码运行时,一般可见如下警告。它并不会影响最终结果,因此可以不予理会:
FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.
打开该文件,可以看到爬取的数据如下:
最后,附上完整代码
# coding:utf-8
import requests
import pandas as pd
import json
k=0
list=pd.DataFrame(columns=['股票代码','股票名称','所在行业','股市类型','交易市场','每股收益','每股净收益','营业总收入','营业收入同比增长','净利润','净利润同比增长','每股净资产','净资产收益率','每股经营现金流量','销售毛利率','利润分配','股息率'])
for i in range(1,237):
url='https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=UPDATE_DATE%2CSECURITY_CODE&sortTypes=-1%2C-1&pageSize=50&pageNumber='+str(i)+'&reportName=RPT_LICO_FN_CPD&columns=ALL&filter=(REPORTDATE%3D%272022-12-31%27)'
header={'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15',
'Cookie': 'st_asi=delete; st_inirUrl=https%3A%2F%2Fbaidu.com%2F; st_psi=20240227192236777-113300301066-4864084221; st_pvi=11934193275781; st_sn=13; st_sp=2024-02-26%2015%3A19%3A30; JSESSIONID=37A0895F7D4E1F2005EF79C5B041FC90; st_si=63431626210633; HAList=ty-1-000001-%u4E0A%u8BC1%u6307%u6570; qgqp_b_id=b06ad5d3eaab94f81f12355ecebb1842'}
money=requests.get(url=url,headers=header)
money.encoding='utf8'
data=json.loads(money.text)
chart=data["result"]['data']
for j in range(50):
try:
chartj=chart[j]
print(chartj)
code=chartj['SECURITY_CODE']
abbr=chartj['SECURITY_NAME_ABBR']
market=chartj['TRADE_MARKET']
type=chartj['SECURITY_TYPE']
eps=chartj['BASIC_EPS']
epsde=chartj['DEDUCT_BASIC_EPS']
income=chartj['TOTAL_OPERATE_INCOME']
try:
incomegrtht=float(chartj['YSTZ'])/100
except:
incomegrtht = chartj['YSTZ']
profit=chartj['PARENT_NETPROFIT']
try:
profitgrtht=float(chartj['SJLTZ'])/100
except:
profitgrtht = chartj['SJLTZ']
bps=chartj['BPS']
try:
roe=float(chartj['WEIGHTAVG_ROE'])/100
except:
roe=chartj['WEIGHTAVG_ROE']
mgjy=chartj['MGJYXJJE']
try:
xsml=float(chartj['XSMLL'])/100
except:
xsml=chartj['XSMLL']
lrfp=chartj['ASSIGNDSCRPT']
hy=chartj['PUBLISHNAME']
try:
gxl=float(chartj['ZXGXL'])/100
except:
gxl=chartj['ZXGXL']
list.loc[k]=[code,abbr,hy,type,market,eps,epsde,income,incomegrtht,profit,profitgrtht,bps,roe,mgjy,xsml,lrfp,gxl]
k=k+1
except:
continue
list.to_excel('list.xlsx',index=False)