使用 Stata 爬取上市公司年报合集

上市公司年报的获取方法很多,今天我们一起学习下如何从巨潮资讯网爬取,非常好的一点是,巨潮资讯网并不反爬,所以爬取起来简单很多。

爬取的网站网址是:http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search

从网站右侧的筛选窗口可以筛选报告下载。所以我的爬取思路是,首先获取全部可选的上市公司列表,然后逐个公司爬取其历年年报的链接然后再下载。

获取所有可选的上市公司列表

通过网页分析,可以找到可选的上市公司列表数据在这里:

是一个 json 格式的文件,可以使用 insheetjson 命令处理:

*- 所有可选的公司:http://www.cninfo.com.cn/new/data/szse_stock.json

copy "http://www.cninfo.com.cn/new/data/szse_stock.json" ., replace

*- 安装 insheetjson:
*- ssc install insheetjson
*- ssc install libjson
*- 首先查看响应
insheetjson using "szse_stock.json", showr flatten

*- 构造数据结构
clear
gen str30 code = ""
gen str30 pinyin = ""
gen str30 category = ""
gen str30 orgId = ""
gen str30 zwjc = ""
insheetjson code pinyin category orgId zwjc using "szse_stock.json", table(stockList) ///
columns("code" "pinyin" "category" "orgId" "zwjc")
compress
gen stext = code + "," + orgId
save codelist, replace

一共是 5630 家上市公司。

爬取单家公司的年报链接

以平安银行近三年的年报为例:

对着 query 右键复制 curl 代码:

curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' \
-H 'Accept: */*' \
-H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' \
-H 'Connection: keep-alive' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
-H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' \
-H 'Origin: http://www.cninfo.com.cn' \
-H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-raw 'pageNum=1&pageSize=30&column=szse&tabName=fulltext&plate=&stock=000001%2Cgssz0000001&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2020-10-02~2023-10-03&sortName=&sortType=&isHLtitle=true' \
--compressed \
--insecure

curl 语句是在 DOS 窗口或者终端里面运行的,如果想在 Stata 中运行 curl 语句,需要在 curl 的前面加上 ! 或者 shell,另外需要把 curl 语句改成一行,然后在结尾加上 -o res.json 表示把获取到的内容存储在 res.json 文件中:

!curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' -H 'Origin: http://www.cninfo.com.cn' -H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' -H 'X-Requested-With: XMLHttpRequest' --data-raw 'pageNum=1&pageSize=30&column=szse&tabName=fulltext&plate=&stock=000001%2Cgssz0000001&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2020-10-02~2023-10-03&sortName=&sortType=&isHLtitle=true' --compressed --insecure -o res.json

我们可以试试把 seDate 修改成 “2000-01-01~2023-10-02”(pageSize 也可以试试修改成 100,不过没什么效果):

!curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' -H 'Origin: http://www.cninfo.com.cn' -H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' -H 'X-Requested-With: XMLHttpRequest' --data-raw 'pageNum=1&pageSize=30&column=szse&tabName=fulltext&plate=&stock=000001%2Cgssz0000001&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2000-01-01~2023-10-03&sortName=&sortType=&isHLtitle=true' --compressed --insecure -o res.json

再对得到的 res.json 文件进行处理:

insheetjson using "res.json", showr flatten
clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
insheetjson secCode secName announcementTitle adjunctUrl using "res.json", ///
table("announcements") columns("secCode" "secName" "announcementTitle" "adjunctUrl")
compress
list in 1/10, compress

*> +------------------------------------------------------------------------------+
*> | secC~e secName announcementTitle adjunctUrl |
*> |------------------------------------------------------------------------------|
*> 1. | 000001 平安银行 2022年年度报告摘要 finalpage/2023-03-09/1216072955.PDF |
*> 2. | 000001 平安银行 2022年年度报告 finalpage/2023-03-09/1216072952.PDF |
*> 3. | 000001 平安银行 2021年年度报告 finalpage/2022-03-10/1212533413.PDF |
*> 4. | 000001 平安银行 2021年年度报告摘要 finalpage/2022-03-10/1212533358.PDF |
*> 5. | 000001 平安银行 2020年年度报告 finalpage/2021-02-02/1209224370.PDF |
*> |------------------------------------------------------------------------------|
*> 6. | 000001 平安银行 2020年年度报告摘要 finalpage/2021-02-02/1209224367.PDF |
*> 7. | 000001 平安银行 2019年年度报告摘要 finalpage/2020-02-14/1207305490.PDF |
*> 8. | 000001 平安银行 2019年年度报告 finalpage/2020-02-14/1207305488.PDF |
*> 9. | 000001 平安银行 2018年年度报告摘要 finalpage/2019-03-07/1205881079.PDF |
*> 10. | 000001 平安银行 2018年年度报告 finalpage/2019-03-07/1205881066.PDF |
*> +------------------------------------------------------------------------------+

这样就得到了第一页的结果,类似的方法再获取第二页的:

*- 第二页
!curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' -H 'Origin: http://www.cninfo.com.cn' -H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' -H 'X-Requested-With: XMLHttpRequest' --data-raw 'pageNum=2&pageSize=30&column=szse&tabName=fulltext&plate=&stock=000001%2Cgssz0000001&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2000-01-01~2023-10-03&sortName=&sortType=&isHLtitle=true' --compressed --insecure -o res2.json

clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
insheetjson secCode secName announcementTitle adjunctUrl using "res2.json", ///
table("announcements") columns("secCode" "secName" "announcementTitle" "adjunctUrl")
compress
save df2, replace

不过可能会有多页,所以还是循环比较好:

*- 准备一个空数据
clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
save dfall, replace

*- 循环爬取 3 页
forval i = 1/3 {
di "`i'"
qui cap {
*- 创建临时文件
tempfile tempjson
!curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' -H 'Origin: http://www.cninfo.com.cn' -H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' -H 'X-Requested-With: XMLHttpRequest' --data-raw 'pageNum=`i'&pageSize=30&column=szse&tabName=fulltext&plate=&stock=000001%2Cgssz0000001&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2000-01-01~2023-10-03&sortName=&sortType=&isHLtitle=true' --compressed --insecure -o `tempjson'

clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
insheetjson secCode secName announcementTitle adjunctUrl using "`tempjson'", ///
table("announcements") columns("secCode" "secName" "announcementTitle" "adjunctUrl")
compress
append using dfall
save dfall, replace
}
}

*- 查看结果
use dfall, clear
drop if index(announcementTitle, "摘要")
replace adjunctUrl = "http://static.cninfo.com.cn/" + adjunctUrl

这样我们就得到了一家公司所有年份的年报文件链接了。

爬取全部上市公司的年报链接

循环所有的上市公司代码即可,这里仅展示前 6 家公司的:

*- 爬取全部上市公司的年报链接
use codelist, clear
*- 以前六家公司为例
keep in 1/6

*- 创建一个 urldir 保存
cap mkdir "urldir"
forval j = 1/`=_N' {
local codetemp = "`=code[`j']'"
local stexttemp = "`=stext[`j']'"
di "`codetemp'"

*- 如果文件不存在才爬取
if !fileexists("urldir/`codetemp'.dta") {
preserve
*- 准备一个空数据
clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
qui save dfall, replace

*- 循环爬取 5 页
forval i = 1/5 {
qui cap {
*- 创建临时文件
tempfile tempjson
!curl 'http://www.cninfo.com.cn/new/hisAnnouncement/query' -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' -H 'Connection: keep-alive' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Cookie: JSESSIONID=B436758842708ED923684BB19BC585F3; _sp_ses.2141=*; _sp_id.2141=b13ee9ff-2493-4f42-a7f5-089a2990e863.1694410049.10.1696258131.1694788568.fcf26b41-22e6-43f2-90dc-55da9ae64753; routeId=.uc1' -H 'Origin: http://www.cninfo.com.cn' -H 'Referer: http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' -H 'X-Requested-With: XMLHttpRequest' --data-raw 'pageNum=`i'&pageSize=30&column=szse&tabName=fulltext&plate=&stock=`stexttemp'&searchkey=&secid=&category=category_ndbg_szsh&trade=&seDate=2000-01-01~2023-10-03&sortName=&sortType=&isHLtitle=true' --compressed --insecure -o `tempjson'

clear
gen str100 secCode = ""
gen str100 secName = ""
gen str100 announcementTitle = ""
gen str100 adjunctUrl = ""
insheetjson secCode secName announcementTitle adjunctUrl using "`tempjson'", ///
table("announcements") columns("secCode" "secName" "announcementTitle" "adjunctUrl")
compress
drop if index(announcementTitle, "摘要")
append using dfall
compress
save urldir/`codetemp', replace
}
}
restore
}
}

*- 合并 urldir 文件夹下的文件
local files: dir "urldir" files "*.dta"
use urldir/000001.dta, clear
drop in 1/`=_N'

foreach f in `files'{
append using "urldir/`f'"
}
replace adjunctUrl = "http://static.cninfo.com.cn/" + adjunctUrl

*- 删除不需要的结果
drop if index(announcementTitle, "英文")
drop if index(announcementTitle, "补充")
save dfall, replace

这里为了避免爬取过程中出现问题,我把每家公司爬取的结果也分别保存到了 urldir 文件夹中,方便再次循环的时候跳过这些已经爬取好的。

这样会更加稳健。

爬取到的结果中还有很多是补充报告或者英文报告等,这些文件如果不需要的话可以筛选出来删除。

另外,里面还有一些同一年两份文件的情况,一份是更新前的,一份是更新后的。这种由于命名方式多种多样,所以得谨慎删除。不过这些更新的内容往往都很少,使用更新前的还是更新后的实际上对文本分析的结果几乎没有影响,所以也不用太在意。

批量下载 pdf 文件

如果有时间的话,可以更加认真的筛选要下载的 pdf 文件。筛选好之后就可以下载了,也很简单:

*- 批量下载 pdf 文件
use dfall, clear

*- 保存的 pdf 采用如下命名规则
gen year = ustrregexs(0) if ustrregexm(announcementTitle, "\d{4}")
gen title = secCode + "_" + year + "_" + announcementTitle + ".pdf"

*- 创建文件夹保存
cap mkdir "pdf"
forval i = 1/`=_N' {
if !fileexists("pdf/`=title[`i']'") {
copy "`=adjunctUrl[`i']'" "pdf/`=title[`i']'", replace
}
}

如果要下载的 pdf 文件很多,可以考虑把 dfall.dta 拆分成多个文件,然后打开多个 Stata 分别下载对应的文件,效率很快很多。

文件管理

完整下载所有的年报后会得到巨多的 pdf 文件,难以管理,这里我们可以使用代码索引文件筛选自己需要的:

*- 文件管理
local files: dir "pdf" files "*"
local n: word count `files'
clear all
set obs `n'
gen file = ""
local j = 1
foreach i in `files' {
qui replace file = "`i'" in `j'
local j = `j' + 1
}
gen code = ustrregexs(1) if ustrregexm(file, "(\d{6})_(\d{4})_")
gen year = ustrregexs(2) if ustrregexm(file, "(\d{6})_(\d{4})_")
destring year, replace
save filedf, replace

list in 1/10, compress

*> +----------------------------------------------------------+
*> | file code year |
*> |----------------------------------------------------------|
*> 1. | 000007_2008_2008年年度报告.pdf 000007 2008 |
*> 2. | 000007_2005_深达声A2005年年度报告.pdf 000007 2005 |
*> 3. | 000002_2003_万 科 A2003年年度报告.pdf 000002 2003 |
*> 4. | 000004_2008_2008年年度报告(更新后).pdf 000004 2008 |
*> 5. | 000002_2006_2006年年度报告(调整后).pdf 000002 2006 |
*> |----------------------------------------------------------|
*> 6. | 000004_2009_2009年年度报告(更新后).pdf 000004 2009 |
*> 7. | 000007_2003_深达声A2003年年度报告.pdf 000007 2003 |
*> 8. | 000002_2007_2007年年度报告.pdf 000002 2007 |
*> 9. | 000007_2004_深达声A2004年年度报告.pdf 000007 2004 |
*> 10. | 000001_2007_2007年年度报告.pdf 000001 2007 |
*> +----------------------------------------------------------+

点击这里跳转到 RStata 短书平台获取附件:使用 Stata 爬取上市公司年报合集

评论