Stata 如何爬取所有 A 股上市公司的基本信息并展示地域分布?

最近有小伙伴想爬取和讯网的上市公司列表然后绘图展示各省上市公司数量:http://stockdata.stock.hexun.com/gszl/jbgk.aspx

实际上在之前的系列课程「Stata 编程导论」的 <8. Stata网络数据爬取:JSON篇> 中讲解过这个案例,今天我们再单独看一下。

通过网页分析(Fn + F12)可以看到这个表格对应的数据在这里:

对应的链接是这个:

http://stockdata.stock.hexun.com/gszl/data/jsondata/jbgk.ashx?count=20&titType=null&page=1&callback=hxbase_json15

显然这里的 20 表示每页显示 20 条,可以改成 5000:

http://stockdata.stock.hexun.com/gszl/data/jsondata/jbgk.ashx?count=5000&titType=null&page=1&callback=hxbase_json15

这个数据看起来像是 JSON 数据,但它其实不是一个 JSON 数据,而是个 JS 对象(带回掉函数的),关于 JSON 与 JS 对象的关系,百度百科里面给出了一个比较:

var obj = {a: 'Hello', b: 'World'}; //这是一个对象,注意键名也是可以使用引号包裹的
var json = '{"a": "Hello", "b": "World"}'; //这是一个 JSON 字符串,本质是一个字符串

JSON 数据的处理可以使用 insheetjson 命令,如何把这个 JS 对象转换为 JSON 呢?

先下载下来:

copy "http://stockdata.stock.hexun.com/gszl/data/jsondata/jbgk.ashx?count=5000&titType=null&page=1&callback=hxbase_json15" "temp.json", replace

这个 temp.json 文件里面只有一行,245 万个字符,这样的文件是不能直接使用 infix 之类的命令读入 Stata 的,我们可以使用 mata 程序处理它,处理的模板就是将它变成一个 json:

mata:
fin = fopen("temp.json", "r")
line = fread(fin, 3000000)
line = subinstr(line, `"'"', `"""', .)
line = subinstr(line, `""_blank""', `"'_blank'"', .)
line = subinstr(line, `"openshowd(this,""', "", .)
line = subinstr(line, `"","1")"', "", .)
line = subinstr(line, `""Closed(this)""', "", .)
line = subinstr(line, `"<img alt="" src=""', "", .)
line = subinstr(line, `""/>"', "", .)
line = subinstr(line, "sum", `""sum""', .)
line = subinstr(line, "list", `""list""', .)
line = subinstr(line, "Number", `""Number""', .)
line = subinstr(line, "StockNameLink", `""StockNameLink""', .)
line = subinstr(line, "Stockname", `""Stockname""', .)
line = subinstr(line, "Pricelimit", `""Pricelimit""', .)
line = subinstr(line, "lootchips", `""lootchips""', .)
line = subinstr(line, "shareholders", `""shareholders""', .)
line = subinstr(line, "Institutional", `""Institutional""', .)
line = subinstr(line, "Iratio", `""Iratio""', .)
line = subinstr(line, "deviation", `""deviation""', .)
line = subinstr(line, "maincost", `""maincost""', .)
line = subinstr(line, "district", `""district""', .)
line = subinstr(line, "Cprice", `""Cprice""', .)
line = subinstr(line, "Stockoverview", `""Stockoverview""', .)
line = subinstr(line, "hyLink", `""hyLink""', .)
line = subinstr(line, "dyLink", `""dyLink""', .)
line = subinstr(line, "gnLink", `""gnLink""', .)
line = subinstr(line, "StockLink", `""StockLink""', .)
line = subinstr(line, "Addoptional", `""Addoptional""', .)
line = subinstr(line, "hxbase_json15(", "", .)
line = subinstr(line, "}]})", "}]}", .)
fclose(fin)
mata stata cap erase temp2.json
fout = fopen("temp2.json", "w")
fwrite(fout, line)
fclose(fout)
end

处理之后我们得到了一个 temp2.json 文件,这个文件就是一个 JSON 数据了:

我们先给这个文件转下码,因为我注意到里面有乱码:

utrans temp2.json

utrans 是我写的一个非常简单的小命令:

*! utf-8中文转码
*! utrans 文件名.后缀名
*! 示例:utrans temp.do
cap prog drop utrans
prog define utrans
version 14.0
syntax anything
cap preserve
clear
cap qui{
unicode encoding set gb18030
unicode translate "`anything'"
unicode erasebackups, badidea
unicode analyze "`anything'"
unicode erasebackups, badidea
}
if r(N_needed) == 0 di in yellow "转码完成"
if r(N_needed) != 0 di in red "转码失败"
end

查看响应:

insheetjson using "temp2.json", showresponse flatten

存储为 Stata 数据:

clear
gen str100 Stockname = ""
gen str100 Pricelimit = ""
gen str100 lootchips = ""
gen str100 shareholders = ""
gen str100 Institutional = ""
gen str100 Iratio = ""
gen str100 district = ""
gen str100 Cprice = ""
gen str200 maincost = ""
insheetjson Stockname Pricelimit lootchips shareholders Institutional Iratio district Cprice maincost using "temp2.json", table(list) col("Stockname" "Pricelimit" "lootchips" "shareholders" "Institutional" "Iratio" "district" "Cprice" "maincost")
replace district = ustrregexs(1) if ustrregexm(district, ">(.*)<")
replace maincost = ustrregexs(1) if ustrregexm(maincost, `"'>(.*)</a"')
foreach i of varlist _all {
replace `i' = "" if `i' == "--"
}
compress
destring, replace
save stocklist, replace

是不是感觉超酷,因为我们知道爬虫俱乐部写了个 cnstock 命令(该命令爬取的就是上次课的最后一个案例,最近这个命令挂了):

*- 安装 cnstock(就别安装了)
*- ssc install cnstock

不过这个命令只能获取股票的代码和名称,我们这段程序呢,可以获取股票的详细信息!那我们不编一个命令是不是可惜了?

*! 微信公众号 RStata
*! 2023 年 9 月 17 日
cap prog drop cnstock3
prog def cnstock3
version 7.0
di in yellow "欢迎使用微信公众号 RStata 开发的 cnstock3 命令,该命令可以实时下载中国上市公司的详细信息。"
di in yellow "下载中..."
qui{
clear
tempfile filein fileout
copy "http://stockdata.stock.hexun.com/gszl/data/jsondata/jbgk.ashx?count=5000&titType=null&page=1&callback=hxbase_json15" `filein', replace
mata: file("`filein'", "`fileout'")
*- 处理 json 格式的数据
gen str100 Stockname = ""
gen str100 Pricelimit = ""
gen str100 lootchips = ""
gen str100 shareholders = ""
gen str100 Institutional = ""
gen str100 Iratio = ""
gen str100 district = ""
gen str100 Cprice = ""
gen str200 maincost = ""
insheetjson Stockname Pricelimit lootchips shareholders Institutional Iratio district Cprice maincost using "`fileout'", table(list) col("Stockname" "Pricelimit" "lootchips" "shareholders" "Institutional" "Iratio" "district" "Cprice" "maincost")
replace district = ustrregexs(1) if ustrregexm(district, ">(.*)<")
replace maincost = ustrregexs(1) if ustrregexm(maincost, `"'>(.*)</a"')
foreach i of varlist _all {
replace `i' = "" if `i' == "--"
}
compress
destring, replace
}
di in green "获取成功..."
end

mata:
void file(string scalar filein, string scalar fileout) {
fin = fopen(filein, "r")
line = fread(fin, 3000000)
line = ustrfrom(line, "GBK", 4)
line = subinstr(line, `"'"', `"""', .)
line = subinstr(line, `""_blank""', `"'_blank'"', .)
line = subinstr(line, `"openshowd(this,""', "", .)
line = subinstr(line, `"","1")"', "", .)
line = subinstr(line, `""Closed(this)""', "", .)
line = subinstr(line, `"<img alt="" src=""', "", .)
line = subinstr(line, `""/>"', "", .)
line = subinstr(line, "sum", `""sum""', .)
line = subinstr(line, "list", `""list""', .)
line = subinstr(line, "Number", `""Number""', .)
line = subinstr(line, "StockNameLink", `""StockNameLink""', .)
line = subinstr(line, "Stockname", `""Stockname""', .)
line = subinstr(line, "Pricelimit", `""Pricelimit""', .)
line = subinstr(line, "lootchips", `""lootchips""', .)
line = subinstr(line, "shareholders", `""shareholders""', .)
line = subinstr(line, "Institutional", `""Institutional""', .)
line = subinstr(line, "Iratio", `""Iratio""', .)
line = subinstr(line, "deviation", `""deviation""', .)
line = subinstr(line, "maincost", `""maincost""', .)
line = subinstr(line, "district", `""district""', .)
line = subinstr(line, "Cprice", `""Cprice""', .)
line = subinstr(line, "Stockoverview", `""Stockoverview""', .)
line = subinstr(line, "hyLink", `""hyLink""', .)
line = subinstr(line, "dyLink", `""dyLink""', .)
line = subinstr(line, "gnLink", `""gnLink""', .)
line = subinstr(line, "StockLink", `""StockLink""', .)
line = subinstr(line, "Addoptional", `""Addoptional""', .)
line = subinstr(line, "hxbase_json15(", "", .)
line = subinstr(line, "}]})", "}]}", .)
fclose(fin)
fout = fopen(fileout, "w")
fwrite(fout, line)
fclose(fout)
}
end

注意 Stata 命令里面的 ado 代码和 mata 代码要分开,不能直接在 ado 代码里面嵌入 mata 代码块,因为 ado 命令是以 end 为识别标志结束的。

另外注意这里的代码和前面的代码有几处不同:

  1. ado 里面尽量不要下载文件到本地,而应该使用临时文件:tempfile filein fileout;
  2. unicode encoding set gb18030、unicode translate “文件名”、unicode erasebackups, badidea 或者 utrans 命令只能转码工作目录下面的文件,因此这里使用的是 line = ustrfrom(line, “GBK”, 4) 进行转码。
  3. 把这部分代码保存为 cnstock3.ado 即可使用。

然后运行:

cnstock3
save cnstock3, replace

然后我们就可以根据里面的 district 变量统计各省上市公司的数量并绘图展示了:

use cnstock3, clear
contract district
drop if missing(district)
encode district, gen(district2)
tw bar _freq district2, hori ///
yla(1(1)32, val) xsize(16) ysize(20) ///
xti("上市公司数量") ti("各个省份上司公司的数量") ///
subti("数据来源:和讯网") caption("绘制:微信公众号 RStata") yti("")
gr export pic6_3.png, replace width(4800)

如果想要根据柱条的高度排序,可以使用 sencode 命令:

*- 安装 sencode: ssc install sencode
sencode district, gen(district3) gsort(_freq)
tw bar _freq district3, hori ///
yla(1(1)32, val) xsize(16) ysize(20) ///
xti("上市公司数量") ti("各个省份上司公司的数量") ///
subti("数据来源:和讯网") caption("绘制:微信公众号 RStata") yti("")
gr export pic6_4.png, replace width(4800)

反过来排序也可以:

sencode district, gen(district4) gsort(-_freq)
tw bar _freq district4, hori ///
yla(1(1)32, val) xsize(16) ysize(20) ///
xti("上市公司数量") ti("各个省份上司公司的数量") ///
subti("数据来源:和讯网") caption("绘制:微信公众号 RStata") yti("")
gr export pic6_5.png, replace width(4800)

点击这里跳转到 RStata 短书平台获取附件:Stata 如何爬取所有 A 股上市公司的基本信息并展示地域分布?

评论