为排除业绩地雷,有人提到一个选股指标:最近三年净利润同比增长10%+,且净资产收益率(ROE)15%+。于是很好奇究竟是哪些股票符合“绩优”的范畴?
虽然在部分APP、网站有提供此类选股功能,但同时一直也想着以爬虫的名义做些技术性的尝试,于是便有了这个DEMO。
1. 抓取全市场股票数据
数据来源无非东财、同花顺等,这里选择了东财。
访问http://quote.eastmoney.com/stock_list.html页面,可以看到全部的股票列表。通过查看HTML可以看到其页面结构,是两个UL列表,因此,便可通过request
和cheerio
模块获取数据并加以处理了。如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
request({
method: 'get',
uri: 'http://quote.eastmoney.com/stock_list.html',
encoding: null
}, (err, response, body) => {
if (err) {
return cb(err);
}
const $ = cheerio.load(iconv.decode(body, 'gb2312'), {decodeEntities: false});
const list = [];
const $stocks = $('#quotesearch li > a');
$stocks.each((i, ele) => {
const $ele = $(ele);
const href = $ele.attr('href');
if (href) {
const code = href.match(/(s[hz])(\d{6})/i);
const name = $ele.html().match(/([^()]+)/i)[1];
if (code) {
list.push({
name,
code: code[2],
label: code[0],
market: code[1]
});
}
}
});
cb(null, list);
});
抓取的结果会涉及中文编码的问题,因此,需要额外进行处理:
- 1
const $ = cheerio.load(iconv.decode(body, 'gb2312'), {decodeEntities: false});
2. 存储股票数据
获取数据后,根据其数据内容建立相应的库表,以进行持久化存储。这里采用async.times
和事务结合的方式进行处理。如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
models.sequelize.transaction((t) =>
new Promise((resolve, reject) => {
async.times(data.length, (i, nextFn) => {
Stock.create(data[i], {
transaction: t
}).then((stock) => nextFn(null, stock));
}, (err, result) => {
if (err) {
reject(util.catchError({
status: 500,
code: STATUS_CODES.STOCK_SAVE_ERROR,
message: 'Stock Save Error.',
messageDetail: `${data.length} stocks save failed.`,
data
}));
} else {
resolve(result);
}
});
})
).then(successCb, errorCb);
3. 抓取股票财务数据
进入东财的股票详情页面,再点击进入“F10财务分析”页面,通过网络请求可以拿到财务指标的数据接口。对其URL进行分析,可以找到其规律:http://f10.eastmoney.com/NewFinanceAnalysis/MainTargetAjax?type=1&code=xxx,其中的code就是股票代码参数(含前缀)。因此,抓取代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
request({
method: 'get',
uri: `http://f10.eastmoney.com/NewFinanceAnalysis/MainTargetAjax?type=1&code=${stock.label}`,
json: true,
encoding: null
}, (err, res, data) => {
if (err) {
return cb(err);
}
cb(null, {finance: data, stock});
});
4. 存储财务数据
同存储股票数据,将抓取到的财务数据的所有字段进行持久化存储。代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
async.times(data.stocks.length, (i, nextFn) => {
stockService.importStockFinance(data.stocks[i], (err, finance) => {
console.log(`Process progress: ${(i + 1)} / ${data.stocks.length}`);
nextFn(null, finance);
});
}, (err, result) => {
if (err) {
return next(err);
}
console.log(`${data.stocks.length} stocks is imported. Be Happy! -_^`);
logger.info(formatOpLog({
fn: 'importStockFinance',
msg: `finance data is imported.`,
req
}));
res.type('application/json');
res.send({
status: 200,
code: STATUS_CODES.SUCCESS,
message: null,
data: result.length
});
});
5. 数据分析
拿到数据之后便可以进行数据分析了。我们的目标是筛选出最近三年每年归属净利润同比增长10%(或15%)以上,且加权净资产收益率在15%以上的股票。
最初想法是直接在SQL中分组过滤三年的数据,但过于复杂,且性能并没有太多优势,于是便转而在应用层实现。代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
const stocks = {};
result.stocks.forEach((stock) => {
stocks[stock.stockCode] = stocks[stock.stockCode] || [];
if (stocks[stock.stockCode].length < 3) {
stocks[stock.stockCode].push(stock);
}
});
const stockList = [];
Object.keys(stocks).forEach((code) => {
let match = true;
for (let finance of stocks[code]) {
const gsjlrtbzz = parseFloat(finance.gsjlrtbzz) || 0;
const jqjzcsyl = parseFloat(finance.jqjzcsyl) || 0;
if (gsjlrtbzz < 10 || jqjzcsyl < 15 || finance.date < '2017-01-01') {
match = false;
break;
}
}
if (!match) {
delete stocks[code];
} else {
stockList.push(stocks[code]);
}
});
res.render(`${appConfig.pathViews}/stock-filter`, {
stockList
});
6. 结果展示
最后一步便是对查询结果进行友好的界面化展示,这里无疑是选择表格(或列表)。代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
<div style="font-weight: bold;margin: 10px 0;">
总数:<span style="color: #f00;"><?- stockList.length ?></span> 只股票
</div>
<table>
<tr>
<th>代码</th>
<th>名称</th>
<th>报告期</th>
<th>营业总收入同比增长(%)</th>
<th>归属净利润同比增长(%)</th>
<th>扣非净利润同比增长(%)</th>
<th>加权净资产收益率(%)</th>
<th>摊薄净资产收益率(%)</th>
<th>摊薄总资产收益率(%)</th>
<th>资产负债率(%)</th>
</tr>
<? stockList.forEach(function(finance){ ?>
<? finance.forEach(function(item, itemIdx){ ?>
<tr>
<? if(itemIdx < 1){ ?>
<td rowspan="<?- finance.length ?>"><?- finance[0].Stock.code ?></td>
<td rowspan="<?- finance.length ?>"><?- finance[0].Stock.name ?></td>
<? } ?>
<td><?- item.date ?></td>
<td><?- item.yyzsrtbzz ?></td>
<td><?- item.gsjlrtbzz ?></td>
<td><?- item.kfjlrtbzz ?></td>
<td><?- item.jqjzcsyl ?></td>
<td><?- item.tbjzcsyl ?></td>
<td><?- item.tbzzcsyl ?></td>
<td><?- item.zcfzl ?></td>
</tr>
<? }) ?>
<? }) ?>
</table>
效果如下:
至此,完美拿到想要的结果。
通过这个结果可知,大A的“核心资产”或绩优股并不多,区区不到200只,占比不过4.52%。