上市公司年报的获取方法很多,今天我们一起学习下如何从巨潮资讯网爬取,非常好的一点是,巨潮资讯网并不反爬,所以爬取起来简单很多。
爬取的网站网址是:http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search
从网站右侧的筛选窗口可以筛选报告下载。所以我的爬取思路是,首先获取全部可选的上市公司列表,然后逐个公司爬取其历年年报的链接然后再下载。
获取所有可选的上市公司列表 通过网页分析,可以找到可选的上市公司列表数据在这里:
是一个 json 格式的文件,可以使用 insheetjson 命令处理:
copy "http://www.cninfo.com.cn/new/data/szse_stock.json" ., replace 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 + "," + orgIdsave 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
这样就得到了第一页的结果,类似的方法再获取第二页的:
!curl 'http: 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 forval i = 1/3 { di "`i'" qui cap { tempfile tempjson !curl 'http: 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/6cap 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 forval i = 1/5 { qui cap { tempfile tempjson !curl 'http: 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 } } 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/" + adjunctUrldrop if index (announcementTitle, "英文" )drop if index (announcementTitle, "补充" )save dfall, replace
这里为了避免爬取过程中出现问题,我把每家公司爬取的结果也分别保存到了 urldir 文件夹中,方便再次循环的时候跳过这些已经爬取好的。
这样会更加稳健。
爬取到的结果中还有很多是补充报告或者英文报告等,这些文件如果不需要的话可以筛选出来删除。
另外,里面还有一些同一年两份文件的情况,一份是更新前的,一份是更新后的。这种由于命名方式多种多样,所以得谨慎删除。不过这些更新的内容往往都很少,使用更新前的还是更新后的实际上对文本分析的结果几乎没有影响,所以也不用太在意。
批量下载 pdf 文件 如果有时间的话,可以更加认真的筛选要下载的 pdf 文件。筛选好之后就可以下载了,也很简单:
use dfall, clear 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 allset obs `n' gen file = "" local j = 1foreach 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
点击这里跳转到 RStata 短书平台获取附件:使用 Stata 爬取上市公司年报合集
评论