Py学习  »  Python

Python:基于selenium爬取科创板审核问询

连享会 • 1 年前 • 227 次点击  

👇 连享会 · 推文导航 | www.lianxh.cn

连享会课程 · 基于机器学习的因果推断方法

作者:范思妤 (南京大学)
邮箱:fansiyu@smail.nju.edu.cn

温馨提示: 文中链接在微信中无法生效。请点击底部「阅读原文」。或直接长按/扫描如下二维码,直达原文:


目录

  • 1. 科创板审核问询披露

  • 2. 爬虫实战

    • 2.1 安装 selenium 及浏览器驱动

    • 2.2 浏览器驱动配置使用

    • 2.3 分析网页结构

    • 2.4 获取 url 列表

    • 2.5 遍历 url 列表爬取审核问询披露

  • 3. 相关推文



1. 科创板审核问询披露

科创板试点的注册制改革,强调“以信息披露为核心”。为在发行上市审核坚持以信息披露为核心,把好上市企业入口质量关,上交所主要采用公开化问询式审核方式,即交易所提出问询的问题,发行人对这些问题进行回复和说明,中介机构对问询事项的核查过程和结论,以“一问一答”的方式及时向市场公开。

本文重点关注如何通过 Python 的 selenium 库爬取上述审核问询流程披露的原始文件,并在发行公司层面对问询与回复情况进行汇总统计。

2. 爬虫实战

2.1 安装 selenium 及浏览器驱动

在本文中,我们使用 Python 中的 selenium 库对网页进行爬取。selenium 通过创建模拟浏览器的方式进行爬取,可以完全模拟真实用户的动态操作。在配置好 Python 环境后,打开命令提示符,并键入如下命令安装 selenium 库:

pip install selenium

或者使用国内镜像源安装 selenium 库:

pip install selenium -i http://pypi.douban.com/simple/ --trusted-host  pypi.douban.com

接下来,我们需要安装浏览器驱动。此处我们主要讲解 Windows 系统安装 chrome 浏览器驱动的步骤和方法。其他系统及其他浏览器驱动的安装,请自行网络搜索相关教程。

首先,我们需要确定 chrome 浏览器版本。打开 chrome 浏览器,在新标签页输入 chrome://settings/help 进入设置界面,查看目前的版本信息。

在获取浏览器版本信息后,需下载对应版本的浏览器驱动。打开 chrome 浏览器 驱动下载地址,找到和目前版本最接近的驱动,Windows 系统用户需下载 win32 版本。

解压驱动文件包得到 chromedriver.exe,保存到指定路径,并务必将当前路径添加到环境变量中(我的电脑 右键属性 高级系统设置 高级 环境变量 系统变量 Path)。

2.2 浏览器驱动配置使用

我们使用如下代码创建浏览器驱动对象。此外,我们可以根据爬虫目标网页调整浏览器选项,提供稳定运行环境。

from selenium import webdriver
from selenium.webdriver import ChromeOptions

CHROME_OPTIONS = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}   # 1代表显示图片,2代表不显示图片
CHROME_OPTIONS.add_experimental_option("prefs", prefs)
CHROME_OPTIONS.add_experimental_option("excludeSwitches",["enable-automation"]) 
CHROME_OPTIONS.add_experimental_option("useAutomationExtension"False)
CHROME_OPTIONS.add_argument('--disable-blink-features=AutomationControlled')
CHROME_DRIVER = r'D:/chromedriver/chromedriver.exe'  # 此处为chromedriver.exe所在路径

# 声明浏览器
driver = webdriver.Chrome(executable_path=CHROME_DRIVER, options=CHROME_OPTIONS)  

2.3 分析网页结构

首先,我们需要浏览待爬取的页面结构,并检查网页确定网页类型。

上交所科创板股票审核页面如下。最上方一行分类列示已递交申请的发行人的状态,我们的样本仅爬取已经有注册结果的发行人(即已经走完整个科创板审核问询流程。

所有发行人按照时间由近及远的顺序列示在页面表格中。每行发行人全称对应一个超链接,点击即进入发行人审核问询的详情页面。因此,我们整体的爬虫思路如下:

  • 获取全部发行人对应的链接列表;
  • 遍历目标链接列表爬取某一页面的特定信息,并储存到本地。

2.4 获取 url 列表

如上所述,我们仅爬取已经有注册结果的发行人。因此,首先需要用 selenium 自动化模拟鼠标点击操作,以点击页面上方的“注册结果”,获取跳转后的页面上的信息。

# 导入所需要的包
from selenium.webdriver.common.by import By
import time

# 请求页面
url = "http://kcb.sse.com.cn/renewal/"
driver.get(url)           

# 通过Xpath定位到“注册结果”,并点击,等待页面加载
driver.find_element(By.XPATH,'//*[@id="select5"]/a/span[2]').click()
time.sleep(3)

通过加载后的检查页面,我们发现 url 就包含在发行人全称的 href 属性中。我们进行如下步骤的操作以获取完整的发行人 url 列表:

  • 定位发行人全称,并提取元素的 href 属性值;
  • 定位“下一页”按钮,模拟鼠标操作进行翻页;
  • 重复以上两步,直至最后一页。

selenium 提供了多种元素定位方式,如 xpath,id,name 等。在本文中,我们用 Xpath 定位元素。具体步骤如下图,得到对应元素的 Xpath 后,通过语法 @href 选取元素属性。

代码如下:

# Xpath定位元素
all_url_list = []
url_xpath = '//*[@id="dataList1_container"]/tbody/tr/td[2]/a/@href'
next_page = '//*[@id="dataList1_container_next"]/span'

# 获取发行人url列表
for i in range(1,28):        # 最大页数是28
    print("正在抓取第%s页的内容" %i)
    html = driver.page_source
    tree = etree.HTML(html)
    url_list = tree.xpath(url_xpath)
    all_url_list.extend(url_list)
    time.sleep(1)
    if i<=26 : 
        # 点击下一页
        driver.find_element(By.XPATH,next_page).click()   
        time.sleep(2)        # 等待页面加载
    else:         
        pass                 # 最后一页没有“下一页”
driver.quit()

# 打印最后10个url,检查是否所有url爬取成功
for url in all_url_list[-10:]:
    print(url)

2.5 遍历 url 列表爬取审核问询披露

在获取了全部发行人 url 列表之后,我们需要遍历 url 列表,获取特定发行人审核问询的详情页面。在这一页面,我们需要爬取的关键内容如下:

发行人基本信息:

  • 公司全称(fullname)
  • 公司简称(shortname)
  • 保荐机构(broker)
  • 会计师事务所(auditor)
  • 律师事务所(lawyer)
  • 受理日期(acceptdate)
  • 首次问询日期(inquirydate)
  • 上市委会议通过日期(passdate)

审核问询披露文件:

  • 发行人及保荐机构回复意见
  • 会计师回复意见
  • 法律意见书

代码如下:


#————爬取每个url对应的科创板发行上市公司页面————#

# 导入所需的包
import time
from lxml import etree
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
import requests
import os
from urllib.request import urlretrieve

# 在路径下创建文件夹
os.mkdir(r'./companies')

# 浏览器驱动配置及使用
CHROME_OPTIONS = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}   # 1代表显示图片,2代表不显示图片
CHROME_OPTIONS.add_experimental_option("prefs", prefs)
CHROME_DRIVER = r'D:/chromedriver/chromedriver.exe'
driver = webdriver.Chrome(executable_path=CHROME_DRIVER, options=CHROME_OPTIONS)

# 发行人基本信息的Xpath定位
fullname_xpath        = '//*[@id="issuer_full"]'
shortname_xpath       = '//*[@id="issuer_sec"]'
broker_xpath          = '//*[@id="sponsor_org"]/a'
auditor_xpath         = '//*[@id="accounts_org"]/a'
lawyer_xpath          = '//*[@id="law_firm"]/a'
acceptdate_xpath      = '//*[@id="step1F"]/div'
inquirydate_xpath     = '//*[@id="step2F"]/div'
passdate_xpath        = '//*[@id="step3F"]/div'

# 注册制审核问询披露文件的Xpath定位
SECLetter_xpath       = '//*[@id="yjhf"]/tbody/tr/td[2]/a'

# 创建空列表以盛放后续爬虫信息
all_fullname    = []
all_shortname   = []
all_broker      = []
all_auditor     = []
all_lawyer      = []
all_acceptdate  = []
all_inquirydate = []
all_passdate    = []
all_num_letter1 = []
all_num_letter2 = []
all_num_letter3 = []

for url_item in all_url_list:
    url = 'http://kcb.sse.com.cn' + url_item
    driver.get(url) 
    time.sleep(2)
    html = driver.page_source
    tree = etree.HTML(html)
    
    # 获取发行人基本信息的文本节点
    fullname_list    = tree.xpath(fullname_xpath + '/text()')
    all_fullname.append(fullname_list[0])

    shortname_list   = tree.xpath(shortname_xpath+ '/text()')
    all_shortname.append(shortname_list[0])
    
    broker_list      = tree.xpath(broker_xpath+ '/text()')
    content          = ';'.join(broker_list)  # 可能会有多个保荐机构,用分号链接,下同
    all_broker.append(content)
    
    auditor_list     = tree.xpath(auditor_xpath+ '/text()')
    content          = ';'.join(auditor_list)
    all_auditor.append(content)
    
    lawyer_list      = tree.xpath(lawyer_xpath+ '/text()')
    content          = ';'.join(lawyer_list)
    all_lawyer.append(content)
    
    acceptdate_list  = tree.xpath(acceptdate_xpath+ '/text()')
    all_acceptdate.append(acceptdate_list[0])
    
    inquirydate_list = tree.xpath(inquirydate_xpath+ '/text()')
    all_inquirydate.append(inquirydate_list[0])
    
    passdate_list    = tree.xpath(passdate_xpath+ '/text()')
    all_passdate.append(passdate_list[0])

    # 获取问询与回复信息的文本节点
    SECLetter_list   = tree.xpath(SECLetter_xpath  + '/text()')
    # 获取问询与回复的href属性,为后续下载对应文件pdf做准备
    SECLetter_url    = tree.xpath(SECLetter_xpath  + '/@href')
    
    # 为每一公司创建子文件夹,用以盛放分类后的文件pdf
    os.mkdir('./companies/'+fullname_list[0])
    os.mkdir('./companies/'+fullname_list[0] + '/问询函和回函/')
    os.mkdir('./companies/'+fullname_list[0] + '/审计意见/')
    os.mkdir('./companies/'+fullname_list[0] + '/法律意见/')
    os.mkdir('./companies/'+fullname_list[0] + '/其他/')
    
    # 注册制审核问询披露文件的计数算子
    count_letter1 = 0  
    count_letter2 = 0
    count_letter3 = 0
    
    #获取pdf文件方法1,用requests.get:
    for i in range(len(SECLetter_list)):
        url = SECLetter_url[i]
        r = requests.get('http:'+ url)

        # 通过文件名称对文件进行分类,并计数
        if  ('发行人'in SECLetter_list[i]) or ('落实函' in SECLetter_list[i])           \
            and  ('会计' not in SECLetter_list[i]) and ('律师'not in SECLetter_list[i]) \
            and ('法律' not in SECLetter_list[i]):
            with open('./companies/'+fullname_list[0] + '/问询函和回函/'+SECLetter_list[i]+'.pdf''wb+'as f:
                f.write(r.content)
                count_letter1 += 1
        elif  '会计' in SECLetter_list[i]:  
            with open('./companies/'+fullname_list[0] + '/审计意见/'+SECLetter_list[i]+'.pdf''wb+'as f:
                f.write(r.content)
                count_letter2 += 1
        elif ('律师'in SECLetter_list[i]) or ('法律' in SECLetter_list[i]):
            with open('./companies/'+fullname_list[0] + '/法律意见/'+SECLetter_list[i]+'.pdf''wb+'as f:
                f.write(r.content)
                count_letter3 += 1
        else :
            with open('./companies/'+fullname_list[0] + '/其他/'+SECLetter_list[i]+'.pdf''wb+'as f:
                f.write(r.content)
    
    all_num_letter1.append(count_letter1)
    all_num_letter2.append(count_letter2)
    all_num_letter3.append(count_letter3)
        
    '''
    # 获取pdf文件方法2,用urlretrieve:
    # 以下代码仅做方法思路展示,未对pdf进行进一步分类
    for i in range(len(SECLetter_list)):
        url = 'http:' + SECLetter_url[i]
        urlretrieve(url,filename = './科创板/companies/'+fullname_list[0]+'/'+SECLetter_list[i]+'.pdf')
    '''

driver.quit()

# 将爬虫信息储存到本地excel文件
file = r".\科创板注册制信息披露.xlsx"
final_data = [all_fullname,all_shortname,all_broker,all_auditor,all_lawyer,all_acceptdate, \
    all_inquirydate,all_passdate,all_num_letter1,all_num_letter2,all_num_letter3]
df = pd.DataFrame(final_data).T
df.columns = ["公司全称""公司简称""保荐机构""会计师事务所""律师事务所","受理日期", \
    "开始问询日期","上市委通过日期","问询与回函数量","审计意见数量","律师意见数量"]
df.to_excel(file, index = None)

最终,我们可以得到如下审核问询情况汇总及审核问询披露文件的原始 pdf:

注意:以上代码在计算“问询与回函数量”、“审计意见数量”、“律师意见数量”时仅简单地计数页面上所列示的对应文件的数量,但并不代表发行人实际被问询次数。在现实中,若公司在季度末或者年末更新财务报表,则需根据更新后的财务报表向交易所重新提交以上文件。举个例子:《发行人及保荐机构回复意见》和《发行人及保荐机构回复意见(2022年半年报财务数据更新版)》本质上属于同一份回函的两个版本,计算一次问询次数,而不是两次。此处请读者仔细甄别。

3. 相关推文

Note:产生如下推文列表的 Stata 命令为:
lianxh 爬虫, m
安装最新版 lianxh 命令:
ssc install lianxh, replace

  • 专题:专题课程
    • ⏩ 专题课:文本分析-爬虫-机器学习-2022年4月
    • 连享会:助教入选通知-2022文本分析与爬虫
    • ⚽助教招聘:文本分析-爬虫-机器学习
    • 助教入选结果 - 连享会 文本分析与爬虫直播课
  • 专题:文本分析-爬虫
    • Stata爬虫:爬取地区宏观数据
    • Stata爬虫:爬取A股公司基本信息
    • Stata爬虫-正则表达式:爬取必胜客
    • Python爬虫: 《经济研究》研究热点和主题分析
  • 专题:Python-R-Matlab
    • Python:多进程、多线程及其爬虫应用
    • Python爬虫1:小白系列之requests和json
    • Python爬虫2:小白系列之requests和lxml
    • Python爬虫:爬取华尔街日报的全部历史文章并翻译
    • Python爬虫:从SEC-EDGAR爬取股东治理数据-Shareholder-Activism

课程推荐:因果推断实用计量方法
主讲老师:邱嘉平教授
🍓 课程主页https://gitee.com/lianxh/YGqjp

New! Stata 搜索神器:lianxhsongbl  GIF 动图介绍
搜: 推文、数据分享、期刊论文、重现代码 ……
👉 安装:
. ssc install lianxh
. ssc install songbl
👉  使用:
. lianxh DID 倍分法
. songbl all

🍏 关于我们

  • 连享会 ( www.lianxh.cn,推文列表) 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 直通车: 👉【百度一下: 连享会】即可直达连享会主页。亦可进一步添加 「知乎」,「b 站」,「面板数据」,「公开课」 等关键词细化搜索。


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/152720
 
227 次点击