'''1.从网站获取年报链接'''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time
import re
import pandas as pd
import os
Codes=['603183','603357','603458','603698','603776','603859','603860','603909','605167','688315']
def get_table_sse(code): #√
'''
Get HTML source of 年报链接
:param code: 证券代码,上交所上市公司
:type string: str
:return: None
:rtype: None
'''
browser = webdriver.Edge()
url='http://www.sse.com.cn/disclosure/listedinfo/regular/'
browser.get(url)
browser.set_window_size(1550, 830)
time.sleep(3)
browser.find_element(By.ID, "inputCode").click()
browser.find_element(By.ID, "inputCode").send_keys(code) #'603183'
time.sleep(3)
selector='.sse_outerItem:nth-child(4) .filter-option-inner-inner'
browser.find_element(By.CSS_SELECTOR,selector).click()
browser.find_element(By.LINK_TEXT, "年报").click()
time.sleep(3)
#
selector = "body > div.container.sse_content > div > "
selector += "div.col-lg-9.col-xxl-10 > div > "
selector += "div.sse_colContent.js_regular > "
selector += "div.table-responsive > table"
#
element = browser.find_element(By.CSS_SELECTOR,selector)
table_html = element.get_attribute('innerHTML')
#
fname=f'{code}.html'
f = open(fname,'w',encoding='utf-8')
f.write(table_html)
f.close()
#
browser.quit()
def get_table_sse_codes(codes):
for code in codes:
get_table_sse(code)
def get_data(tr):
p_td = re.compile('(.*?)', re.DOTALL)
tds = p_td.findall(tr)
#
s = tds[0].find('>') + 1
e = tds[0].rfind('<')
code = tds[0][s:e]
#
s = tds[1].find('>') + 1
e = tds[1].rfind('<')
name = tds[1][s:e]
#
s = tds[2].find('href="') + 6
e = tds[2].find('.pdf"') + 4
href = 'http://www.sse.com.cn' + tds[2][s:e]
s = tds[2].find('$(this))">') + 10
e = tds[2].find('')
title = tds[2][s:e]
#
date = tds[3].strip()
data = [code,name,href,title,date]
return(data)
def parse_table(fname,save=True): #√
f=open(fname,encoding='utf-8')
html=f.read()
f.close()
#
p = re.compile('(.+?) ', re.DOTALL)
trs = p.findall(html)
#
trs_new = []
for tr in trs:
if tr.strip() != '':
trs_new.append(tr)
#
data_all = [get_data(tr) for tr in trs_new[1:]]
df = pd.DataFrame({
'code': [d[0] for d in data_all],
'name': [d[1] for d in data_all],
'href': [d[2] for d in data_all],
'title': [d[3] for d in data_all],
'date': [d[4] for d in data_all]
})
#
if save:
df.to_csv(f'{fname[0:-5]}.csv')
return(df)
get_table_sse_codes(Codes)
html_list=os.listdir()
html_list=[i for i in html_list if i.endswith('.html')]
df1=parse_table(html_list[0])
df2=parse_table(html_list[1])
df3=parse_table(html_list[2])
df4=parse_table(html_list[3])
df5=parse_table(html_list[4])
df6=parse_table(html_list[5])
df7=parse_table(html_list[6])
df8=parse_table(html_list[7])
df9=parse_table(html_list[8])
df10=parse_table(html_list[9])
'''2.过滤掉一些不重要的的公告链接'''
import time
def filter_links(words,df,include=True):
'''筛选保留年报链接
:param words:保留或剔除包含关键词的列表
:param df:DataFrame
:param include:keep or ellipsisxclude
'''
ls=[]
for word in words:
if include:
ls.append([word in f for f in df['title']])
else:
ls.append([word not in f for f in df['title']])
index=[]
for r in range(len(df)):
flag = not include
for c in range(len(words)):
if include:
flag = flag or ls[c][r]
else:
flag = flag and ls[c][r]
index.append(flag)
df2 = df[index]
return(df2)
def filter_date(start,end,df):
date = df['date']
v = [d>=start and d<= end for d in date]
df_new = df[v]
return(df_new)
import datetime
def start_end_10y():
dt_now = datetime.datetime.now()
current_year = dt_now.year
start = f'{current_year-9}-01-01'
end = f'{current_year}-12-31'
return((start,end))
def filter_nb_10y(df,
keep_words=['年报','年度报告'],
exclude_words=['摘要','修订稿','持续督导'],
start=''):
if start == '':
start,end = start_end_10y()
else:
start_y = int(start[0:4])
end = f'{start_y + 9}-12-31'
#
df = filter_links(keep_words, df,include=True)
df = filter_links(exclude_words, df,include=False)
df = filter_date(start,end,df)
return(df)
df_all=[df1,df2,df3,df4,df5,df6,df7,df8,df9,df10]
df_all_n=[]
for i in df_all:
df_all_n.append(filter_nb_10y(i,
keep_words=['年报','年度报告'],
exclude_words=['摘要','修订稿','持续督导'],
start=''))
'''3.下载年报'''
import requests
def download_pdf(href, code, year):
"""
下载单份年报,自动命名保存
herf: download link address,
typestring:str
code:证券代码,
year:年报年份,
string: str
return:None
rtype:None
"""
r = requests.get(href,allow_redirects=True)
fname = f'{code}_{year}.pdf'
f = open(fname,'wb')
f.write(r.content)
f.close
#
r.close
def download_pdfs(hrefs,code,years):
for i in range(len(hrefs)):
href = hrefs[i]
year = years[i]
download_pdf(href,code,year)
time.sleep(30)
return()
def download_pdfs_codes(list_hrefs,codes,list_years):
for i in range(len(list_hrefs)):
hrefs = list_hrefs[i]
years = list_years[i]
code = codes[i]
download_pdfs(hrefs, code, years)
return()
hrefs=[]
for i in range(10):
hrefs.append(list(df_all_n[i]['href']))
years=[]
for i in range(10):
years.append(list(df_all_n[i]['date']))
download_pdfs_codes(hrefs,Codes,years)
'''4.解析年报'''
import fitz
import pandas as pd
def get_csv(doc,bounds=('联系人和联系方式','四、 信息披露及备置地点')):
start_pageno = 0
end_pageno = len(doc) - 1
lb,ub=bounds
for n in range(len(doc)):
page = doc[n]
txt = page.get_text()
if lb in txt:
start_pageno = n; break
for n in range(start_pageno,len(doc)):
if ub in doc[n].get_text():
end_pageno = n+1; break
txt1 = ''
for n in range(start_pageno,end_pageno):
page = doc[n]
txt1 += page.get_text()
return(txt1)
#提取基本信息CSV
import numpy as np
na=[np.nan]*len(Codes)
csv2=pd.DataFrame(data={'公司股票代码':Codes,
'公司简称':na,
'公司网址':na,
'公司邮箱':na,
'办公地址':na,
'董秘姓名':na,
'董秘电话':na})
col=csv2.columns
p_list=['公司的中文简称.*?\n(.*?)\n',
'公司网址.*?\n(.*?)\n',
'电子信箱.*?电子信箱.*?\n(.*?)\n',
'公司办公地址.*?\n(.*?)\n',
'姓名.*?\n(.*?)\n',
'电话.*?\n(.*?)\n']
file_list=os.listdir()
file_list=[i for i in file_list if i.endswith('.pdf') and '2023' in i]
for n in range(len(file_list)):
# n=4
filename = file_list[n]
doc = fitz.open(filename)
csv1=get_csv(doc)
i=1
for p in p_list:
try:
p1 = re.compile(p,re.DOTALL)
# p1 = re.compile(p_list[0],re.DOTALL)
IR = p1.findall(csv1)[0]
# IR
csv2.loc[n,col[i]]=IR
# csv2.loc[4,col[1]]='利柏特'
i+=1
except:
i+=1
csv2.to_csv('公司基本信息.csv')
# import pdfplumber
def get_rev(doc,bounds=('主要会计数据和财务指标','主要财务指标')):
start_pageno = 0
end_pageno = len(doc) - 1
lb,ub=bounds
for n in range(len(doc)):
page = doc[n]
txt = page.get_text()
if lb in txt:
start_pageno = n; break
for n in range(start_pageno,len(doc)):
if ub in doc[n].get_text():
end_pageno = n+1; break
txt1 = ''
for n in range(start_pageno,end_pageno):
page = doc[n]
txt1 += page.get_text()
return(txt1)
def get_netp(doc,bounds=('利润分配情况','销售退回')):
start_pageno = 0
end_pageno = len(doc) - 1
lb,ub=bounds
for n in range(len(doc)):
page = doc[n]
txt = page.get_text()
if lb in txt:
start_pageno = n; break
for n in range(start_pageno,len(doc)):
if ub in doc[n].get_text():
end_pageno = n+1; break
txt1 = ''
for n in range(start_pageno,end_pageno):
page = doc[n]
txt1 += page.get_text()
return(txt1)
csv_rev=pd.DataFrame(data={'公司股票代码':Codes})
csv_netp=pd.DataFrame(data={'公司股票代码':Codes})
d_list=['营业收入.*?\n(.*?)\s',
'拟分配的利润或股利.*?\n(.*?)\s']
for n in range(len(Codes)):
# n=0
file_list=os.listdir()
file_list=[i for i in file_list if i.endswith('.pdf') and Codes[n] in i]
for f in range(len(file_list)):
# f=0
try:
doc = fitz.open(file_list[f])
txt11=get_rev(doc)
p1 = re.compile(d_list[0],re.DOTALL)
r = p1.findall(txt11)[0]
r=r.replace(',','')
r=round(float(r)/(10**8),2)
csv_rev.loc[n,file_list[f][7:11]]=r
except:
pass
try:
txt12=get_netp(doc)
p2 = re.compile(d_list[1],re.DOTALL)
netp = p2.findall(txt12)[0]
netp=netp.replace(',','')
netp=round(float(netp)/(10**8),2)
csv_netp.loc[n,file_list[f][7:11]]=netp
except:
pass
#数据整理
csv_rev=csv_rev.set_index(['公司股票代码'])
csv_rev=csv_rev.T
csv_rev = csv_rev.sort_index(ascending=True)
csv_netp=csv_netp.set_index(['公司股票代码'])
csv_netp=csv_netp.T
csv_netp = csv_netp.sort_index(ascending=True)
# csv_rev=csv_rev.apply(pd.to_numeric,errors='ignore')
# csv_netp=csv_netp.apply(pd.to_numeric,errors='ignore')
csv_rev=csv_rev.fillna(0)
csv_netp=csv_netp.fillna(0)
index1=[ int(i)-1 for i in csv_rev.index]
csv_rev['index1']=index1
csv_rev=csv_rev.set_index(['index1'])
index2=[ int(i)-1 for i in csv_netp.index]
csv_netp['index1']=index2
csv_netp=csv_netp.set_index(['index1'])
csv_rev.to_csv('公司营业收入.csv')
csv_netp.to_csv('公司利润分配.csv')
#输出图表
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #确保显示中文
plt.rcParams['axes.unicode_minus'] = False #确保显示负数的参数设置
#输出营业收入图
for i in range(len(csv_rev.columns)):
plt.figure()
plt.plot(csv_rev.index,csv_rev.loc[:,csv_rev.columns[i]],label=u'年营业收入',color='b')
for x,y in zip(csv_rev.index,csv_rev.loc[:,csv_rev.columns[i]]):#显示bar数值
plt.text(x,y,'%.2f'%y,ha='center',va='bottom')
plt.xlabel(u'(年)',fontsize=13)
plt.ylabel(u'年营业收入(亿元)',fontsize=13,rotation=90)
plt.legend(loc='best')
plt.title(u'%s.SH %s-%s年营业收入的可视化'%(str(csv_rev.columns[i]),str(csv_rev.index[0]),str(csv_rev.index[-1])),fontsize=13)
# plt.yticks(range(0,3,30))
plt.savefig( '%s.SH营业收入.jpg'%csv_rev.columns[i])
plt.show()
#输出利润分配图
for i in range(len(csv_netp.columns)):
plt.figure()
plt.plot(csv_netp.index,csv_netp.loc[:,csv_netp.columns[i]],label=u'年利润分配',color='r')
for x,y in zip(csv_netp.index,csv_netp.loc[:,csv_netp.columns[i]]):#显示bar数值
plt.text(x,y,'%.2f'%y,ha='center',va='bottom')
plt.xlabel(u'(年)',fontsize=13)
plt.ylabel(u'年利润分配(亿元)',fontsize=13,rotation=90)
plt.legend(loc='best')
plt.title(u'%s.SH %s-%s年利润分配的可视化'%(str(csv_netp.columns[i]),str(csv_rev.index[0]),str(csv_rev.index[-1])),fontsize=13)
# plt.yticks(range(0,3,30))
plt.savefig( '%s.SH利润分配.png'%csv_netp.columns[i])
plt.show()
#输出10家公司年营业收入
for i in range(len(csv_rev.index)):
plt.figure()
#colors=['#FF8247','33FF66','g','b','y','r','EEAEEE','dda0dd','BBFFFF','63B8FF']
y=csv_rev.iloc[i,:]
y=y.sort_values()
x=csv_rev.columns
plt.bar(x=x,height=y,label=u'年营业收入',color='#BBFFFF')
for x,y in zip(x,y):#显示bar数值
plt.text(x,y,'%.2f'%y,ha='center',va='bottom')
plt.xlabel(u'(股票代码)',fontsize=13)
plt.ylabel(u'年营业收入(亿元)',fontsize=13,rotation=90)
plt.legend(loc='best')
plt.title(u'%s年营业收入的可视化'%str(csv_rev.index[i]),fontsize=13)
# plt.yticks(range(0,3,30))
plt.xticks(rotation=45)
plt.savefig( '%s年营业收入.png'%csv_rev.index[i])
plt.show()
#输出10家公司营业收入
fig=plt.figure()
fig,axs=plt.subplots(constrained_layout=True)
ax1=plt.subplot(211)
x=csv_rev.index
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[0]],label=csv_rev.columns[0],color='b')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[1]],label=csv_rev.columns[1],color='g')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[2]],label=csv_rev.columns[2],color='y')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[3]],label=csv_rev.columns[3],color='r')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[4]],label=csv_rev.columns[4],color='#FF8247')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[5]],label=csv_rev.columns[5],color='#EE82EE')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[6]],label=csv_rev.columns[6],color='#9ACD32')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[7]],label=csv_rev.columns[7],color='#006400')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[8]],label=csv_rev.columns[8],color='#1E90FF')
ax1.plot(x,csv_rev.loc[:,csv_rev.columns[9]],label=csv_rev.columns[9],color='#9400D3')
ax1.set_ylabel('年营业收入(亿元)')
ax1.set_xlabel('年份')
ax1.set_title('十家公司年营业收入图')
ax1.legend(bbox_to_anchor=(0.5,-0.5),ncol=2)
plt.yticks(rotation=90)
plt.rcParams['figure.figsize']=(7, 7)
# ax1.grid(True,axis='both')
plt.savefig( '10家公司营业收入.png')
plt.show()
根据所绘制的专业技术服务业十家上市公司营业收入图来看,目前勘设股份(股票代码603458)、 航天工程(603698)和设计总院(603357)表现最为突出,是前三名。
勘设股份2017年8月9日在上海证券交易所整体上市,并首次公开发行A股。公司主营业务为公路行业工程咨询与工程承包业务,能够承接各种工程建设领域的全过程咨询业务;拥有规划编制、工程监理、施工总承包、检测试验、地质灾害治理等多项资质,业务涵盖公路、市政、建筑、水运、生态环保等多行业的全产业链,能为基础设施建设提供全过程解决方案和服务。
勘设股份在2017年上市当年营业收入便一直在十家公司中遥遥领先,2017-2022年的营业收入分别为19.27、21.52、25.62、27.98、30.39、21.56(单位:亿元,下同),但是在2022年营业收入却出现了较大幅度的减少,排名掉至第三。通过查询当年的新闻得知主要原因是公司工程承包收入下降,另外,在2022年该公司曾经有过一次定增行为,其更换的持股股东名下12家公司可能存在同业竞争情况,所以投资者在投资过程中应当对其保持谨慎的态度。
航天长征化学工程股份有限公司成立于2007年6月,隶属于中国航天科技集团公司中国运载火箭技术研究院,专业从事煤气化技术及关键设备研发、工程设计、技术服务、设备成套供应及工程总承包。在近十年数据中,该公司营业收入除2015-2017年有短暂、小幅的下滑趋势外,大体上表现出稳步增长的趋势,现已在十家公司中排名第二。
安徽省交通规划设计研究总院股份有限公司是一家综合性勘察设计单位,始建于1960年,是安徽省交通控股集团有限公司国有控股上市企业。公司拥有工程勘察综合、工程设计综合、城乡规划、工程咨询、公路及水运工程检测和监理等多项甲级资质,以及市政公用工程、公路工程、机电工程施工总承包一级资质,是通过质量/环境/职业健康三证一体认证的高新技术企业。
由图中可以看出自上市以来,该公司的营业收入基本呈现出稳中向好的态势,并且在2022年已经处于十家公司中的领先地位,并且营业收入达到了27.98亿元。
诺禾致源(688315)、利柏特(605167)两家公司则是处于十家公司中的第二梯队,因为两家公司都是上市不久,所以可能后续潜力会比较大,投资者可以对其持观望态度。
剩下未提及的其余五家则一直表现平平,没有亮眼之处。
营业收入可以看出一个公司的规模,而公司经营赚取的可以用来分配给股东的净利润是指在利润总额中按规定缴纳所得税以后并且弥补完亏损以及提取盈余公积以后,公司的利润留存部分,是衡量一个企业经营效率的主要指标,但需要注意的是,公司所分配的净利润是由公司自己决定的,公司有权利不将净利润分配给股东,而选择将其投入再投资,用提升公司股价的方式使投资者获益。
根据所分配净利润随时间趋势变化图,可以观察到专业技术服务业的公司向股东分配的净利润都较少,只有设计总院(603357)、勘设股份(603458)、中公高科(6038600)和诺禾致源(688315)自上市以来坚持向股东分配净利润,但是分配的数量也并不客观,例如诺禾致源,其2021年、2022年的营业收入分别为18.66,19.26,呈上升趋势,但其这两年向股东分配的净利润分别为0.24,0.19,呈下降趋势,可能的原因有:
(1)该公司虽然营业收入增加,但随着生产规模的扩大,生产成本也有所增加,所以公司能够向股东分配的净利润也减少了;
(2)公司的分配策略进行了改变,由向股东分配净利润变为将获得的净利润变成留存收益,用于公司的未来发展。
根据目前较为主流的分配策略来看,第二种可能性更大,但并不排除第一种或二者皆有的可能性。
所以根据对于上市公司所分配给股东的净利润的分析我们可以看出,投资者在分析上市公司年报时,并不能单纯关注某一个或某几个指标,应当理解不同指标间的联系,将各个指标综合分析,同时应当关注与目标公司相关的重大事项,及时改变投资策略以规避潜在的风险。
这门课的很多代码老师上课已经讲解过了,更多地是需要耐心去做。写代码的过程中经常会遇到报错,写报告的过程中可能经常会感到心累,但就是两个字——“耐心”,耐心地去“百度”,耐心地“求助”,耐心地坚持下去,相信你一定能成功!
另外,很感谢吴老师开了这门课,使我受益匪浅,希望老师的网站越办越好!