项目介绍
通过对东方财富网的盈利预测板块爬虫,拿到股票的每股盈利预测,结合该股的现市值,就可算出股票的预测市盈率,从而帮助我们筛选股票。而neo4j是一个图形数据库,我们将股票信息存入neo4j中并且以知识图谱的形式展现出来。
开发日志
4.9及之前
首先我们要完成对东方财富的爬虫工作,一开始打算爬的是个股研报板块,因为市盈率他帮你计算好了,选用jsoup,jsoup的大致流程是创建一个httpclient-->创建get或者post请求-->获取响应-->获取页面内容
public void testJsoup() throws Exception {
// 创建HttpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建GET请求
HttpGet httpGet = new HttpGet("https://reportapi.eastmoney.com/report/list?cb=datatable8079493&industryCode=*&pageSize=50&industry=*&rating=&ratingChange=&beginTime=2020-12-24&endTime=2022-12-24&pageNo=5&fields=&qType=0&orgCode=&code=*&rcode=&p=5&pageNum=5&pageNumber=5&_=1671854592251");
httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
// 获取响应
CloseableHttpResponse response = httpClient.execute(httpGet);
// 获取页面内容
if (response.getStatusLine().getStatusCode() == 200) {
String html = EntityUtils.toString(response.getEntity(), "UTF-8");
// 创建Document对象
Document document = Jsoup.parse(html);
Element element=document.body();
StringBuffer stringBuffer=new StringBuffer();
// System.out.println(element.text());
// String strReg="datatable.?[(]";
String json=element.text().substring(17);//前17位无用
JSONObject jObject1=new JSONObject(json);
//获取对象中的数组
JSONArray data = jObject1.getJSONArray("data");
for(int i=0;i<data.length();i++){
JSONObject jObject2=data.getJSONObject(i);
String stockName=jObject2.getString("stockName");
String stockCode=jObject2.getString("stockCode");
String predictNextTwoYearEps=jObject2.getString("predictNextTwoYearEps");
String predictNextTwoYearPe=jObject2.getString("predictNextTwoYearPe");
String predictNextYearEps=jObject2.getString("predictNextYearEps");
String predictNextYearPe=jObject2.getString("predictNextYearPe");
String predictThisYearEps=jObject2.getString("predictThisYearEps");
String predictThisYearPe=jObject2.getString("predictThisYearPe");
stringBuffer
// .append(stockName).append(",")
.append(stockCode).append(",")
.append(predictNextTwoYearEps).append(",")
.append(predictNextTwoYearPe).append(",")
.append(predictNextYearEps).append(",")
.append(predictNextYearPe).append(",")
.append(predictThisYearEps).append(",")
.append(predictThisYearPe).append(",")
.append("\n");
}
File newfile=new File("D:\\个股研报\\test.csv");//待写入文件
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newfile),"UTF-8"));
bufferedWriter.write(stringBuffer.toString());
bufferedWriter.close();
System.out.println("文件写入内容完成");
response.close();
httpClient.close();
}
}
很快遇到了第一个问题:为什么打印出的html内容没数据?经过F12检查network发现该网页的数据是通过js获取的,请求的url是这样的
https://reportapi.eastmoney.com/report/list?cb=datatable8079493&industryCode=*&pageSize=50&industry=*&rating=&ratingChange=&beginTime=2020-12-24&endTime=2022-12-24&pageNo=5&fields=&qType=0&orgCode=&code=*&rcode=&p=5&pageNum=5&pageNumber=5&_=1671854592251
response就是一个名为datatable8079493的json,这下又犯难了,一个是url末尾的_=1671854592251和datatable8079493不知道规律,那就不能写出下一面的url;还有一个问题是response的datatable8079493长度不定,也就不好把他从数据中剥离出来。好消息是response有比html上显示的更多的预测内容,它有预测到后两年的市盈率,而html中只有今年和明年的。
此时我又了解到另一个工具包htmlunit,它可以完成浏览器行为的模拟,例如点击。于是我开始使用htmlunit进行爬虫,它的工作流程是创建一个webclient-->拿htmlpage-->处理内容
public void UnitTest() throws Exception{
WebClient webClient=new WebClient();
//禁止css加载
webClient.getOptions().setCssEnabled(false);//禁止css
webClient.getOptions().setJavaScriptEnabled(true);//允许js
HtmlPage page=webClient.getPage("https://data.eastmoney.com/report/profitforecast.jshtml");
webClient.waitForBackgroundJavaScript(2000);
System.out.print(firstPage.asXml());
}
因为htmlunit对css和js的兼容比较差,所以一般来说都要关闭,但是我们的数据是通过js获取的,当然不能关,还有要等待js完成,所以有
webClient.waitForBackgroundJavaScript(2000);
也就是等待两千毫秒。
页面成功拿到了,问题又出现了。
这个table表没id啊,htmlunit官方文档里对table只有通过id获取,相当难办。
4.10
今天灵机一动,既然jsoup的document对象是用html的String类作为参数创建,而jsoup可以通过类名来查询,那我们不是可以用htmlunit来完成跳转下一面,再返回html页面给jsoup处理吗?所以今天就完成htmlunit跳转测试。
跳转就是找到对应的a标签并点击。
List<HtmlAnchor> ao = index.getByXPath("//a");
HtmlAnchor which = null;
for(HtmlAnchor h :ao) {
String href = h.getAttribute("data-page").toString();//拿到datapage属性
if(href.equals("2")) {//如果datapage等于2
// System.out.println(href);
which = h;
break;
}
}
//点击跳转到第二面
HtmlPage index2 = which.click();
4.11
今天完成跳转到下一面并把每一面的html内容放入集合容器中。
1.选集合容器
我们的需求是有序且不重复,所以选linkedhashset。为什么linkedhashset有序?因为它是继承hashset的,而hashset是用hashmap实现的,底层是数组加链表和红黑树。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
hashset在添加元素时用的是hashmap的put方法,它把元素作为key,把PRESENT作为value,当key相同时,value也相同,此时就会用新的value替换旧value,也就达到了不重复的要求。
2.如何判断哪个超链接是下一页?
声明一个pageNum的整型变量代表当前页面值,在if的判断条件中将data-page转成整型来比对当前页面值加1。
Integer.parseInt(href) == pageNum+1
运行报错:href为"",也就是说在某次循环中,href为空串,说明有的datapage的值是空值,虽然按理来说datapage的值只会是数字或者空值,但是为了程序的复用性更好,我们用正则表达式判断href的值是否为数字。
Pattern p=Pattern.compile("^[0-9]{1,2}$");
if (p.matcher(href).matches() && Integer.parseInt(href) == pageNum+1) {
3.我们找到了下一面但是如何让这一过程重复下去呢?
双层循环
我们先将我们当前页面的html传入容器中,再跳到下一面,将HtmlPage的值指向我们当前这一面且pageNum加一。但是什么时候停止呢?就是到最后一面嘛。可是什么时候到最后一面?也就是说我们要找到最大页码值,当当前页码值等于最大页码值就可以了。
如何取得最大页码值?
1.取得list的倒数第二个
可以看到a标签倒数第二个就是最大页码,但是谁能保证它是不是倒数第二呢?html中有很多很多超链接,随便加些a标签就会使这个方法失效。不可取
2.写一个getmax的方法,将list中的字符串转换成整数并判断大小
我们可以拿出所有的data-page比大小嘛,找出最大的data-page就好啦,但是有点麻烦。
3.判断data-page是否是pageNum+1,是的就跳转并break;加一个理想page值,值为当前page值+1,当出循环时,若page不是理想page值时,就break结束循环。
因为pageNum是当前页码,我们想要datapage为pageNum加一的超链接,我们希望每次出循环时我们都完成了跳转,pageNum是之前的加一,但是到最后一面的时候,并没有这个datapage,因为此时pageNum最大,我们没有跳转,也就是说与我们的期望值不符。
这个期望值是借鉴快速失败机制
快速失败也就是在迭代容器时,不允许改变其内容,否则就会抛出ConcurrentModificationException异常。
其原理就是迭代器在迭代的时候有一个modCount值以及一个expectedModCount值,当访问下一个元素前,迭代器会检