於心怡的实验报告📑

目录

Ⅰ.正式作业代码(点击跳转)

  1. 初始数据获取。利用selenium库通过模拟鼠标点击获取各公司年报披露界面,将年报列表的html文件保存到本地并解析为 dataframe方便后续调用。遍历下载链接下载所需年报。(查看代码)
  2. 财务数据提取。循环访问各公司各年年报,将pdf文件读成字符串形式并使用正则表达式匹配需要的“办公地址”、“公司网址”、“营业收入”和“基本每股收益”数据。 将各公司的数据保存到本地,便于后面画图。(查看代码)
  3. 由于公司数量超过十家,按近十年总收入为所有公司排序,取前十名进行绘图。(查看代码)

Ⅱ.实验结果解读(点击跳转)

Ⅲ.完整操作流程视频(点击跳转)

Ⅳ附录

一、实验中遇到的问题(点击跳转)
  1. 问题介绍及示例
  2. 利用目录定位页数,获取表格以提取财务数据的代码
二、实验心得(点击跳转)
三、实验数据(点击跳转)
更新了上交所官网爬虫和源码解析代码和实验心得、实验数据(2022/6/14)

一.正式作业代码💻


代码 PART 1

自动匹配被分配到的公司爬取年报并解析网页,根据解析结果下载年报

A.预备函数定义

i.根据学生姓名匹配行业公司,分离上交所与深交所股票

  # -*- coding: utf-8 -*-
  """
Created on Wed May 11 17:33:21 2022
@author: 於心怡、郭嘉懿、傅元娴
"""
  import pdfplumber
  import pandas as pd
  import re
  from selenium import webdriver
  from selenium.webdriver.common.by import By
  from selenium.webdriver.common.keys import Keys
  from selenium.webdriver.common.action_chains import ActionChains
  import time
  import os
  import requests
  from bs4 import BeautifulSoup
  #定义所需函数
  '''
  根据学生姓名匹配被分配到的行业深市上市的公司
  '''
  def InputStu():
      Names = str(input('请输入姓名:'))
      Namelist = Names.split()
      return Namelist

  def Match(Namelist,assignment):
          match = pd.DataFrame()
          for name in Namelist:
              match = pd.concat([match,assignment.loc[assignment['完成人']==name]])
          Number = match['行业'].tolist()
          return Number

  def Get_sz(data):
       sz=['200','300','301','00','080']   #深市股票代码A股是以000开头;深市B股代码是以200开头;中小板股票以002开头;
                                           #深市创业板的股票是以300、301开头(比如第一只创业板股票特锐德)。
                                           #另外新股申购代码以00开头、配股代码以080开头。
       lst = [ x for x in data for startcode in sz if x[3].startswith(startcode)==True ]
       df = pd.DataFrame(lst,columns=data[0]).iloc[:,1:]
       return df

  def Get_sh(data):  #上交所股票代码6开头
      lst = [ x for x in data if x[3].startswith('6')==True ]
      df = pd.DataFrame(lst,columns=data[0]).iloc[:,1:]
      return df

  def Getcompany(matched,df):
      df_final = pd.DataFrame()
      df_final = df.loc[df['行业大类代码']==matched[0]]
      return df_final

  def Clean(lst):    #把*ST前面的"*"去掉,否则文件保存的时候不方便,后面绘图的时候加上即可
      for i in range(len(lst)):
          lst[i] = lst[i].replace('*','')
      return lst

ii.深交所爬虫(上交所爬虫点击这里)

  
  '''
  利用selenium爬取所需公司年报
  '''
  #这里别忘了根据个人浏览器定义函数里的browser

  def InputTime(start,end): #找到时间输入窗口并输入时间
      START = browser.find_element(By.CLASS_NAME,'input-left')
      END = browser.find_element(By.CLASS_NAME,'input-right')
      START.send_keys(start)
      END.send_keys(end + Keys.RETURN)

  def SelectReport(kind):  #挑选报告的类别
      browser.find_element(By.LINK_TEXT,'请选择公告类别').click()
      if   kind == 1:
          browser.find_element(By.LINK_TEXT,'一季度报告').click()
      elif kind == 2:
          browser.find_element(By.LINK_TEXT,'半年报告').click()
      elif kind == 3:
          browser.find_element(By.LINK_TEXT,'三季度报告').click()
      elif kind == 4:
          browser.find_element(By.LINK_TEXT,'年度报告').click()

  def SearchCompany(name): #找到搜索框,通过股票简称查找对应公司的报告
      Searchbox = browser.find_element(By.ID, 'input_code') # Find the search box
      Searchbox.send_keys(name)
      time.sleep(0.2)
      Searchbox.send_keys(Keys.RETURN)

  def Clearicon():        #清除选中上个股票的历史记录
      browser.find_elements(By.CLASS_NAME,'icon-remove')[-1].click()

  def Clickonblank():     #点击空白
      ActionChains(browser).move_by_offset(200, 100).click().perform()

  def Save(filename,content):
      with open(filename+'.html','w',encoding='utf-8') as f:
          f.write(content)

iii.网页html解析到dataframe(深交所、巨潮资讯网、新浪财经,[上交所网页解析点击这里])

  
  '''
  解析html获取年报表格(代码来源于吴老师上课分享)
  '''
  class DisclosureTable():
      '''
      解析深交所定期报告页搜索表格
      '''
      def __init__(self, innerHTML):
          self.html = innerHTML
          self.prefix = 'https://disc.szse.cn/download'
          self.prefix_href = 'https://www.szse.cn/'
          # 获得证券的代码和公告时间
          p_a = re.compile('<a.*?>(.*?)</a>', re.DOTALL)
          p_span = re.compile('<span.*?>(.*?)</span>', re.DOTALL)
          self.get_code = lambda txt: p_a.search(txt).group(1).strip()
          self.get_time = lambda txt: p_span.search(txt).group(1).strip()
          # 将txt_to_df赋给self
          self.txt_to_df()

      def txt_to_df(self):
          # html table text to DataFrame
          html = self.html
          p = re.compile('<tr>(.*?)</tr>', re.DOTALL)
          trs = p.findall(html)

          p2 = re.compile('<td.*?>(.*?)</td>', re.DOTALL)
          tds = [p2.findall(tr) for tr in trs[1:]]
          df = pd.DataFrame({'证券代码': [td[0] for td in tds],
                             '简称': [td[1] for td in tds],
                             '公告标题': [td[2] for td in tds],
                             '公告时间': [td[3] for td in tds]})
          self.df_txt = df



      # 获得下载链接
      def get_link(self, txt):
          p_txt = '<a.*?attachpath="(.*?)".*?href="(.*?)".*?<span.*?>(.*?)</span>'
          p = re.compile(p_txt, re.DOTALL)
          matchObj = p.search(txt)
          attachpath = matchObj.group(1).strip()
          href       = matchObj.group(2).strip()
          title      = matchObj.group(3).strip()
          return([attachpath, href, title])

      def get_data(self):
          get_code = self.get_code
          get_time = self.get_time
          get_link = self.get_link
          #
          df = self.df_txt
          codes = [get_code(td) for td in df['证券代码']]
          short_names = [get_code(td) for td in df['简称']]
          ahts = [get_link(td) for td in df['公告标题']]
          times = [get_time(td) for td in df['公告时间']]
          #
          prefix = self.prefix
          prefix_href = self.prefix_href
          df = pd.DataFrame({'证券代码': codes,
                             '简称': short_names,
                             '公告标题': [aht[2] for aht in ahts],
                             'attachpath': [prefix + aht[0] for aht in ahts],
                             'href': [prefix_href + aht[1] for aht in ahts],
                             '公告时间': times
              })
          self.df_data = df
          return(df)

  #巨潮资讯网获取的html源码解析为dataframe格式
  def cninfo_to_dataframe(filename):
      f = open(filename+'.html',encoding='utf-8')
      html = f.read()
      f.close()
      soup = BeautifulSoup(html)
      links = soup.find_all('a') #找到所有a标签
      Code=[]
      Name=[]
      Title=[]
      href=[]
      Href=[]
      Text=[]
      p=re.compile('.*?&announcementId=(\d+).*?&announcementTime=(\d{4}-\d{2}-\d{2})')
      for link in links:
          Text.append(link.text) #获取a标签的文字
          Href.append(link.get('href')) #获取a标签的链接
      for n in range(0,len(Text),3):
          Code.append(Text[n]) #每一行第一个a标签的是代码,第二个是简称,第三个是标题
          Name.append(Text[n+1])
          Title.append(Text[n+2])
          num=re.findall(p,Href[n+2]) #在年报href中匹配attachpath需要的字段
          href.append('http://www.cninfo.com.cn/new/announcement/download?bulletinId='+str(num[0][0])+'&announceTime='+str(num[0][1]))
      df=pd.DataFrame({'代码':Code,
                       '简称':Name,
                       '公告标题':Title,
                       '链接':href,})
      return df

  #新浪财经获取的html源码解析为dataframe格式
  def sina_to_dataframe(name):
      f = open(name+'.html',encoding='utf-8')
      html = f.read()
      f.close()
      p_time=re.compile('(\d{4})(-\d{2})(-\d{2})')
      times=p_time.findall(html)
      y=[int(t[0]) for t in times]
      m=[t[0]+t[1].replace('0','') for t in times]
      d=[t[0]+t[1]+t[2] for t in times]
      soup = BeautifulSoup(html,features="html.parser")
      links = soup.find_all('a')
      href=[]
      Code=[]
      Name=[]
      Title=[]
      Href=[]
      Year=[]
      p_id=re.compile('&id=(\d+)')
      for link in links:
          Title.append(link.text.replace('*',''))
          href.append(link.get('href'))
      for n in range(0,len(Title)):
          Code.append(code)
          Name.append(name)
          matchedID=p_id.search(href[n]).group(1)
          Href.append('http://file.finance.sina.com.cn/211.154.219.97:9494/MRGG/CNSESH_STOCK/%s/%s/%s/%s.PDF'
                      %(str(y[n]),m[n],d[n],matchedID))
          Year.append(y[n-1])
      df=pd.DataFrame({'代码':Code,
                       '简称':Name,
                       '公告标题':Title,
                       '链接':Href,
                      '年份':Year})
      df=df[df['年份']>=2011]
      return df
      

iv.过滤解析完毕的dataframe(摘要,取消,英文型)

  '''
  过滤年报并下载文件
  '''
  def Readhtml(filename):
      with open(filename+'.html', encoding='utf-8') as f:
          html = f.read()
      return html

  def tidy(df):  #清除“摘要”型、“(已取消)”型、“英文版”型文件
      d = []
      for index, row in df.iterrows():
          ggbt = row[2]
          a = re.search("摘要|取消|英文", ggbt)
          if a != None:
              d.append(index)
      df1 = df.drop(d).reset_index(drop = True)
      return df1


  def Loadpdf(df):#用于下载文件
      d1 = {}
      for index, row in df.iterrows():
          d1[row[2]] = row[3]
      for key, value in d1.items():
          f = requests.get(value)
          with open (key+".pdf", "wb") as code:
              code.write(f.content)

B.整体操作代码

i.根据学生姓名匹配所分配的行业公司,并把两市上市公司分开。
  
  #操作代码
  '''
  第一步,根据学生姓名自动挑选出所分配行业的上市公司
  '''
  pdf = pdfplumber.open('industry.pdf') #打开行业分类表
  table = pdf.pages[0].extract_table() #这里由于我的公司在第一页,只读了第一页数据
                                       #(全部数据读起来要10+s,如果有其他学生数据提取的需要可以稍作修改变成读取全部)
  for i in range(len(table)):   #填充每行的行业大类代码
      if table[i][1] == None:
          table[i][1] = table[i-1][1]
  asign = pd.read_csv('001班行业安排表.csv',converters={'行业':str})[['行业','完成人']] #这里要注意把行业代码转为字符串,不然会失去开头的0
  Names = InputStu()  #输入学生姓名
  MatchedI = Match(Names,asign)  #匹配学生对应的行业
  sz = Get_sz(table)
  sh = Get_sh(table)
  df_sz = Getcompany(MatchedI,sz)  #获取所分配行业在深交所上市公司df
  df_sh = Getcompany(MatchedI,sh)  #获取所分配行业在上交所上市公司df
  Company = df_sz[['上市公司代码','上市公司简称']]
  Company2 = df_sh[['上市公司代码','上市公司简称']]
  My_Company=Company.append(Company2)
  My_Company.to_csv('company.csv',encoding='utf-8-sig') #将分配到的所有公司保存到本地文件

ii.爬取两市公司年报披露html表格(深市上市公司爬取深交所官网,沪市上市公司爬取新浪财经(此处示例),上交所官网爬虫点击这里)
  
  '''
  第二步爬取所需公司年报披露表
  '''
  print('\n(爬取网页中......)')
  browser = webdriver.Chrome()#这里别忘了根据个人浏览器选择
  browser.get('https://www.szse.cn/disclosure/listed/fixed/index.html')
  End = time.strftime('%Y-%m-%d', time.localtime())
  InputTime('2012-01-01',End)

  SelectReport(4) # 调用函数,选择“年度报告”
  Clickonblank()

  #在深交所官网爬取深交所上市公司年报链接
  for index,row in Company.iterrows():
      code = row[0]
      name = row[1].replace('*','')
      SearchCompany(code)
      time.sleep(0.5) # 延迟执行0.5秒,等待网页加载
      html = browser.find_element(By.ID, 'disclosure-table')
      innerHTML = html.get_attribute('innerHTML')
      Save(name,innerHTML)
      Clearicon()

  #在新浪财经爬取上交所上市公司年报下载链接(注:更新了上交所官网爬虫和页面解析,可以在下一个代码块中查看)
  for index,row in Company2.iterrows():
     code = row[0]
     name = row[1].reaplce('*','')
     browser.get('https://vip.stock.finance.sina.com.cn/corp/go.php/vCB_Bulletin/stockid/%s/page_type/ndbg.phtml'%code)
     html = browser.find_element(By.CLASS_NAME, 'datelist')
     innerHTML = html.get_attribute('innerHTML')
     Save(name,innerHTML)
  browser.quit()

iii.对公司年报披露表格网页进行解析和下载
  
  '''
  第三步,解析html获取年报表格存储到本地并下载年报文件
  '''
  print('\n【开始保存年报】')
  print('正在下载深交所上市公司年报')
  i = 0
  for index,row in Company.iterrows():   #下载在深交所上市的公司的年报
      i+=1
      name = row[1].replace('*','')
      html = Readhtml(name)
      dt = DisclosureTable(html)
      df = dt.get_data()
      df1 = tidy(df)
      df1.to_csv(name+'.csv',encoding='utf-8-sig')
      os.makedirs(name,exist_ok=True)#创建用于放置下载文件的子文件夹
      os.chdir(name)
      Loadpdf(df1)
      print(name+'年报已保存完毕。共',len(Company),'所公司,当前第',i,'所。')
      os.chdir('../') #将当前工作目录爬到父文件夹,防止下一次循环找不到html文件

  print('正在下载上交所上市公司年报')
  j=0
  for index,row in Company2.iterrows():
      j+=1
      name= row[1].replace('*','')
      html = Readhtml(name)
      df = sina_to_dataframe(name) #这里的代码还可以换成cninfo_to_dataframe(name) 或 sse_to_dataframe(name),取决于网页源码的来源网站
      df1 = tidy(df)
      df1.to_csv(name+'.csv',encoding='utf-8-sig')
      os.makedirs(name,exist_ok=True)#创建用于放置下载文件的子文件夹
      os.chdir(name)
      Loadpdf(df1)
      print(name+'年报已保存完毕。共',len(Company2),'所公司,当前第',j,'所。')
      os.chdir('../') #将当前工作目录爬到父文件夹,防止下一次循环找不到html文件

注:由于上交所官网单次最高查询跨度为3年,(巨潮资讯网不会爬)故选择爬取新浪财经网。但是本代码对于新浪财经的解析不能准确获取公司 名称的变动,并且极个别年报没有下载链接,当需要获取的公司有多个在上交所上市时适合使用,如果该行业上交所上市公司不多,可以去巨潮资讯网手动copy年报列表链接的html 代码,并将代码中"df = sina_to_dataframe(name)"改为"df = cninfo_to_dataframe(name)"即可更精确地下载年报。

更新:后经过和郭嘉懿讨论发现上交所输入股票代码再点报告类型可以检索出全部报告,就可以不受手动选时间跨度三年的限制。下面附上上交所爬虫代码 以及网页解析代码。

上交所爬虫

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

def get_sse(code):
    browser = webdriver.Chrome()#这里别忘了根据个人浏览器选择
    browser.get('http://www.sse.com.cn/disclosure/listedinfo/regular/')
    searchbox=browser.find_element(By.ID,'inputCode')
    searchbox.send_keys(code)
    time.sleep(1)
    selectlist = browser.find_element(By.CSS_SELECTOR, ".sse_outerItem:nth-child(4) .filter-option-inner-inner")
    selectlist.click()
    annual = browser.find_element(By.LINK_TEXT,'年报')
    annual.click()
    time.sleep(1)
    html = browser.find_element(By.CLASS_NAME, 'table-responsive')
    innerHTML = html.get_attribute('innerHTML')
    searchbox.clear()
    return innerHTML
#df为之前存储的上交所公司股票代码和简称的dataframe
for index,row in df.iterrows(): #这里为了代码简便爬虫部分用的自定义函数,但是这样爬起来比较慢(因为每爬一个公司就要开关浏览器)
    code=row[0]                 #如果上交所公司数量大可以适当修改代码把爬虫的内容写到循环里,避免重复开关浏览器。
    name=row[1]
    filename = name.replace('*','')
    html = get_sse(code)
    Save(filename,html) #Save()函数在之前已经定义过

Save()函数
上交所网页解析代码(函数定义法和面向对象编程两种都可以选择)
  
    
import re
import pandas as pd

def sse_to_dataframe(filename):
    f = open(filename+'.html',encoding='utf-8')
    html = f.read()
    f.close()
    p_row=re.compile('<tr>(.*?)</tr>',re.DOTALL)
    trs=p_row.findall(html)
    p_data=re.compile('<td.*?>(.*?)</td>',re.DOTALL)
    tds=[p_data.findall(t) for t in trs if p_data.findall(t)!=[]]
    p_code=re.compile('<span>(\d{6})</span>')
    p_name=re.compile('<span>(\w+|-)</span>')
    p_href=re.compile('<a.*?href="(.*?.pdf)".*?>')
    p_title=re.compile('<a.*?>(.*?)</a>')
    codes=[p_code.search(td[0]).group(1) for td in tds]
    names=[p_name.search(td[1]).group(1) for td in tds]
    links=[p_href.search(td[2]).group(1) for td in tds]
    titles=[td[3][:4]+p_title.search(td[2]).group(1) for td in tds] #早年有的公司年报标题每年都一样,前面加一个发布年份以区分
    pubtime=[td[3] for td in tds]
    data=pd.DataFrame({'证券代码':codes,
                       '股票简称':names,
                       '公告标题':titles,
                       '公告链接':links,
                       '发布时间':pubtime})
    for index,row in data.iterrows():
        title=row[2]
        time=row[-1][:4]
        if ("年度报告" not in title and "年报" not in title) or (int(time))<2012:
            data=data.drop(index=index)
    return(data)

class DisclosureTable_sh():
    '''
    解析深交所定期报告页搜索表格
    '''
    def __init__(self, innerHTML):
        self.html = innerHTML
        self.prefix = 'http://www.sse.com.cn'
        p_code=re.compile('<span>(\d{6})</span>')
        p_name=re.compile('<span>(\w+|-)</span>')
        p_href=re.compile('<a.*?href="(.*?.pdf)".*?>')
        p_title=re.compile('<a.*?>(.*?)</a>')
        self.get_code = lambda td: p_code.search(td).group(1)
        self.get_name = lambda td: p_name.search(td).group(1)
        self.get_href = lambda td: p_href.search(td).group(1)
        self.get_title = lambda td: p_title.search(td).group(1)
        self.txt_to_df() #调用txt_to_df(self),得到初始化dataframe用于后续匹配

    def txt_to_df(self):
        # html table text to DataFrame
        html = self.html
        p_tr = re.compile('<tr>(.*?)</tr>', re.DOTALL)
        trs = p_tr.findall(html)
        p_td = re.compile('<td.*?>(.*?)</td>', re.DOTALL)
        tds=[p_td.findall(td) for td in trs if p_td.findall(td)!=[]]
        df = pd.DataFrame({'证券代码': [td[0] for td in tds],
                           '股票简称': [td[1] for td in tds],
                           '公告标题和链接': [td[2] for td in tds],
                           '公告时间': [td[3] for td in tds]})
        self.df_txt=df

    def get_data(self):
        get_code  = self.get_code
        get_name  = self.get_name
        get_href  = self.get_href
        get_title = self.get_title

        df = self.df_txt
        prefix = self.prefix
        codes   = [get_code(td) for td in df['证券代码']]
        names   = [get_name(td) for td in df['股票简称']]
        links   = [prefix+get_href(td) for td in df['公告标题和链接']]
        titles  = [td[3][:4]+get_title(td) for td in df['公告标题和链接']]
        pubtime = [td for td in df['公告时间']]
        data = pd.DataFrame({'证券代码':codes,
                           '股票简称':names,
                           '公告标题':titles,
                           '公告链接':links,
                           '公告时间':pubtime})
        for index,row in data.iterrows():
            title = row[2]
            time = int(row[-1][:4])
              if "年度报告" not in title and "年报" not in title:
                data=data.drop(index=index)
              if time<2011:
                data_latest10=data.drop(index=index)
        self.df_alldata = data
        self.df_data = data_latest10
        return data_latest10
      
      
    

面向对象的编程方式更结构化,可以构建多个关于源文件的可调用数据,有机会要好好学习(又是一个flag)
使用的时候用dt=DisclosureTable_sh(html); df=dt.get_data()替代sse_to_dataframe/sina_to_dataframe/cninfo_to_dataframe(html)


几个年报披露网页的个人总结📌
网页🔗 页面复杂度 数据质量 爬取方式 评价
深交所官网 中等 selenium模仿点击 适合学习爬虫初期进行练习,可以通过检查功能找到所有需要点击的目标
上交所官网 中等偏高 中等(筛选年报有时也会出现不是年报的文件,早年的数据股票代码和简称缺失,标题严重重复,多个年份报告名仅为“公司简称+年报” 储存文件需要对报告标题进行修改) selenium模仿点击 纯手动检查难以爬取,最好借助一定工具,容易报错
巨潮资讯网 复杂 很高(所有上市公司年报都可以搜索到) 暂时不会,选择年报类型的地方每次刷新id前有一个随机数 爬取难度大(我菜),但是数据很全
新浪财经 简单 一般(有的公司年份不全、有的年份没有PDF文件,下载链接个别公司年份会变化) 改变网页链接去到不同公司年报汇总独立页面,观察链接格式编写下载链接 最好爬(新浪的传统特点),改变股票代码即可去到不同公司的页面,缺点是数据质量不高

结果:

爬取下来的源码(html格式),解析成dataframe的表格(csv格式),自动下载的年报(pdf格式)


各公司查询近十年年报结果源码

结果截图
结果截图

解析后得到的表格

结果截图
结果截图

通过访问表格"attachpath"栏自动下载的年报

结果截图
结果截图

保存下来的所有公司名文件


结果截图

代码 PART 2

提取“营业收入(元)”、“基本每股收益(元 ╱ 股)”

“股票简称”、“股票代码”、“办公地址”、“公司网址”


  Created on Tue May 24 12:24:18 2022
  """
  @author: Napstablook
  """

  import pandas as pd
  import fitz
  import re

  Company=pd.read_csv('company.csv').iloc[:,1:] #读取上一步保存的公司名文件并转为列表
  company=Company.iloc[:,1].tolist()
  t=0
  for com in company:
      t+=1
      com = com.replace('*','')
      df = pd.read_csv(com+'.csv',converters={'证券代码':str})  #读取存有公告名称的csv文件用来循环访问pdf年报
      df = df.sort_index(ascending=False)
      final = pd.DataFrame(index=range(2011,2022),columns=['营业收入(元)','基本每股收益(元/股)']) #创建一个空的dataframe用于后面保存数据
      final.index.name='年份'
      code = str(df.iloc[0,1])
      name = df.iloc[-1,2].replace(' ','')

      for i in range(len(df)): #循环访问每年的年报
          title=df.iloc[i,3]
          doc = fitz.open('./%s/%s.pdf'%(com,title))
          text=''
          for j in range(15): #读取每份年报前15页的数据(一般财务指标读15页就够了,全部读取的话会比较耗时间)
              page = doc[j]
              text += page.get_text()
          p_year=re.compile('.*?(\d{4}) .*?年度报告.*?') #捕获目前在匹配的年报年份
          year = int(p_year.findall(text)[0])
          #设置需要匹配的四种数据的pattern
          p_rev = re.compile('(?<=\n)营业总?收入(?\w?)?\s?\n?([\d+,.]*)\s\n?')
          p_eps = re.compile('(?<=\n)基本每股收益(元/?/?\n?股)\s?\n?([-\d+,.]*)\s?\n?')
          p_site = re.compile('(?<=\n)\w*办公地址:?\s?\n?(.*?)\s?(?=\n)',re.DOTALL)
          p_web =re.compile('(?<=\n)公司\w*网址:?\s?\n?([a-zA-Z./:]*)\s?(?=\n)',re.DOTALL)

          revenue=float(p_rev.search(text).group(1).replace(',',''))    #将匹配到的营业收入的千分位去掉并转为浮点数

          if year>2011:
           pre_rev=final.loc[year-1,'营业收入(元)']
           if pre_rev/revenue>2:
               print('%s%s营业收入下跌超过百分之50,可能出现问题,请手动查看'%(com,title))

          eps=p_eps.search(text).group(1)
          final.loc[year,'营业收入(元)']=revenue  #把营业收入和每股收益写进最开始创建的dataframe
          final.loc[year,'基本每股收益(元/股)']=eps

      final.to_csv('【%s】.csv' %com,encoding='utf-8-sig')  #将各公司数据存储到本地测csv文件

      site=p_site.search(text).group(1) #匹配办公地址和网址(由于取最近一年的,所以只要匹配一次不用循环匹配)
      web=p_web.search(text).group(1)

      with open('【%s】.csv'%com,'a',encoding='utf-8-sig') as f:  #把股票简称,代码,办公地址和网址写入文件末尾
          content='股票简称,%s\n股票代码,%s\n办公地址,%s\n公司网址,%s'%(name,code,site,web)
          f.write(content)
      print(name+'数据已保存完毕'+'(',t,'/',len(company),')')

结果:存到本地的数据文件(csv格式)


所有公司的历年营业收入,每股收益数据和公司简称,股票代码,办公地址以及网址

结果截图
结果截图

注:

使用fitz读取民和股份2013年年度报告时出现问题,表格的数据顺序错误。
故民和股份2013年数据为手动添加

错误细节

结果截图 结果截图

代码 PART 3

营业收入前十的公司绘制“营业收入(元)”、“基本每股收益(元 ╱ 股)”随时间变化趋势图

按每一年度,对该行业内公司“营业收入(元)”、“基本每股收益(元 ╱ 股)”绘制对比图


绘图代码使用jupyter notebook编写,请点击链接查看网页

此处仅放上绘图结果

绘图结果

畜牧业营业收入排名前十的公司的“营业收入(元)”随时间变化趋势图📈

结果截图

畜牧业营业收入排名前十的公司的“基本每股收益(元 ╱ 股)”随时间变化趋势图”📉

结果截图

按每一年度,畜牧业上市公司“营业收入(元)”对比图📊

结果截图
结果截图
结果截图

按每一年度,畜牧业上市公司“基本每股收益(元 ╱ 股)”对比图📊

结果截图
结果截图
结果截图

Ⅱ.实验结果解读📝


Ⅲ.完整操作流程视频🎬(上传于6.1所以沪市示例的爬虫是新浪财经)

整体操作衔接视频。建议放大浏览器到150%-175%收获全屏观看以防字看不清。😆
操作流程视频

Ⅳ.附录

一.实验中遇到的问题☕

本次实验一开始对于匹配公司历年数据采取的方法是先匹配每份年报的目录,根据目录定位财务指标所在章节,然后用pdfplumber提取对应页面的表格然后根据关键字匹配数据。 但是这种方法在实践中遇到了诸多困难:

  • 总有一些公司的年报不是标准的,“公司简介和主要财务指标”,“会计数据和业务数据摘要”这两种外,还发现有的公司的章节名叫“会计数据和财务指 标摘要”(如天邦股份2011年年报)
  • 有的公司年报目录使用了加粗字体,这样读到内存里是一个字节重复四次,无法成功匹配目录。而绝大部分公司年报在页眉页脚处没有当前所处章节, 只有“XX公司XXXX年年度报告”字样,不适用上课讲的通过获取每页所处章节再定位的方法。
  • 最重要的一点:在匹配目录的过程中,发现有大量的年报目录标示的页码和正文内容对不上。目录上现实的页码其实都是下一节的页码。每一家公司基本都会出现这种情况, 同一家公司有的年报页码对,有的年报页码错,没有规律。暂时怀疑是公司选择了不同的页码对应标准导致的。
  • 错误示例:页码直接错误,目录写“会计数据和主要财务指标摘要”在第2页,实际在第9页。 错误示例:页码是下一节的内容。目录写第二节“公司简介和主要财务指标”在第12页,实际上第12页是第三节
  • 最后,有的公司表格内有换行,这样pdfplumber读取表格的时候就无法直接对应上数据,导致提取失败。
  • 例如:下面这样的表格通过pdfplumber提取出来对应“营业收入”一行的第一个数据是“25.26%” 虽然这些问题在你有一双勤奋的双手和充足的时间时都不算事儿——遇到个性的标题就取布尔值并集然后用df.loc筛选;目录加粗了我就手动把目录删掉再添一个不加粗的。页码不对我就看运行报错的公司年份 修改页码后再继续运行,到后面我发现报错的频率实在是太高了,然后就每处理一个公司前先打开他所有的年报文件,一年一年改正页码再跑。 对于pdfplumber在表格换行时匹配不到准确的数据,后来我发现这样匹配的都是距上年变动的百分比,于是就用上一年度数据进行计算得到本年数据。
    经历了漫长的改目录时间,我发现收入和每股收益的数据虽然都获取到了,但是有个别公司的办公地址和网址还是没有办法完美地匹配。因为有的公司的年报里办公地址 还会写其审计单位的办公地址,当表格不标准的时候,问题各式各样。虽然这些问题都被解决了,但是至此我认为这种方式已经背离了当初为了快捷选择 pdfplumber.extract_tables()的初衷。 于是选择了最直接的方法写正则表达式来匹配各种数据。这样运行速度也变快了,心也没那么累了,遇到问题调整正则表达式即可,而不是面对千奇百怪的问题。

    代码:如果遇到不是用加粗字编写目录和用下一节页码编(拿脚编)目录页码的公司的话,还是可以用的!

    
    import pandas as pd
    import pdfplumber
    import fitz
    import re
    
    Company=pd.read_csv('company.csv').iloc[:,1:]
    company=Company.iloc[:,0].tolist()
    t=0
    for com in company:
      t+=1
      df = pd.read_csv(com+'.csv',converters={'证券代码':str})
      df = df.sort_index(ascending=False)
      final = pd.DataFrame(index=range(2011,2022),columns=['营业收入(元)','基本每股收益(元/股)'])
      final.index.name='年份'
      code = str(df.iloc[0,1])
      name = df.iloc[-1,2]
      name=name.replace('*','').replace(' ','')
    
      for i in range(len(df)):
          title=df.iloc[i,3]
          doc = fitz.open('./%s/%s.pdf'%(com,title))
          text=''
          for j in range(8):
              page = doc[j]
              text += page.get_text()
          p_year=re.compile('(\d{4}) .*?年度报告',re.DOTALL)
          p=re.compile(r'(?<=\n)(第?\w{1,2}、?节?章?)\s*([\w、]*).*?(\d+).*?(?=\n)'
                       ,re.DOTALL)
          result = p.findall(text)
          year = int(p_year.findall(text)[0])
          catalog = pd.DataFrame({'节数':[t[0] for t in result ],
                                  '标题':[t[1] for t in result],
                                  '页码':[t[2] for t in result]})
          pdf =pdfplumber.open('./%s/%s.pdf'%(com,title))
    
          A='公司简介和主要财务指标'
          B='会计数据和业务数据摘要'
          C='会计数据和财务指标摘要'
          a=catalog['标题']==A
          b=catalog['标题']==B
          c=catalog['标题']==C
          page=int(catalog.loc[a|b|c].iloc[0,2])
          lst=[]
          lst1=[]
          data=[]
          Data=[]
          Search=['营业收入','营业收入(元)','营业总收入','营业总收入(元)'
                  ,'基本每股收益(元/股)','基本每股收益(元/股)','基本每股收益(元/']
          Search1=['办公地址','公司办公地址','公司网址','公司国际互联网网址',]
          if year==2011 :
             data=''
             for x in range(page-1,page+2):
                      data =data+pdf.pages[x].extract_text()
             p_rev = re.compile('(?<=\n)(营业)总?收入(?\w?)?\s*([-\d+,.]*).*?')
             p_eps = re.compile('(?<=\n)(基本每股收益(元/*/?股))\s*([-\d+,.]*)\s*')
             lst = [p_rev.findall(data)[0],p_eps.findall(data)[0]]
          elif year>=2015: #2015年及以后,当前节包含公司简介,多获取一页
             for z in range(page-1,page+2):
                     data =data+pdf.pages[z].extract_tables()
          else:
             for y in range(page-1,page+1):
                     data =data+pdf.pages[y].extract_tables()
    
    
          for i in range(len(data)):
              Data +=data[i]
    
          for row in Data:
              row=[i for i in row if (i!='')&(i!=None)]
              row=[i.replace('\n','') for i in row]
              if row==[]:
                  continue
              elif row[0] in Search:
                  lst.append(row)
              elif row[0] in Search1:
                  lst1.append(row)
    
          if len(lst)>4:
              del lst[4:]
    
          revenue=lst[0][1]
          if len(revenue)<=7:
              revenue=(float(revenue.strip('%'))/100+1)*float(final.loc[year-1,'营业收入(元)'])
              revenue=str(round(revenue,2))
          else:
              revenue=float(revenue.replace(',',''))
    
          eps=lst[1][1]
          final.loc[year,'营业收入(元)']=revenue
          final.loc[year,'基本每股收益(元/股)']=eps
    
      final.to_csv('【%s】.csv' %name,encoding='utf-8-sig')
    
      site=lst1[0][1]
      p=re.compile('[a-zA-Z./*]+')
      WEB=[ p.findall(item[1]) for item in lst1]
      WEB=[ t for t in WEB if t!=[]]
      web=re.sub("\[|'|\]",'',str([t for t in WEB if len(str(t))>=10]))
      web=web.replace(', ','.')
    
      with open('【%s】.csv'%name,'a',encoding='utf-8-sig') as f:
          content='股票简称,%s\n股票代码,%s\n办公地址,%s\n公司网址,%s'%(name,code,site,web)
          f.write(content)
      print(name+'数据已保存完毕'+'(',t,'/',len(company),')')
    
    

    二.实验心得💡

    第一次系统地用python完成较为复杂的任务,涉及数据筛选,爬虫,绘图等内容。尤其强化了正则表达式和爬虫的学习, 收获颇丰,也领会了这两个工具的实用性。须知代码还是得常敲常新,遇到困难善用搜索,提高自学能力。编程中遇到问题可以试着 转变思路换方法,不要在一条路上死磕,提高自己的思维能力,不能所有问题都想着手动调整数据解决。
    感谢吴老师的悉心教导,本学期的课程内容框架清晰,内容严谨丰富,让我系统地学习了python对于数据处理的知识。特别是扩展的关于工作目录,环境变量的知识,解决了之前学习python 时令人疑惑的问题。 布置的几次作业难度递进,衔接流畅,能很好地锻炼到所学知识。希望未来有更多锻炼代码的机会,不负所学,探索python的更多用法。

    三.实验数据📁

    实验数据(python代码,html源码,html解析csv,年报数据获取csv,没有年报文件,压缩包要300M,就不浪费老师服务器空间了)

    代码中年报爬取下载部分为作业三小组共同完成,其他部分代码为独立完成。
    报告很长,感谢你看到这!初版代码上传时间较早,目前更新了部分内容,如发现问题/需探讨交流欢迎联系我😼


    🏁---------------FIN---------------🏁