为排除业绩地雷,有人提到一个选股指标:最近三年净利润同比增长10%+,且净资产收益率(ROE)15%+。于是很好奇究竟是哪些股票符合“绩优”的范畴?

虽然在部分APP、网站有提供此类选股功能,但同时一直也想着以爬虫的名义做些技术性的尝试,于是便有了这个DEMO。

1. 抓取全市场股票数据

数据来源无非东财、同花顺等,这里选择了东财。

访问http://quote.eastmoney.com/stock_list.html页面,可以看到全部的股票列表。通过查看HTML可以看到其页面结构,是两个UL列表,因此,便可通过requestcheerio模块获取数据并加以处理了。如下:

javascriptCopy code
  • 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); });
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); });

抓取的结果会涉及中文编码的问题,因此,需要额外进行处理:

javascriptCopy code
  • 1
const $ = cheerio.load(iconv.decode(body, 'gb2312'), {decodeEntities: false});
const $ = cheerio.load(iconv.decode(body, 'gb2312'), {decodeEntities: false});

2. 存储股票数据

获取数据后,根据其数据内容建立相应的库表,以进行持久化存储。这里采用async.times和事务结合的方式进行处理。如下:

javascriptCopy code
  • 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);
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就是股票代码参数(含前缀)。因此,抓取代码如下:

javascriptCopy 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}); });
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. 存储财务数据

同存储股票数据,将抓取到的财务数据的所有字段进行持久化存储。代码如下:

javascriptCopy code
  • 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 }); });
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中分组过滤三年的数据,但过于复杂,且性能并没有太多优势,于是便转而在应用层实现。代码如下:

javascriptCopy code
  • 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 });
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. 结果展示

最后一步便是对查询结果进行友好的界面化展示,这里无疑是选择表格(或列表)。代码如下:

htmlCopy code
  • 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%