林黄奕的实验报告

目录

一、数据展示及可视化(点击跳转)

二、实验结果解读(点击跳转)

三、网页爬取——下载年报(点击跳转)

四、PDF文件爬取——提取数据(点击跳转)

五、附录(点击跳转)

(一)项目源代码介绍(点击跳转)

(二)实验中所遇问题(点击跳转)

(三)实验心得(点击跳转)

PART ONE

数据展示及可视化

(一)年报展示

上海证券交易所深圳证券交易所爬取共40家批发业上市公司的近十年(2013-2022)400份年度报告。将爬取所得的年报按证券代码存放进对应文件夹中,文件夹截图如下所示。

图1 年报文件夹

年报文件夹截图

可以看到共有40个文件夹,其中存储有该证券代码所对应的上市公司近十年的年度报告。

图2 年度报告

在上述文件夹中存储的年报如下所示。

年报截图

图3 年报数据

从爬取到的年报中提取的数据如下所示。

年报数据(部分)

提取的信息可以分为三类,一是公司基本信息,而是公司董事会秘书信息,三是企业相关财务指标。具体字段含义如下所示。

  1. code: 证券代码; year:年报所属年份; name:公司名称; location:办公地址; web:公司网址;
  2. secretary_name:董事会秘书姓名; secretary_tel:董事会秘书电话; secretary_email:董事会秘书邮箱;
  3. Revenue:营业收入; Net_profit:归属于上市公司股东的净利润; Earnings_per_share:每股收益

(二)绘图结果

本节内容主要是对对营业收入(亿元)、归属于上市公司股东的净利润(亿元)和每股收益(元)进行可视化分析。计算各上市公司2022年相较于2013年的变化程度,发现个别公司在近十年发展极快(东方银星的变化幅度甚至达到170倍)。

我认为近十年发展较快的公司具有特殊性,可能无法很好地代表行业的发展水平,所以剔除十年内营业收入变动过大的公司,选2022年营业收入排名前十的上市公司作为行业代表对相关指标进行可视化,并据此分析行业发展状况。选取所得的公司代码及简称如下所示。

    600153:建发股份 600755:厦门国贸 000701:厦门信达 600058:五矿发展 000829:天音控股
    000028:国药一致 600981:汇鸿集团 600511:国药股份 600278:东方创业 600056:中国医药

年报数据处理及绘图代码在此处不赘述,此处仅放上根据指标绘图所得的折线图、双坐标图(折线图+柱状图)、横向条形图、饼图以及堆叠条形图,具体代码请点击链接查看网页。

1 折线图

图4 批发业十家上市公司营业收入(亿元)时间序列图(合并)

回到实验结果解读
营业收入折线图(合并)

图5 批发业十家上市公司营业收入(亿元)时间序列图(分开)

回到实验结果解读
营业收入折线图(单独)

图6 批发业十家上市公司归属于上市公司股东的净利润(亿元)时间序列图(合并)

回到实验结果解读
净利润折线图(合并)

图7 批发业十家上市公司归属于上市公司股东的净利润(亿元)时间序列图(分开)

回到实验结果解读
净利润折线图(单独)

图8 批发业十家上市公司每股收益(元)时间序列图(合并)

回到实验结果解读
每股收益折线图(合并)

图9 批发业十家上市公司每股收益(元)时间序列图(分开)

回到实验结果解读
每股收益折线图(单独)

2 双坐标图——柱状图+折线图

图10 批发业十家上市公司营业收入(亿元)双坐标图

回到实验结果解读
营业收入双坐标图

图11 批发业十家上市公司归属于上市公司股东的净利润(亿元)双坐标图

回到实验结果解读
净利润双坐标图

图12 批发业十家上市公司每股收益(元)双坐标图

回到实验结果解读
每股收益双坐标图

3 横向条形图

图13 批发业十家上市公司营业收入(亿元)对比图

回到实验结果解读
营业收入横向条形图

图14 批发业十家上市公司归属于上市公司股东的净利润(亿元)对比图

回到实验结果解读
净利润横向条形图

图15 批发业十家上市公司每股收益(元)对比图

回到实验结果解读
每股收益横向条形图

4 饼图

图16 批发业十家上市公司营业收入占比

回到实验结果解读
营业收入占比

5 营业收入堆叠条形图

图17 行业(批发业)表现——营业收入及增速

回到实验结果解读
行业营业收入及增速

PART TWO

实验结果解读

上部分绘制的图均是根据营业收入、归属于上市公司股东的净利润以及每股收益指标及其衍生指标绘制而成的,故接下来分别从不同指标切入分别进行解读。在下文中,点击蓝色图序号即可跳转至上文所对应的图。

(一)营业收入解读

从合并绘制的营业收入折线图图4中可以看到,建发股份(600153)和厦门国贸(600755)这两家公司近十年营业收入增长较快,远远领先于其他公司。五矿发展(600058)的营业收入在2013和2014年领先于其他公司,但近十年有较明显的先下降后缓慢上升的趋势,五矿发展(600058)已失去领先地位。

除五矿发展(600058)外,其余公司近十年均有一定幅度的增长,但由于纵坐标刻度的原因,即不同公司的衡量标准相同而不能很好地观察到其余公司近十年营业收入的变化情况,故分别绘制每家公司的营业收入时间序列图,如图5所示。可以看到,除五矿发展(600058)外,其余公司近十年营业收入均呈上升趋势。

此外,本实验还将营业收入及其增速整合在一张图中,即绘制营业收入双坐标图,如图10所示。左右坐标轴分别代表营业收入(亿元)和营业收入增速,柱状图代表营业收入,右坐标轴是营业收入增速,而黑色虚线代表营业收入的增速为0。从图中可知,除五矿发展(600058)外,其他公司近十年间柱状图呈现上升趋势,蓝色折线除个别年份,大多都在黑色虚线上方,意味着代表公司近十年的增速大多为正,营业收入逐年攀升,行业发展态势较好。其中,2022年,建发股份(600153)实现营业收入8,328.12亿元,同比增长17.7%,厦门国贸(600755)实现营业收入5,219.18亿元,同比增长12.3%。而五矿发展(600058)的营业收入呈现先大幅下降而后缓慢上升的趋势。

本实验还绘制了10家公司在近十年的每年营业收入对比情况以及占比情况,如图13图16所示。从图13中可以明显看出在2013和2014年,五矿发展(600058)的营业收入领先于其他公司而在接下来的年份中逐渐落后,建发股份(600153)和厦门国贸(600755)后来居上。若以营业收入简单地代表企业在批发业中占据的市场份额,同样可以看到,在2013年,五矿发展(600058)的营业收入占据到10家公司总营业收入的42.47%,接近一半,在2014年仍旧十家公司的榜首,但自2015年,除2017年较之2016年有小幅度的上升外,五矿发展(600058)的市场份额逐年下降,与上文中对营业收入折线图、双坐标图以及横向条形图分析的结果一致。建发股份(600153)和厦门国贸(600755)在十年的发展中占据越来越大的市场份额,其中,建发股份(600153)居于首位,厦门国贸(600755)次之,其余公司的营业收入占比相差不大。

(二)归属于上市公司股东的净利润解读

从合并绘制的净利润折线图图6中可以明显看到,建发股份(600153)和厦门国贸(600755)这两家公司近十年归属于上市公司股东的净利润(下面简称净利润)增长较快,而五矿发展(600058)和厦门信达(000701)的净利润在2015年、2019年亏损39.53亿和24.93亿,这一数据已与对应年报相比对,并不是由于解析pdf错误所致的异常数据。

从分别绘制的净利润折线图图7中可以较为清晰地观察到十家公司近十年净利润的变化情况,除五矿发展(600058)、厦门信达(000701)、天音控股(000829)以及汇鸿集团(600981)外,其余公司近十年的净利润均为正数,大都呈现上升趋势。

图14中可以明显看到,建发股份(600153)近十年净利润居于首位,遥遥领先于其他公司,而厦门国贸(600755)除个别年份(2015年和2016年)外均仅次于建发股份(600153),位居第二。2022年,建发股份(600153)的净利润为62.82亿元,同比增长2.30%,厦门国贸(600755)的净利润为35.89亿元,全年同比增长4.41%,两家公司业绩再创新高。而结合上述对营业收入的分析可知,五矿发展(600058)的营业收入虽然在2013年和2014年居于首位,但是净利润在行业内并不具有优势。

图11绘制的净利润双坐标图中的柱状图会比折线图更清晰地呈现不同公司在不同年份是否出现亏损。可以看到,五矿发展(600058)、厦门信达(000701)、天音控股(000829)以及汇鸿集团(600981)近些年来的净利润较不稳定,甚至出现亏损现象,而建发股份(600153)的盈利能力较强,在2014-2018年净利润增速逐年增加,发展迅猛,而后增速放缓,厦门国贸(600755)、国药一致(000028)以及国药股份(600511)出现相似情况,均呈现净利润增速先上升后下降的趋势。近十年十家公司每股收益变化与净利润变化相似,在此就不过多赘述。

(三)个例分析

从上述分析可知五矿发展(600058)的表现较为“突出”,作为国内黑色金属流通领域最大的综合服务商,五矿发展近些年来的业绩极其不稳定,在2015年甚至亏损了近39.53亿,进一步查阅资料后简单总结了致使其亏损的两大因素。其一,钢材、冶金原材料价格波动,使得该公司部分存货出现减值;其二,公司负债中美元负债占一定规模,受2015年人民币对美元大幅贬值影响,汇兑损失大幅增加。

(四)总结

以所选取的十家公司为行业代表,绘制营业收入堆叠条形图和总营业收入增速双坐标图来呈现批发业近十年的发展状况,如图17所示。从柱状图中可以看到,批发业营业收入即销售额年年攀升,但增长速度开始减缓。

对制约批发业发展的原因进行分析:首先,可能是因为批发业已达到了一个饱和点,缺少新的经济增长点。其次,由于批发业通常涉及大量的原材料采购和存货管理,原材料的供应和价格波动会直接影响企业的成本和利润。此外,汇率的波动导致企业所具有的资产和负债的价值波动的同时也影响到了其成本和销售收入。如果本国货币贬值,企业的进口成本会增加,而出口收入可能会增加。相反,如果本国货币升值,企业的进口成本会减少,但出口收入也可能会减少。而当企业从其他国家采购商品时,如果本国货币贬值,可能会面临采购成本上涨的风险。

对建发股份、厦门国贸等行业龙头企业成功案例进行学习后总结出以下几点建议。首先,批发行业企业要加强科技赋能,推动互联互通,加速数字化建设。在数字经济时代下,批发行业可以通过运用物联网、大数据、人工智能等新兴技术,推动供应链运营业务实现线上化、数字化、移动化和可视化升级,持续拓展数字化产品矩阵,不断拓宽信息服务场景,强化与上下游客商的数据对接与业务协同,赋能管理降本增效。其次,批发业企业可以加强金融支持,面对持续波动的大宗商品行情,企业可以灵活运用期货、期权、掉期、远期等衍生品工具进行对冲套保,来降低大宗商品价格波动风险,也可通过创新模式来提高服务附加价值。此外,企业可以不断加强与国内外大型物流供应商合作,逐渐完善国内和国际物流仓储服务网络,链接供应链上下游客户及物流生态圈伙伴,提高物流过程数字化管理能力

PART THREE

网页爬取——下载年报

进行年报爬取部分的工作,首先,导入相关第三方库和自定义库。

      
        # =================导入第三方库和自定义库=================
        import sys
        sys.path.append(r'D:\0AAAAAAAAAAAAAAAAAsmalldeskbook\Financial Data Acquisition and Processing\annual_report\src\annual_report')

        import os
        import re
        import time
        import glob
        import requests
        import numpy as np
        import pandas as pd
        from ip import acquire_ip
        import dataframe_image as dfi
        from get_code import get_page,change_code
        from download_html import get_html_sz,get_html_sh,get_mul_html_sh,get_mul_html_sz
        from html_process import filter_mul,html_info_sh,html_info_sz
        from pdf_process import txt,get_Net,get_sale,get_profit,get_pro_sal_s,get_mul_info,get_mul_info_secretary,get_mul_pro_sal

        os.chdir(r'D:\0AAAAAAAAAAAAAAAAAsmalldeskbook\Financial Data Acquisition and Processing\annual_report\data')

(一)获得属于批发业的上市公司代码

        
        # =================获得属于批发业的上市公司代码=================
        # 将行业分类的pdf下载到本地后,用定义的get_page函数提取pdf文件中的表格
        df0  = get_page("行业分类.pdf",77)  #获取第78页 
        df1 = get_page("行业分类.pdf",78) #获取第79页 
        df2 = get_page("行业分类.pdf",79) #获取第80页
        df = pd.concat([df0,df1,df2],axis = 0,ignore_index = True)
        
        # 处理获取到的上市公司数据
        df = df.fillna(method='ffill')
        df = df[df['行业大类名称']=='批发业']
        df = df.reset_index(drop=True)
        df = df[['行业大类名称','上市公司代码','上市公司简称']]
        
        # 提取所需公司代码,按其所属交易所(上交所/深交所)分别保存为excel文件
        df.上市公司代码 = list(map(change_code,df.上市公司代码))
        sz_info = df[~df.上市公司代码.apply(lambda x: x[0] == '6')]
        sh_info = df[df.上市公司代码.apply(lambda x: x[0] == '6')]
        sz_info = sz_info.reset_index(drop=True)
        sz_info.to_excel('sz_info.xlsx')
        sh_info = sh_info.reset_index(drop=True)
        sh_info.to_excel('sh_info.xlsx')
        

      
    

(二)对批发业上市公司年报页面进行爬取,并将其存储为html格式文件

        
        # =================对批发业上市公司年报页面进行爬取,并将其存储为html格式文件=================

        # 爬取上交所上市公司年报页面,以html格式文件进行存储
        sh_info = pd.read_excel('sh_info.xlsx',index_col=0)
        # 或 get_mul_html_sh(sh_info.上市公司代码.tolist())
        # 成功下载则返回1,未成功下载则返回nan值
        sh_info_return = sh_info.上市公司代码.apply(lambda x:get_html_sh(x))
        
        
        # 若要爬取的上市公司过多,可能报错:
        # TimeoutException: 由于目标计算机积极拒绝,无法连接。 (os error 10061)
        
        # 上次成功爬取600751后报错
        before_code = sh_info.上市公司代码.tolist().index(600751)
        sh_info_return_1 = sh_info.上市公司代码[before_code+1:].apply(lambda x:get_html_sh(x,'http://183.165.224.4:8089'))
        # 上次成功爬取600981后报错
        before_code = sh_info.上市公司代码.tolist().index(600981)
        sh_info_return_2 = sh_info.上市公司代码[before_code+1:].apply(lambda x:get_html_sh(x))
        
        # 可将上市公司代码列表拆分,分别使用不同的代理ip进行爬取
        # ip_lst为不同的代理ip存放列表
        
        # 获取代理ip列表
        ip_lst = acquire_ip()
        sh_info_lst = [sh_info.上市公司代码.tolist()[i:i + 10] for i in range(0, len(sh_info), 10)]
        for i,j in enumerate(sh_info_lst):
            get_mul_html_sh(j,ip_lst[i])
            time.sleep(10)
        
        
        # 爬取深交所上市公司年报页面,以html格式文件进行存储
        sz_info = pd.read_excel('sz_info.xlsx',index_col=0)
        
        # 或 get_mul_html_sz(sz_info.上市公司代码.tolist())
        sz_info_return = sz_info.上市公司代码.apply(lambda x:get_html_sz(x))
        

      
    

(三)使用正则表达式从爬取的页面中提取公司的代码、简称、年报名称以及年报的pdf下载链接

        
        # =================使用正则表达式从爬取的页面中提取公司的代码、简称、年报名称以及年报的pdf下载链接=================

        # sh_info.xlsx和sz_info.xlsx是依据行业分类所得的批发业上市公司信息,用于与爬取到的html中所含信息进行比对
        sh_info = pd.read_excel('sh_info.xlsx',index_col=0)
        sz_info = pd.read_excel('sz_info.xlsx',index_col=0)
        
        sh_info.上市公司代码 = sh_info.上市公司代码.apply(lambda x:change_code(x))
        sz_info.上市公司代码 = sz_info.上市公司代码.apply(lambda x:change_code(x))
        
        
        # 从html中提取有效信息——上交所
        # 读取html
        f = open('上交所年报.html',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)
        
        remove_row1 = '证券代码证券简称公告标题公告时间'
        remove_row2 = '证券代码证券简称公告标题公告时间'
        
        while remove_row1 in trs_new:
            trs_new.remove(remove_row1)
        
        while remove_row2 in trs_new:
            trs_new.remove(remove_row2)
        
        data_all = [html_info_sh(k) for k in trs_new]
        
        df_sh = 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]
        })
        
        
        # 从html中提取有效信息——深交所
        # 读取html
        f = open('深交所年报.html',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 = [html_info_sz(k) for k in trs_new]
        
        df_sz = pd.DataFrame({
            'code': [d[0] for d in data_all],
            'name': [d[1] for d in data_all],
            'attachpath': [d[2] for d in data_all],
            'title':[d[3] for d in data_all],
            'date': [d[4] for d in data_all]
        })
        
        # 在爬取html过程中,可能由于网络延迟等原因并没有成功爬取到某上市公司的html,即df_sh.code(爬取所得上市公司代码)中未含有sh_info.上市公司代码(根据行业分类所得的行业内上市公司代码)中的代码
        # 故重新运行函数进行爬取(已成功爬取)
        get_mul_html_sh(list(set(sh_info.上市公司代码).difference(set(df_sh.code))))      
        检验是否爬取全批发业上市公司
        
        #=============爬取全html数据后重新使用正则表达式从爬取的页面中提取公司的代码、简称、年报名称以及年报的pdf下载链接=============

        # 从html中提取有效信息——上交所
        # 读取html
        f = open('上交所年报.html',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)

        remove_row1 = '证券代码证券简称公告标题公告时间'
        remove_row2 = '证券代码证券简称公告标题公告时间'

        while remove_row1 in trs_new:
            trs_new.remove(remove_row1)

        while remove_row2 in trs_new:
            trs_new.remove(remove_row2)

        data_all = [html_info_sh(k) for k in trs_new]

        df_sh = 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]
        })


        # 从html中提取有效信息——深交所
        # 读取html
        f = open('深交所年报.html',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 = [html_info_sz(k) for k in trs_new]

        df_sz = pd.DataFrame({
            'code': [d[0] for d in data_all],
            'name': [d[1] for d in data_all],
            'attachpath': [d[2] for d in data_all],
            'title':[d[3] for d in data_all],
            'date': [d[4] for d in data_all]
        })

        # 再次比对行业分类所含的批发业上市公司信息与爬取到的html中所含信息,并未有差异,即代表爬取所得的html包含全部批发业上市公司
        再次比对
        
      
    

(四)年报信息筛选

        
        # =================2.4 年报信息筛选,并将筛选好的数据存为xlsx格式文件=================
        # 观察整理后的数据可以发现,一年内某一页的数据不止一行,还存在摘要、英文版年报、补充公告、认可意见等,需要剔除无关数据行
        
        # 剔除近十年出现ST的代码
        st_sh = list(set(df_sh[df_sh.title.apply(lambda x:'ST' in x)].code))
        st_sz = list(set(df_sz[df_sz.name.apply(lambda x:'ST' in x)].code))
        df_sh = df_sh.set_index('code').drop(st_sh,axis = 0).reset_index()
        df_sz = df_sz.set_index('code').drop(st_sz,axis = 0).reset_index()
        
        st_sh = list(set(df_sh[df_sh.name.apply(lambda x:'ST' in x)].code))
        df_sh = df_sh.set_index('code').drop(st_sh,axis = 0).reset_index()
        
        # 信息筛选——上交所
        df_sh = filter_mul(df_sh,'title',["摘要","英文","公告","说明","审核","意见","已取消","审计"])
        
        # 生成年报对应年份
        df_sh['year'] = df_sh.date.to_frame().apply(lambda x:int(x[0][:4])-1,axis=1)
        
        # 存在年报更正现象,即某一年的年报存在多份
        # 根据年报发布的时间顺序,选取更正后的年报
        df_sh = df_sh.sort_values(['code','date'])
        df_sh = df_sh.groupby(['code','year']).tail(1)
        
        # 信息筛选——深交所
        df_sz = filter_mul(df_sz,'title',["摘要","英文","公告","说明","审核","意见","已取消","审计"])
        
        # 生成年报对应年份
        df_sz['year'] = df_sz.date.to_frame().apply(lambda x:int(x[0][:4])-1,axis=1)
        
        
        # 存在年报更正现象,即某一年的年报存在多份
        # 根据年报发布的时间顺序,选取更正后的年报
        df_sz = df_sz.sort_values(['code','date'])
        df_sz = df_sz.groupby(['code','year']).tail(1)
        
        
        # 选取近十年(2013-2022)的年报
        df_sh = df_sh[df_sh.year>2012]
        df_sz = df_sz[df_sz.year>2012]
        df_sh.reset_index(drop=True,inplace=True)
        df_sz.reset_index(drop=True,inplace=True)
        
        
        df_sh = df_sh.set_index('code').loc[list(set(sh_info.上市公司代码).intersection(set(df_sh.code)))].reset_index()
        df_sz = df_sz.set_index('code').loc[list(set(sz_info.上市公司代码).intersection(set(df_sz.code)))].reset_index()
        
        
        # 剔除十年年报有缺失的企业
        code_sh_10 = df_sh.groupby('code').name.count()[df_sh.groupby('code').name.count()==10].index.tolist()
        code_sz_10 = df_sz.groupby('code').name.count()[df_sz.groupby('code').name.count()==10].index.tolist()
        df_sh = df_sh.set_index('code').loc[code_sh_10].reset_index()
        df_sz = df_sz.set_index('code').loc[code_sz_10].reset_index()
        
        # 将筛选好的数据存为xlsx格式文件
        df_sh.to_excel('sh_data.xlsx')
        df_sz.to_excel('sz_data.xlsx')

      
    

(五)使用request库访问pdf下载链接,下载公司年报至对应文件夹

        
        # =================根据pdf下载链接,爬取公司的年报至对应文件夹=================
        df_sh = pd.read_excel('sh_data.xlsx',index_col=0)
        df_sz = pd.read_excel('sz_data.xlsx',index_col=0)
        
        
        # 创建文件夹储存年报pdf文件
        for i in set(df_sh.code):
            j = change_code(str(i))
            os.makedirs(r'.\\年报\\'+j)
        
        for i in set(df_sz.code):
            j = change_code(str(i))
            os.makedirs(r'.\\年报\\'+str(j))
        
        # 存储对应公司的年报至对应文件夹——上交所
        i = 1
        for a in list(set(df_sh.code)):
            df_part = df_sh[df_sh.code==a]
            if len(df_part)==0:
                continue
            tol_year = list(set(df_part.year))
            tol_year.sort()
            aa = change_code(str(a))
            for b in tol_year:
                df_part_year = df_part[df_part.year==b]
                
                filename = '.\\年报\\'+aa+"\\"+aa+'_'+str(b)+".pdf"
                output = aa+'_'+str(b)+".pdf"
                print("{}/{}: 正在爬取{}".format(i,len(df_sh),output))
                t = requests.get(df_part_year.href.iloc[0])
                time.sleep(4)
                with open(filename,"wb") as fp:
                    fp.write(t.content)
                i+=1          

        # 存储对应公司的年报至对应文件夹——深交所
        i = 1
        for a in list(set(df_sz.code)):
            df_part = df_sz[df_sz.code==a]
            aa = change_code(str(a))
            if len(df_part)==0:
                continue
            for b in range(len(df_part)):
                df_part_row = df_part.iloc[b]
                filename = '.\\年报\\'+aa+"\\"+aa+'_'+str(df_part_row.year)+".PDF"
                output = aa+'_'+str(df_part_row.year)+".pdf"
                print("{}/{}: 正在爬取{}".format(i,len(df_sz),output))
                t = requests.get(df_part_row.attachpath)
                time.sleep(5)
                with open(filename,"wb") as fp:
                    fp.write(t.content)
                i+=1

      
    
爬取年报

PART FOUR

PDF文件爬取——提取数据

通过年度报告要求披露内容中的“主要会计数据和财务指标”部分,可以获取到反映公司经营状况和盈利能力的数据,如营业收入,归属于上市公司股东的净利润等。故在完成上述数据爬取操作之后使用正则表达式解析爬取的pdf文件,提取公司名称、办公地址、公司网址、董事会秘书的姓名、电话和邮箱以及营业收入、归属于上市公司股东的净利润和每股收益,在获取公司信息的同时观察批发业上市公司近十年(2013-2022)的业绩及盈利能力变化。

        
        df_sh = pd.read_excel('sh_data.xlsx',index_col=0)
        df_sz = pd.read_excel('sz_data.xlsx',index_col=0)
        
        # 获取上交所和深交所证券代码文件夹中的所有pdf文件地址,便于后续提取
        pdf_list = []
        
        for i in list(set(df_sh.code))+list(set(df_sz.code)):
            j = change_code(str(i))
            file_path = "D:\\0AAAAAAAAAAAAAAAAAsmalldeskbook\\Financial Data Acquisition and Processing\\annual_report\\data\\年报\\"+j     
            file = glob.glob(os.path.join(file_path, "*.pdf"))     
            file.sort()
            for j in file:
                pdf_list.append(j) 
                
        df_pdf = pd.DataFrame()
        pdf_name = []
        pdf_years = []
        for r in pdf_list:
            if r[-6]=='_':
                pdf_name.append(r[-17:-11])
                pdf_years.append(r[-10:-4])
            else:
                pdf_name.append(r[-15:-9])
                pdf_years.append(r[-8:-4])
        
        df_pdf['code'] = pdf_name
        df_pdf['year'] = pdf_years
        df_pdf['pdf_path'] = pdf_list
        df_pdf = df_pdf.sort_values(by=['code','year'], ascending=False)
        df_pdf = df_pdf.reset_index(drop=True)
        print(df_pdf.head()) 
      
    

(一)解析pdf,提取公司名称、公司网址、办公地址

        
        # 解析pdf,提取公司名称、公司网址、办公地址

        df_info = get_mul_info(df_pdf)
        df_pdf['name'] = df_info.apply(lambda x:x[0]).tolist()
        df_pdf['location'] = df_info.apply(lambda x:x[1]).tolist()
        df_pdf['web'] = df_info.apply(lambda x:x[2]).tolist()
        

      
    

(二)解析pdf,提取董事会秘书姓名、电话和电子邮箱

        

        # 解析pdf,提取董事会秘书姓名、电话和电子邮箱
        df_info_secretary = get_mul_info_secretary(df_pdf)
        df_pdf['secretary_name'] = df_info_secretary.apply(lambda x:x[0]).tolist()
        df_pdf['secretary_tel'] = df_info_secretary.apply(lambda x:x[1]).tolist()
        df_pdf['secretary_email'] = df_info_secretary.apply(lambda x:x[2]).tolist()
      
    

(三)解析pdf,提取营业收入、归属于上市公司股东的净利润和每股收益(基本每股收益)

        
        # 解析pdf,提取营业收入、归属于上市公司股东的净利润和每股收益(基本每股收益)

        # 遇到问题——①有的是一、有的是直接(三),不要写'、'; 
        # ②由于表格换行的原因,基本每股收益可能会出现在不同行从而造成正则表达式的匹配错误;③有的是每股收益,有的是基本每股收益,稀释每股收益
        # ④专有名词中间可能会有换行符、其他字符(因为换页)等;
        # ⑤需要去找匹配错误的,一份一份去找
        # ⑥若专有名词中间有换行符,切片时也会出错
        
        sale_net_profit = get_mul_pro_sal(df_pdf)
        解析年报  
        # 将提取到的年报信息存入df_pdf数据框中
        df_pdf['Revenue'] = sale_net_profit.apply(lambda x:x[0]).tolist()
        df_pdf['Net_profit'] = sale_net_profit.apply(lambda x:x[1]).tolist()
        df_pdf['Earnings_per_share'] = sale_net_profit.apply(lambda x:x[2]).tolist()
      
    

(四)数据清洗

        
        # 数据清洗
        
        df_pdf.location = df_pdf.location.apply(lambda x:str(x))
        df_pdf.location = df_pdf.location.apply(lambda x:x.replace(' ',''))
        df_pdf.location = df_pdf.location.apply(lambda x:x.replace('\n',''))
        df_pdf.to_excel('年报数据.xlsx')

        df_pdf[['code','year','name', 'location', 'web', 'secretary_name',
        'secretary_tel', 'secretary_email', 'Revenue', 'Net_profit',
        'Earnings_per_share']].head()
        提取信息
      
    

PART FIVE

附录

(一)项目源代码介绍

  1. 本项目源代码由 上市公司年报爬取及可视化分析(点击查看)前部分中调用的各自定义模块两部分构成
  2. 上文中所展示的代码源自上市公司年报爬取及可视化分析源代码,其中调用了各自定义模块中的函数,上文中并未具体展示自定义模块中的自定义函数相关代码。自定义模块如下,点击即可查看模块中的自定义函数。
  3. download_html点击查看
  4. 负责年报披露网页的html数据爬取。使用selenium访问上海证券交易所及深圳证券交易所的年报披露网页,爬取批发业上市公司年报页面。其中,get_html_sh(code)、get_html_sz(code)函数分别实现某一上海证券交易所、深圳证券交易所上市公司年报披露页面的html文件爬取。get_mul_html_sh(codes)、get_mul_html_sz(codes)函数通过循环调用get_html_sh(code)、get_html_sz(code)函数,实现对含有多家上市公司证券代码的可迭代数据codes的年报披露页面的爬取。
  5. get_code点击查看
  6. 当证券代码为数值型数据时,将其转换为字符型数据时,不满六位数的证券代码(如428)需要在前面补0才可得到正确的证券代码。故在该模块中定义了change_code(x)函数,实现补全证券代码这一功能。
  7. html_process点击查看
  8. 负责对包含年报信息的html文件进行解析,提取证券代码、证券简称、年报下载链接、年报名称以及年报发布时间,并对dataframe进行信息筛选。

    html解析:html_info_sh(tr)、html_info_sz(tr)分别实现对上交所爬取所得年报html和深交所爬取所得年报html中的某行数据的解析,提取其中的证券代码、证券简称、年报下载链接、年报名称以及年报发布时间。

    信息筛选:filter(df,col,word)可以实现剔除df数据框中col列中包含单一字符串所在行,而filter_mul(df,col,words)可以剔除df数据框中col列中包含多字符串的可迭代数据words所在行,实现信息筛选功能。
  9. pdf_process点击查看
  10. 定义了可以实现公司名称、办公地址、公司网址、董秘姓名、董秘电话和董秘电子邮箱、营业收入、归属于上市公司股东的净利润的信息提取函数。
  11. ip点击查看
  12. 该模块中的acquire_ip()可实现代理ip的获取。

(二)实验中所遇问题

在课上以及完成实验报告的过程中代码常常会报错,但我通过不断的努力和摸索,最终成功地解决了这些问题。

1.用Selenium模拟点击时报出异常NoSuchElementException:Unable to locate......,通过查阅资料后得知,可能是因为程序运行速度过快而页面中对应的元素尚未出现,也就找不到目标元素,从而报错。故等待目标元素出现后在进行定位,使用下述代码解决了问题,也就是在20秒内每隔500毫秒扫描1次页面变化,当出现指定的元素后结束。

        
    import selenium.webdriver.support.ui as ui
    wait = ui.WebDriverWait(browser,20)
    wait.until(lambda browser: browser.find_element(By.CSS_SELECTOR, ".sse_outerItem:nth-child(4) .filter-option-inner-inner"))
  

2.此外,还出现TimeoutException: 由于目标计算机积极拒绝,无法连接。 (os error 10061)问题,也就是由于频繁地访问,IP被限制了。当待爬取数据过多,使用Python频繁访问某网站时可能会被拒绝访问。因为一个网站它会检测某一段时间某个IP的访问次数,如果访问次数过多,网站会禁止你的访问。所以我通过ip模块中的acquire_ip函数获取可用的代理ip后每隔一段时间换一个代理,或将需要爬取的数据拆分,分别使用不同的代理ip进行爬取来解决,这样便不会出现因为频繁访问而导致禁止访问的现象。此外,也可选择在流量较小的时间段进行爬取。

(三)课程心得

本项目基于上市公司年报数据爬取与分析,通过学习使用selenium自动化完成多页面数据爬取,完成对批发业上市公司近十年已披露的年度报告的批量下载,同时,使用正则表达式解析pdf文件,提取公司名称、办公地址、公司网址、董事会秘书的姓名、电话和邮箱以及营业收入、归属于上市公司股东的净利润和每股收益,最后使用pandas和matplotlib实现财务指标的数据分析及可视化。

我以往对于Python的应用多是涉及基本语法、numpy和pandas等第三方库的应用,虽然在学习这门课之前,我对爬虫已有简单的了解,但由于爬虫的知识体系较为庞大,内容也过于复杂,我自学的效果不是很好,抱着想要加深对爬虫的认识的想法选修了这门课程。这门课强化了我对正则表达式和爬虫的理解,让我对数据爬取的整个流程有了较为清晰的认知,并具备了一定Python数据爬取的能力。

在学习爬虫的过程中我领悟了以下几点:

1.要选择合适的编程平台,我选择了在Visual Studio Code中使用jupyter notebook运行python代码,其中分块运行模式让我感觉相较于spyder更便于调试代码。

2.编程中重要的是分析问题和解决问题的能力,多动手,多试错,才能进步地更快。

3.在学习爬虫的过程中,除了上课认真听讲、看学习视频以及阅读相关书籍外,在计算机上对爬虫具体操作步骤进行练习是必须的。我认为任何一门编程语言的学习都离不开简单模仿、创新设计再到向外拓展这三个步骤,在对基于Python的网络爬虫学习过程中,我一步步地实现了从初识、了解再到深入这一阶梯式学习,将相关代码逐一在计算机中输入并运行。

4.通过学习以Python为基础的网络爬虫技术,我深刻地认识到了这项技能的实用性,它可以帮助我们自动化地抓取网上的大量数据,从而为我们的研究提供了数据基础。但是在进行网络爬虫时也需注意一些合法性和道德性问题,避免对网站进行恶意攻击和抓取。

但本项目仍存在着一些问题有待解决及优化:

1.在使用正则表达式匹配获取相关数据时由于不同证券不同年份的年报存在差异性,爬取到的数据中可能存在空格、换行符等无效字符,数据需要进行进一步的清洗;

2.数据爬取速度较慢,可进一步在爬虫中使用异步实现高性能的数据爬取操作;

非常感谢吴老师的悉心教导,以通俗易懂的语言讲述较为复杂的知识,让我理解了正则表达式等高深内容,也扩展的关于工作目录,环境变量的知识。这门课加深了我对Python数据采集的兴趣,在未来会进一步学习爬虫领域相关知识,如反爬和防反爬机制、异步爬虫等内容,从而在数据获取方面实现更多有用的功能。

回到目录