张艺轩的实验报告

学号:0204766

目 录

    1.HTML链接整理、年报下载及数据清洗 点击查看

    2.提取营业收入、归属于上市公司股东净利润以及公司信息 点击查看

    3.部分财务指标数据可视化处理 点击查看

    4.可视化结果分析 点击查看

    5.实验心得 点击查看

    6.【附录】所属行业匹配规则程序设计 点击查看

按照老师的要求,依据证监会所发布的最新一版的行业分类,笔者匹配到了电气机械和器材制造业的前10家公司。现将该行业前10家上市公司的基本信息列示如下。所属行业匹配规则的代码实现过程见后文附录部分 点击查看

所属电气机械和器材制造业的10家公司

上市公司代码 上市公司简称
000049 德赛电池
000070 特发信息
000333 美的集团
000400 许继电气
000521 长虹美菱
000533 顺钠股份
000541 佛山照明
000921 格力电器
000651 海信家电
002828 思源电气

一、HTML链接整理、年报下载及数据清洗

1.1 函数定义

该部分函数旨在定义爬取各大交易所交易所定期报告页面的函数, 并对爬取之后的页面进行信息提取。即提取出每一家公司的年报的名称、 pdf的链接等等,最终将这些指标保存为一个CSV文件。

1.1.1 深交所官网数据获取方式定义

        
    import time
    import re
    import pandas as pd
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys

    def get_table_szse(code):
        driver = webdriver.Edge()
        driver.get("http://www.szse.cn/disclosure/listed/fixed/index.html")
        driver.set_window_size(1552, 832)
        time.sleep(3)
        driver.find_element(By.ID, "input_code").click()
        driver.find_element(By.ID, "input_code").send_keys(code)
        time.sleep(3)
        driver.find_element(By.ID, "input_code").send_keys(Keys.DOWN)
        driver.find_element(By.ID, "input_code").send_keys(Keys.ENTER)
        driver.find_element(By.CSS_SELECTOR, "#select_gonggao .c-selectex-btn-text").click()
        time.sleep(3)
        driver.find_element(By.LINK_TEXT, "年度报告").click()
        time.sleep(3)
        driver.find_element(By.CSS_SELECTOR, ".input-left").click()
        driver.find_element(By.CSS_SELECTOR, "#c-datepicker-menu-1 .calendar-year span").click()
        driver.find_element(By.CSS_SELECTOR, ".active li:nth-child(113)").click()
        driver.find_element(By.CSS_SELECTOR, "#c-datepicker-menu-1 tr:nth-child(1) > .available:nth-child(3) > .tdcontainer").click()
        driver.find_element(By.CSS_SELECTOR, "#c-datepicker-menu-2 tr:nth-child(2) > .weekend:nth-child(1) > .tdcontainer").click()
        driver.find_element(By.ID, "query-btn").click()
        element = driver.find_element(By.ID, 'disclosure-table')
        # add
        time.sleep(5)
        table_html = element.get_attribute('innerHTML')
        fname = '{}.html'.format(code)
        f = open(r"E:\张艺轩\计算机实验\金融数据获取与处理\nianbao\src\nianbao\data\HTML_Table\{}".format(fname),'w',encoding='utf-8')
        f.write(table_html)
        f.close()
        driver.quit()
    #循环获取
    def get_table_szse_codes(codes):
        for code in codes:
            get_table_szse(code)

        #HTML解析
    def get_szse_data(tr):
        p_td = re.compile('(.*?)',re.DOTALL)
        tds = p_td.findall(tr) #tds是一个列表,包含四个标签对
        # 代码
        code_compile = re.compile('code.*?>(.*?)',re.DOTALL)
        code = code_compile.findall(tds[0])[0]
        # 名称
        name_compile = re.compile('title=.*?>(.*?)')
        name = name_compile.findall(tds[1])[0]
        # pdf地址
        pdf_compile = re.compile('(.*?)',re.DOTALL)
        title = title_date_compile.findall(tds[2])[0]
        date = title_date_compile.findall(tds[3])[0]
        data = [code,name,herf,title,date]
        return data

    def parse_szse_table(fname):
        f = open(fname,encoding = 'utf-8')
        html = f.read()
        f.close()
        #
        p = re.compile('(.+?)',re.DOTALL)
        trs = p.findall(html)
        # 找出不为空的“tr对”
        trs_new = []
        for tr in trs:
            if tr.strip() != '':
                trs_new.append(tr)
        #del trs_new[0]
        # 应用get_data()函数和提取code、name、herf、title 和 date;
        data_all = [get_szse_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]})
        # 将时间序列标准化
        df['date'] = df['date'].apply(pd.to_datetime) 
        #
        return df
        
        

1.1.2 上交所官网数据获取方式定义

        
    import re
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.action_chains import ActionChains
    from selenium.webdriver.support import expected_conditions
    from selenium.webdriver.support.wait import WebDriverWait
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
    import time
    
    def get_table_sse(code):
        browser = webdriver.Edge()
        #以下皆为用selenium.IDE录制的导出结果
        browser.get("http://www.sse.com.cn/disclosure/listedinfo/regular/")
        # 2 | setWindowSize | 1552x832 | 
        browser.set_window_size(1552, 832)
        # 3 | click | id=inputCode | 
        browser.find_element(By.ID, "inputCode").click()
        # 4 | type | id=inputCode | 601919
        browser.find_element(By.ID, "inputCode").send_keys("601919")
        # 5 | click | css=.sse_outerItem:nth-child(4) .filter-option-inner-inner | 
        time.sleep(5) #在此等待5秒
        browser.find_element(By.CSS_SELECTOR, ".sse_outerItem:nth-child(4) .filter-option-inner-inner").click()
        # 6 | click | linkText=年报 | 
        browser.find_element(By.LINK_TEXT, "年报").click()
        # 7 | select | css=.dropup > .selectpicker | label=年报
        dropdown = browser.find_element(By.CSS_SELECTOR, ".dropup > .selectpicker")
        dropdown.find_element(By.XPATH, "//option[. = '年报']").click()

        #
        css_selector = "body > div.container.sse_content > div > div.col-lg-9.col-xxl-10 > div > div.sse_colContent.js_regular > div.table-responsive > table"
        element = browser.find_element(By.CSS_SELECTOR, css_selector)

        table_html = element.get_attribute('innerHTML')
        f = open('601919年报.html','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) #tds是一个列表,包含四个标签对
        #
        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('herf="') + 6
        e = tds[2].find('.pdf"') + 4
        herf = 'https://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,herf,title,date]
        return data
    #用函数来封装上述代码
    def parse_table(table_html):
        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]})
        return df
        
        

1.1.3 数据清洗、过滤函数定义

    
    import pandas as pd
    from datetime import datetime

    def filter_links(words,df,include = True):
        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):
        start = pd.to_datetime(start)
        end = pd.to_datetime(end)
        date = df['date']
        v = [d >= start and d <= end for d in date]
        df_new = df[v]
        return df_new

    def start_end_10y():
        df_now = datetime.now()
        current_year = df_now.year
        start = f'{current_year-9}-01-01'
        end = f'{current_year}-12-31'
        return (start,end)

    #集成过滤函数
    def filter_nb_10y(df,keepwords = ['年报','年度报告'],
                        exclude_word = ['摘要','更新后','已取消','更正后','英文版','英文']
                        ,start = '',end = ''):
        if start == '':
            start,end = start_end_10y()
        else:
            start_y = int(pd.to_datetime(start[0:4]))
            end = '{}-12-31'.format(start_y + 9)
        #
        df = filter_links(keepwords,df,include=True)
        df = filter_links(exclude_word,df,include= False)
        df =  filter_date(start,end,df)
        return df

    def prepare_hrefs_years(df):
        hrefs = list(df['href'])
        years = [str(int(d[0:4])-1) for d in df['date']]
        code = list(df['code'])
        return pd.DataFrame([code,hrefs, years],index = ['code','href','year']).T
       
        

1.1.3 年报批量下载函数定义

    
    import requests
    import pandas as pd
    import time
    import os
    
    def download_pdf(href,code,year):
        r = requests.get(href, allow_redirects=True)
        path = "E:\\张艺轩\\计算机实验\\金融数据获取与处理\\nianbao\\src\\nianbao\\data"
        if os.path.exists(path+'\\{}'.format(code)) == True:
            fname = '{}_{}.pdf'.format(code,year)
            f = open(path+'\\{}\\{}'.format(code,fname), 'wb')
            f.write(r.content)
            f.close()
            r.close()
        else:
                os.mkdir(path)
                fname = '{}_{}.pdf'.format(code,year)
                f = open(path+'\\{}\\{}'.format(code,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_code(list_hrefs,codes,list_years):
        for i in range(len(list_hrefs)):
            hrefs = list_hrefs[i]['href']
            years = list_years
            code = codes[i]
            download_pdfs(hrefs,code,years)
        return()
    
         

1.1.4 调用上述代码示例

该部分利用之前定义的函数,先爬取HTML提取信息,过滤后保存为CSV文件。 而后通过读取CSV文件,访问每一张DataFrame的href列,获取pdf下载链接, 最终爬取到10家上市公司的10年的pdf格式的年报。其中,为了便于文件管理,笔者还引入了 os模块进行操作。运行结果如下所示。

    
    from sse import get_table_sse,get_table_sse_codes,get_data,parse_table
    from szse import get_table_szse,get_table_szse_codes,get_szse_data,parse_szse_table
    from filter_url import filter_links,filter_date,start_end_10y,filter_nb_10y,prepare_hrefs_years
    from download import download_pdf,download_pdfs,download_pdfs_code
    import pandas as pd
    import numpy as np
    from pdf_parse import get_target_subtxt,get_th_span,get_bounds,get_keywords,parse_key_fin_data,file_name_walk,pdf_sum_pa,get_target
    import fitz
    import re
    import os
    
    # ----------------------------------------HTML爬取-------------------------------------------------------
    # 建立股票列表,以便于进行调用
    # 必须dtype = object,否则前面的0将省略
    elec_stock = pd.read_csv(r'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/股票代码.csv',
                                encoding= 'UTF-8',dtype = object) 
    elec_stock.pop('Unnamed: 0')
    elec_stock = elec_stock[['上市公司代码', '上市公司简称']]
    
    #  建立一个文件夹路径备用
    paths = "E:\\张艺轩\\计算机实验\\金融数据获取与处理\\nianbao\\src\\nianbao\\data"
    # 如果股票代码是以szcode列表里列示的开头方式,则使用深交所href获取方式,否则用上交所href获取方式
    # 以CSV格式保存
    szcode =('200','300','301','000','080','00')
    for index,row in  elec_stock.iterrows():
        code = row[0]
        name = row[1]
        if code.startswith(szcode) ==True:
            get_table_szse(code)
    
        else:
            get_table_sse(code)
    # 过滤已经保存的CSV文件,去掉含有'摘要','已取消','英文版','英文'和'更新前'等字段的链接
    obj = os.listdir(r"E:\张艺轩\计算机实验\金融数据获取与处理\nianbao\src\nianbao\data\HTML_Table")
    for i in obj:
            df = parse_szse_table(r'{}\HTML_Table\{}'.format(paths,i))
            df = filter_nb_10y(df,keepwords = ['年报','年度报告','更新后','更正后'],
                            exclude_word = ['摘要','已取消','英文版','英文','更新前']
                            ,start = '',end = '')
            df.to_csv('{}\\HREF_Data\\{}.csv'.format(paths,i[0:6]))
            
    #------------------------------------------下载年报-----------------------------------------------------
    # 整理CSV,整理出符合要求的href列表
    obj1 = os.listdir(r"E:\张艺轩\计算机实验\金融数据获取与处理\nianbao\src\nianbao\data\HREF_Data")
    list_hrefs = []
    for i in obj1:
        df = pd.read_csv(paths+'\\HREF_Data\\{}'.format(i),dtype = object)
        temp = prepare_hrefs_years(df)
        list_hrefs.append(temp)
    # 获取年报
    download_pdfs_code(list_hrefs,elec_stock['上市公司代码'],list_hrefs[0]['year'])
    
    

1.2 运行结果

所爬取的HTML文件列表、从CSV当中提取的信息组成的CSV文件及文件列表(以“美的集团”)以及10家公司年报列表如下所示。

结果截图
所爬取的HTML文件以及CSV文件列表.png

结果截图
年报pdf文件列表(局部).png

结果截图
CSV列表(以“美的集团”为例).png

二、提取营业收入、归属于上市公司股东净利润以及公司信息

2.1 函数定义以及代码调用

2.1.1 pdf文件解析函数定义

    
    import fitz
    import re
    import pandas as pd
    import os
   
    # 定位需要的内容
    def get_target_subtxt(doc,bounds = (['主要会计数据及财务指标','主要会计数据和财务指标'],'归属于上市公司股东的净资')): #
        # 默认设置为首页页码
        start_pageno = 0
        end_pageno = len(doc) - 1
        # 获取上界页码
        for n in range(len(doc)):
            # texts = page.get_text()
            if (bounds[0][0]  in doc[n].get_text()) or (bounds[0][1] in doc[n].get_text()):
                start_pageno = n
                break
        # 获取下界页码
        for i in range(start_pageno,len(doc)):
            p = re.compile('(?:\s*\n*归\s*\n*属\s*\n*于\s*\n*上\s*\n*市\s*\n*公\s*\n*司\s*\n*股\s*\n*东\s*\n*的\s*\n*净\s*\n*资\s*\n*产\s*\n*)',re.DOTALL)
            a = p.findall(doc[i].get_text())
            if len(a)==0:
                continue
            elif bounds[1] in a[0].replace('\n','').replace(' ',''):
                end_pageno = i
                break
        # 获取界定的内容
        txt = ''
        if start_pageno == end_pageno:
            txt = doc[start_pageno].get_text()
        else:  
            for _ in range(start_pageno,end_pageno + 1):
                page =doc[_]
                txt += page.get_text()
        
        return txt
    
    # 获取界定
    def get_th_span(txt):
        nianfen = '(20\d\d|199\d)\s*年末?' #
        s = '{}\s*{}.*?{}'.format(nianfen,nianfen,nianfen)
        p = re.compile(s,re.DOTALL)
        matchobj_ = p.search(txt)
        # 
        end  = matchobj_.end()
        year1 = matchobj_.group(1)
        year2 = matchobj_.group(2)
        year3 = matchobj_.group(3)
        #
        flag = (int(year1) - int(year2) == 1) and (int(year2) - int(year3) == 1)
        while not flag:
            matchobj_.search(txt[end:])
            end  = matchobj_.end()
            year1 = matchobj_.group(1)
            year2 = matchobj_.group(2)
            year3 = matchobj_.group(3)
            flag = int(year1) - int(year2) == 1 
            flag = flag and (int(year2) - int(year3) == 1)
        return ([year1,year2,year3],matchobj_.span())
    
    def get_bounds(txt):
        th_span_1st = get_th_span(txt)[1]
        end = th_span_1st[1]
        th_span_2nd = get_th_span(txt[end:])[1]
        th_span_2nd = (end + th_span_2nd[0],end + th_span_2nd[1])
        #
        s = th_span_1st[1]
        e = th_span_2nd[0]-1
        while txt[e] not in '0123456789':
            e = e-1
        return (s,e+1)
    
    
    # 提取关键字
    def get_keywords(subtxt):
        
        p = re.compile(r'\d+\s*?\n\s*?([\u2E80-\u9FFF]+)')
        keywords = p.findall(subtxt)
        if '营业收入' not in keywords:
            keywords.insert(0,'营业收入')
        for i in keywords:
            if len(i) <= 3:
                keywords.remove(i)
        for x in keywords:
            if ('股份有限公司' or '年度报告') in x:
                keywords.remove(x)
        return keywords
    
        
    # 提取成表格
    def parse_key_fin_data(subtxt,keywords):
        '''
        将已知的数据提取成表格
        '''
        ss = []
        s= 0
        for kw in keywords:
            n = subtxt.find(kw,s)
            ss.append(n)
            s = n +len(kw)
        ss.append(len(subtxt))
        data = []
        p = re.compile('\D+(?:\s+\D*)?(?:(.*)|\(.*\))?')
        p2 = re.compile('\s')
        p3 = re.compile('(\s*\n*\-*(\d{1,3}(?:,\d{3})*(?:\.\d+)?(?:\%)?)\s*){3,4}',re.DOTALL)
        for n in range(len(ss)-1):
            s = ss[n]
            e = ss[n+1]
            line = subtxt[s:e]
            # 获取可能换行的账户名
            matchobj = p.search(line)
            account_name = matchobj.group().replace('\n','')
            account_name = p2.sub('',matchobj.group())
            # 获取3年数据
            #amnts = line[matchobj.end():].split()
            
                #
            matchobjs = p3.search(line)
            amnts = matchobjs.group().split()
            #加上账户名称
            amnts.insert(0,account_name)
            #追加到总数据
            data.append(amnts)
        return data
    # 
    def pdf_sum_pa(paths,fil_name):
        '''
        将上述函数汇总,并将解析出来的结果
        保存为一个csv文件
        '''
        pdf_path = '{}\{}'.format(paths,fil_name)
        doc = fitz.open(r'{}'.format(pdf_path)) # r'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/pdf_data/002028/002028_2022.pdf'
        # 获取大致范围的文本
        txt = get_target_subtxt(doc)
        # 建立列名
        col = [ x for x in get_th_span(txt)[0]]
        col.insert(-1,'变动')
        col.insert(0,'指标')
        # 获取精确范围
        span  = get_bounds(txt)
        subtxt = txt[span[0]:span[1]]
        # 获取项目名称
        keywords = get_keywords(subtxt)
        # 建立成列表族
        datas = parse_key_fin_data(subtxt,keywords)
        # 换成DataFame
        df = pd.DataFrame(datas,columns=col)
        new_dirs = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/csv_data'
        dir_name = new_dirs+'\{}'.format(fil_name[0:6])
        if os.path.exists(dir_name) == False:
            os.makedirs(dir_name)
            df.to_csv(r'{}\{}_{}.csv'.format(dir_name,fil_name[0:6],fil_name[7:-4]))
        else:
            df.to_csv(r'{}\{}_{}.csv'.format(dir_name,fil_name[0:6],fil_name[7:-4]))
    #     
    def file_name_walk(file_dir):
        '''
        定义一个返回文件所在的绝对路径和
        文件名的列表的函数
        '''
        file_path_lst = []
        for x in os.walk(file_dir):
                file_path_lst.append((x[0],x[2]))
        return file_path_lst
    
    
    # 临时定义一个起始页码的函数
    def get_target(doc,bounds =['股票简称','信息披露及备置地点']): 
        # 默认设置为首页页码
        start_pageno = 0
        # 获取上界页码
        for n in range(len(doc)):
            # texts = page.get_text()
            if (bounds[0] or bounds[0]) in doc[n].get_text():
                start_pageno = n
                break
        return start_pageno
    
    

2.1.2 公司信息提取代码

该部分内容,充分利用正则表达式,旨在按要求提取出10家上市公司“公司简称”“股票代码”“办公地址”“公司网址”“董秘电话”“董秘&公司电子信箱”等指标的关键信息。 指标数据以每家公司最新的年报为准。

    
    # ---------------------------------------提取公司信息------------------------------------------------
    # 利用file_name_walk找出pdf_data下所有文件的路径和文件名
    paths = "E:\\张艺轩\\计算机实验\\金融数据获取与处理\\nianbao\\src\\nianbao\\data\\pdf_data"
    paths_pos = file_name_walk(paths)
    del paths_pos[0]
    # 创建一个只包含每只股票2022年年报绝对路径的列表,然后遍历调用
    abs_pos_lst = []
    for i in file_name_walk(paths):
        for n in i[1]:
            if '2022' in n:
                abs_pos = '{}/{}'.format(i[0],n)
                abs_pos_lst.append(abs_pos)
            else:
                continue
    # 提前创建一个表格,以便于存储数据
    keys_Data = pd.DataFrame(columns=['公司简称','股票代码','办公地址','公司网址','董秘电话','董秘&公司电子信箱'])
    for i in abs_pos_lst:
        docs = fitz.open(i)#i[0]+'\\{}'.format(n)
        
        brief_ =docs[get_target(docs,bounds =['股票简称','信息披露及备置地点'])].get_text()
        # name
        cor_name_compile = re.compile('.*股票简称.*?\n(.*?)\s*\n*股票代码.*',re.DOTALL)
        name = cor_name_compile.findall(brief_)[0].strip()
        # code
        cor_code_compile = re.compile('.*股票代码.*?\n(.*?)\s.*',re.DOTALL)
        code = cor_code_compile.findall(brief_)[0].strip()
        # address
        cor_address_compile = re.compile('办公地址.*?\n(.*?)\s*\n.*?',re.DOTALL)
        address = cor_address_compile.findall(brief_)[0].strip()
        # web
        cor_web_compile = re.compile('公司网址.*?\n\s*(.*?)\n电子信箱',re.DOTALL)
        web = cor_web_compile.findall(brief_)[0].strip()
        # secretary_telephone
        sec_tel_compile = re.compile('.*电话.*?\n\s*(.*?)\s*\n.*?',re.DOTALL)
        tele = sec_tel_compile.findall(brief_)[0].strip()
        # secretary_mail
        sec_mail_compile = re.compile('.*电子信箱.*?\n\s*(.*?)\n.*',re.DOTALL)
        mail = sec_mail_compile.findall(brief_)[0].strip()      
        keys_Data.loc[len(keys_Data.index)] = {'公司简称':name,'股票代码':code,
                                                '办公地址':address,'公司网址':web,
                                                '董秘电话':tele,'董秘&公司电子信箱':mail}
    keys_Data.to_csv('E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/corperate_info.csv',encoding = 'ANSI')
    
    

2.1.3 pdf文件解析函数调用

此处在提取会计数据表格时,笔者使用了“try-except”异常处理结构:如果格式正常,则正常执行“try”后的语句; 如果解析格式异常,则打印错误类型,执行except后面的内容(用pdfplumber提取)。这样就保证了保证程序的正常运行。

在本例中,只有代码为002028的公司2020年与2021年的年报解析失败,原因用fitz读取换行符太多。利用异常处理结构对该错误进行修正的结果见下文的“错误解决.png”。

    
    #-------------------------------------------表格提取------------------------------------------------------
    # 提取每家公司每年的主要会计数据和财务指标,并保存为csv数据
    file_dir = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/pdf_data'
    pdf_path = file_name_walk(file_dir)
    del pdf_path[0]
    for ele in pdf_path[-2:]:
        for name_ in ele[1]:
            # 利用异常处理结构,保证程序正常运行
            try:
                pdf_sum_pa(ele[0],name_)
            except Exception as e:
                # 002028的2020与2021解析失败,原因用fitz读取换行符太多
                print('--------------------------------------------------')
                print(name_,'出现异常.错误类型为:',e)
                # 利用pdfplumber进行提取
                new_pos = '{}/{}'.format(ele[0],name_)
                new_dirs = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/csv_data'
                dir_name = new_dirs+'\{}'.format(name_[0:6])
                import pdfplumber
                page = pdfplumber.open(new_pos).pages
                for i in page:
                    if '主要会计数据和财务指标' in i.extract_text():
                        df = pd.DataFrame(i.extract_table())
                        df.columns = df.iloc[0]
                        df.drop(0,inplace = True)
                        df.to_csv(r'{}\{}_{}.csv'.format(dir_name,name_[0:6],name_[7:-4]))
                #
                print(name_,'错误已解决,内容成功为pdfplumber所提取')
    
    

接下来,从提取出来的表格提取“营业收入”“归属于上市公股东的净利润” 等字段在当年的数据,按照公司为类别合并成时间序列类型的表格。 执行代码如下所示。具体结果见下文“执行结果”一栏。

    
    # ----获取每一家公司由10年“营业收入”“归属于上市公司股东的净利润”以及“基本每股收益”组成的的时间序列表格------
    file_dir = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/csv_data'
    # file_name_walk获取文件名和路径,返回一个列表
    file_pos = file_name_walk(file_dir)
    del file_pos[0]
    Series_Data_Path = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/Series_data'
    for i in file_pos:
        # 对于每家公司,建立一个合并的对象(列表)
        joint_lst = []
        for x in i[1]:
            df = pd.read_csv('{}/{}'.format(i[0],x),dtype = object,encoding = 'UTF-8')
            if 'Unnamed: 0' in df.columns:
                df = df.drop('Unnamed: 0', axis=1)
            # 每一张表可能有不同的情形,可以将所有的情况进行列示
            lst = ['营业收入(元)-','营业收入(元)','营业收入','营业收入(千元)',
                    '归属于上市公司股东的净利润(元)-',
                    '归属于上市公司股东的净利润(元)','归属于上市公司股东的净利润(千元)',
                    '归属于上市公司股东的净利润','基本每股收益(元/股)-','基本每股收益(元/股)']
            # 用isin函数进行提取
            df = df[df['指标'].isin(lst)].iloc[:,0:2]
            # 整理表格
            df.index = range(len(df))
            df = df.T
            df.columns = df.iloc[0]
            df.drop(index = '指标',inplace = True)
            # 添加合并对象
            joint_lst.append(df)
        # 由10年“营业收入”“归属于上市公司股东的净利润”以及“基本每股收益”组成的的时间序列表格
        Series_Data = pd.concat(joint_lst,axis = 0)
        #
        for i in Series_Data.index:
            # 用any判断是否有些行的内容与列名相同,如果True,则删去与列相同的行
            if any(Series_Data.columns == Series_Data.loc[i]) == True:
                    Series_Data.drop(i,inplace = True)
        # 以CSV的格式进行保存
        Series_Data.to_csv('{}/{}.csv'.format(Series_Data_Path,x[0:6]),encoding = 'ANSI')
    
    

将每一个指标(以“营业收入”为例)时间序列按公司合并, 整理成时间序列类型的数据,方便后续绘图调用。

    
    series_pos = 'E:/张艺轩/计算机实验/金融数据获取与处理/nianbao/src/nianbao/data/Series_data'
    csv_name = os.listdir(series_pos)
    
    # 创建包含代码名称的映射字典
    code_ = ['000049','000070','000333','000400','000521','000533','000541','000921','000651','002028']
    values = ['德赛电池','特发信息','美的集团','许继电气','长虹美菱','顺钠股份','佛山照明','格力电器','海信家电','思源电气']
    nvs = zip(code_,values)
    nvDict = dict( (name,value) for name,value in nvs)
    # 并表
    merge_lst = []
    for _ in csv_name:
        df = pd.read_csv('{}/{}'.format(series_pos,_),encoding = 'ANSI',dtype = object,index_col = 0)
        df_revenue = df.iloc[:,0] #这里只提取营业收入,改变列索引,就可以提取其他的列
        df_revenue.rename(nvDict[_[0:-4]],inplace = True)
        df_revenue.columns = [nvDict[_[0:-4]]]
        merge_lst.append(df_revenue)
    revenue_series = pd.concat(merge_lst,axis = 1)
    revenue_series = revenue_series.applymap(lambda x : x.replace(',','')).applymap(pd.to_numeric)
    revenue_series['美的集团'] = revenue_series['美的集团'].apply(lambda x :x*1000)
    revenue_series = revenue_series.applymap(lambda x : x/1000000000)
    
    
    
    # 合并上市公司股东净利润的时间序列表格
    merge_lst = []
    for _ in csv_name:
        df = pd.read_csv('{}/{}'.format(series_pos,_),encoding = 'ANSI',dtype = object,index_col = 0)
        df_profit = df.iloc[:,1]
        df_profit.rename(nvDict[_[0:-4]],inplace = True)
        df_profit.columns = [nvDict[_[0:-4]]]
        merge_lst.append(df_profit)
    profit_series = pd.concat(merge_lst,axis = 1)
    profit_series = profit_series.applymap(lambda x : x.replace(',','')).applymap(pd.to_numeric)
    profit_series['美的集团'] = profit_series['美的集团'].apply(lambda x :x*1000)
    profit_series = profit_series.applymap(lambda x : x/1000000000)
    
    

2.2 运行结果

结果截图
错误解决.png

结果截图
各公司财务指标时间序列.png
结果截图
各公司营业收入时间序列.png

爬取的公司信息

公司简称 股票代码 办公地址 公司网址 董秘电话 董秘&
公司电子信箱
德赛电池 000049 深圳市南山区高新科技园南区高新南一道德赛科技大厦东座 26 楼 www.desaybattery.com.cn (0755)862 99888 IR@desaybattery.com
特发信息 000070 深圳市南山区高新区中区科丰路 2 号特发信息港大厦 B 栋 18 楼 www.sdgi.com.cn 0755-66833901 sdgi_dmc@sdgi.com.cn
美的集团 000333 广东省佛山市顺德区北滘镇美的大道 6 号美的总部大楼 http://www.midea.com 0757-22607708 IR@midea.com
许继电气 000400 河南省许昌市许继大道 1298 号 http://www.xjec.com/ 0374-3213660 xjdqzqb@163.com
长虹美菱、虹美菱 B 000521、200521 合肥市经济技术开发区莲花路 2163 号 http://www.meiling.com 0551-62219021 lixia@meiling.com
顺钠股份 000533 广东省佛山市顺德区大良街道逢沙萃智路 1 号车创置业广场 1 栋 18 楼 02—07 单元 www.shunna.com.cn 0757-22321218 weihg@shunna.com.cn
佛山照明/粤照明 B 000541、200541 广东省佛山市禅城区汾江北路 64 号 www.chinafsl.com (0757)82810239 fsldsh@chinafsl.com
海信家电 000921 广东省佛山市顺德区容桂街道容港路 8 号 http://hxjd.hisense.cn/ (0757)28362570 hxjdzqb@hisense.com
格力电器 000651 广东省珠海市前山金鸡西路 http://www.gree.com.cn 0756-8669232 gree0651@cn.gree.com
思源电气 002028 上海市闵行区华宁路 3399 号 www.sieyuan.com 021-61610958 IR@SIEYUAN.COM

三、部分财务指标数据可视化处理

3.1 可视化代码

本部分绘图的数据来自于第二部分整理合并的各指标时间序列。笔者从时间序列和横截面横向对比两个角度,对营业收入和上市公司股东净利润进行了可视化处理。

其中,因为美的集团和格力电器的财务指标量级在千亿以上,与其余8家公司大相径庭。为了绘图的显著性,笔者在进行10家上市公司的财务指标可视化的基础上,又进一步剔除了这两家公司,以显示剩余8家公司的时序关系和相互对比关系。

3.1.1 “营业收入”时序图和纵向对比图绘制

(1) 营业收入时序图
    
    # 各家公司营业收入绘图
    from pylab import mpl
    import matplotlib.pyplot as plt
    mpl.rcParams['font.sans-serif'] = ['KaiTi']
    mpl.rcParams['axes.unicode_minus'] = 0
    plt.style.use('ggplot')
    plt.figure(figsize=(15,9))
    for i in range(len(revenue_series)):
        # 绘制子图
        plt.subplot(2,5,i+1)
        plt.plot(revenue_series.iloc[:,i],label = revenue_series.iloc[:,i].name)
        plt.xlabel(u'年 份',fontsize = 12)
        plt.ylabel(u'营业收入(单位:10亿元)',fontsize = 12)
        plt.xticks(fontsize = 12)
        plt.yticks(fontsize = 12)
        plt.legend(loc = 0,fontsize =13)#bbox_to_anchor=(1.01, 1), loc=0, borderaxespad=0
        plt.tight_layout()
        plt.suptitle('电器行业10家上市公司2013-2022年营业收入时序图',fontsize = 18,fontweight='heavy')
    plt.show()
    
        
(2) 营业收入纵向对比图
    
    # 电器行业10家上市公司10年营业收入纵向对比图'
    plt.figure(figsize=(15,6))
    ro = 0
    labels = list(revenue_series.index)
    x = np.arange(len(labels))
    for i in range(len(revenue_series[:])):
        labels = list(revenue_series.index)
        plt.bar(x+ro,revenue_series.iloc[:,i],width=0.08,label = revenue_series.iloc[:,i].name,edgecolor = 'black')
        ro += 0.08
        plt.xticks(x+0.3,labels,fontsize = 13,rotation = 22.5)
        plt.yticks(fontsize = 13)
        plt.xlabel(u'年  份',fontsize = 15)
        plt.ylabel(u'营业收入(单位:10亿元)',fontsize = 15)
        plt.legend()
    plt.title(u'2013-2022年电器行业10家上市公司营业收入横向向对比图',fontsize = 18,fontweight='heavy')
    plt.show()
    
    

3.1.1 “归属于上市公司股东的净利润”时序图和纵向对比图绘制

“归属于上市公司股东的净利润”时序图和纵向对比图绘制原理与之前类似。故在此不再赘述。

3.2 绘图结果

结果截图
营业收入时序图.png

结果截图
营业收入对比图.png

结果截图
净利润时序图.png

结果截图
净利润对比图.png

结果截图
8家营业收入对比图.png

结果截图
8家净利润对比图.png

结果截图
净格力电器财务指标时序图.png

四、可视化结果分析

4.1 财务指标横截面分析

首先,由整体的横向对比图可知,由于“美的集团”和“格力电器”所占市场份额较大,其营业收入和净利润与其他公司不在一个量级,故而笔者在后面又将“美的集团”和“格力电器”剔除,单独对8家进行了横向对比分析。

通过对比可知,“美的集团”“格力电器”“海信家电”以及“许继电气”占据了市场90%以上的份额,且营业收入和净利润连年稳定增长。其余几家公司所占市场份额较少,营业收入和净利润波动较大。其中,像“长虹美菱”“顺钠股份”和“特发信息”等企业在2018年出现了较大幅度的下滑,营业收入甚至出现了负增长。

综上可知,在2018年中美贸易战和我国全面推进供给侧结构性改革的背景下,电器行业的发展连续3年下行压力巨大。而在2022年,电器行业复苏势头明显,各家公司营业收入均出现了不同程度回暖。

4.2 财务指标时间序列分析

4.2.1 营业收入时间序列分析

如图所示,从时间上来看,在10家上市公司中,除了“特发信息”和“顺纳股份”外,其他公司的营业收入均处于波动上升的态势。尤其是“德赛电池”“美的集团”和“思源电气”,仅10年营业收入上涨波动最小,上涨持续时间最久

众所周知,电器行业β值较大,其发展与经济周期存在着密切的关系。自2020年新冠疫情爆发以来,经济下行压力持续加大,对电器行业各公司产生了不同程度的冲击。但是该行业营业收入普遍波动的根本原因并非全由新冠疫情导致,疫情因素只能算作是短期冲击因素。根本原因应在于自2018年以来经济下行周期的开始。

自2018年以来,图中电器行业所呈现的电器行业各家公司营业收入均出现了不同程度的下滑。者一方面是居民需求的转变,居民购买需求转变为以更新换代需求为主,且释放缓慢;另一方面,受政策空窗、经济放缓、原材料价格持续高位、房地产遇冷等等外部因素影响,家电市场规模增长失速。

4.2.2 归属于上市公司股东的净利润时间序列分析

由图可知,归属于上市公司股东的净利润(以下简称“净利润”)走势基本与营业收入成正比例关系。但是波动幅度明显大于营业收入。这一方面是由于2018年以来,成本端生产技术不断革新、大宗商品价格波动剧烈导致的;另一方面根据默顿关于股利政策和公司价值的理论(MM理论),2018年以来,经济波动幅度增大,企业投资机会也随之波动剧烈。因此,企业的鼓励政策存在着较大的不确定性。然而,从图中可知,对于市场份额持有较大的公司,股利政策要比市场份额持有较小的公司更加稳定,且受周期波动的影响较小。

综上,要实现电器行业市场规模的稳步增长,必须从需求供给两端发力。2023年,在宏观经济基本面持续向好的支撑下,家电国内销售规模有望“企稳回升”。首先,短期内消费意愿的恢复和提升,房地产市场预期和信心的回暖有助家电消费回温;其次,中长期来看,城镇化进程继续推进,部分家电保有量仍有较大空间。而中国家电企业坚持不懈的创新,则为行业的健康发展输送着源源不断的内生动能。

五、实验心得

本次实验,从编写到测试,历时1周,最终得以顺利完成。自己虽然经历了无数次报错,但是在我对错误的分析处理当中,自己对Python编程又有更高层次的认识和掌握。自己在这门课程当中主要的收获有以下几点:

首先,在吴燕丰老师的指导下,我对面向对象式的编程有了初窥门径的认识,代码的编写也比以往更有逻辑、更加精简,更加友好。

其次,在吴老师的带领下,自己对于文本爬虫、HTML5和正则表达式的编写有了初步的掌握。上学期,我在学习《金融建模基础》的时候,自己就觉得在数据获取方面存在着很大不足,而这门课正好为我弥补这方面的工具的缺失提供了不错的路径。

最后,希望在这门课的基础上,自己能够继续努力,不断拓展对网络爬虫的认识,更加深入地掌握一些更加面向实践的数理算法。

返回目录

【附录】所属行业匹配规则程序设计

证监会发布的《2021年行业分类》较多,若采用手动搜索的方式寻找学生所属的行业,则十分繁琐。因此,笔者按照吴老师的匹配规则要求,编写了所属行业自动匹配的Python脚本,充分运用计算机的优势,以实现系统化、快速化、自动化的处理。代码和结果展示如下:

    
    # -*- coding: utf-8 -*-
    import pandas as pd
    import pdfplumber
    page = pdfplumber.open(r"行业.pdf").pages
    hangye = []
    # 提取pdf每一页的表格
    for i in page:
        df = pd.DataFrame(i.extract_table()).fillna(method = 'ffill')  # 每一列nan值以上面不为nan的值填充
        hangye.append(df)
    # 将4388张提取的表格合并成一个表格
    industry = pd.concat(hangye,ignore_index = True)
    # 以合并后的表的第一行重新命名
    industry.columns = df.iloc[0]
    industry.drop(0,inplace = True)
    
    # index从1开始
    for i in industry.index:
        # 用any判断是否有些行的内容与列相同,如果True,则删去与列相同的行
        if any(industry.columns == industry.loc[i]) == True:
                industry.drop(i,inplace = True) 
    
    '''
    将已经更新的industry数据按照'行业大类代码'这一列分类聚合,
    然后按照size()找出每一个行业所包含上市公司数,最后按照行业代码原本的顺序排列,
    形成一个名为'industry_group_data'的Series.
    '''
    industry_group_data = industry.groupby('行业大类代码').size().sort_index()
    
    # 从industry_group_data找出所在行业上市公司数目大于10家的行业,并转换成DataFtame.
    more_than_10 = pd.DataFrame(industry_group_data[industry_group_data >= 10])
    
    # 更改原始列名
    more_than_10 = more_than_10.rename(columns = {0:'行业公司个数'})
    
    # 新建两列,将http://www.yyschools.com上的学号变为索引,与行业进行匹配.
    more_than_10['行业大类代码'] = more_than_10.index
    more_than_10['学号'] = list(range(1,len(more_than_10)+1))
    more_than_10 = more_than_10.set_index('学号')
    # 检索出证监会行业大类名称的划分
    ind_div = industry[['行业大类代码','行业大类名称']].drop_duplicates('行业大类代码')
    # 将 more_than_10与ind_div进行合并
    more_than_10 = pd.merge(more_than_10,ind_div,on = '行业大类代码')
    
    # 交互程序
    print('-------------------------')
    stu_code = input('请输入学生序号:')
    ind_code = more_than_10.loc[int(stu_code)-1]['行业大类代码']
    ind_name = more_than_10.loc[int(stu_code)-1]['行业大类名称']
    print('--------------------------------------------')
    print('编号为"{}"的学生,所在行业代码为"{}",即"{}"行业.'.format(stu_code,ind_code,ind_name))
    
    
    # 获取自己所在行业的股票代码和公司简称
    df = industry[industry['行业大类名称']==ind_name].head(10)
    df['行业大类代码'] = df['行业大类代码'].apply(lambda x: str(x))
    df.to_csv('股票代码.csv')
    print('--------------------------------------------')
    print('股票代码和公司简称表格已保存.')
    
    

结果展示如下:

结果截图
所属行业.png
返回目录