简介
趁着周末有空,最近又在做爬虫相关的功能,想将易班的推文信息放到自己的小程序里,奈何网上没有找到合适的文章,于是就自己研究了一下易班推文的爬取,使用Springboot设置定时任务去爬取,在爬取信息成功之后打印爬虫的信息。
该教程仅用于学习用途,如有侵犯您的权益,请联系我删除。
效果图:
广告:
广告:
个人博客,网页版:palewl.cn
小程序端: 培正Lite
源码:码云直达
使用技术:java为前提,使用HttpClient和Jsoup。
使用工具:结合psotMan进行测试
如果你对爬虫登录有兴趣,可以看我上一篇模拟登录的案例:
爬虫之模拟强智系统登录
分析
首先找到需要爬取的页面地址:
http://www.yiban.cn/school/index/puserid/13712362
这里以广东培正学院的易班首页为例,爬取文章信息
随便打开一篇文章,如打开:植树节,为地球绘上一点绿色这篇文章,打开f12调试,找到如图所示的请求
请求地址:http://www.yiban.cn/forum/article/show/article_id/155610598/channel_id/301700/puid/1371236
链接参数分析:
三个参数都有特定的意义,这里先记录一下。
article_id:155610598 //文章的id
channel_id:301700 //频道的id
puid:1371236 //作者的id
这个地址是点击文章后访问的地址,但是打开response发现文章的内容并不在这里。
其实也很容易想到,一般这种情况就可以认为该页面是通过异步的方式来请求数据的。
继续查找:
发现连接:
http://www.yiban.cn/forum/article/showAjax
提交方式:POST
表单内容:
试着去postMan尝试一下:
结果是正确的。
因此这里爬取推文的关键就是得到这三个参数,然后通过这三个参数发起异步请求,从而得到数据。
article_id:155610598 //文章的id
channel_id:301700 //频道的id
puid:1371236 //作者的id
还有个小问题
再次请求这个页面:http://www.yiban.cn/school/index/puserid/13712362
这里的放假通知的地址:
http://www.yiban.cn/forum/schoolAd/articleShow/id/3244805
</a>
跟植树这个地址:
http://www.yiban.cn/forum/article/show/article_id/155610598/channel_id/301700/puid/13712362
链接格式完全不一样,第一个只有简单的id/3244805结尾,但是第二个地址却包含了需要发起异步请求的所有参数。
但是当点击的时候却发现地址:
显然发生了重定向,重定向后的地址才是我们需要的。
思路实现
1.搞清楚两个地址,一个是需要爬取的地址:
添加链接描述
通过这个地址需要获得发起异步请求的三个参数。
另一个是需要发起异步请求的地址:
请求地址: http://www.yiban.cn/forum/article/showAjax
请求方式: POST
2.根据发起的异步请求,需要准备几个实体对象用于接收异步请求返回的结果。
3.准备工具类,使用池的方式管理HttpClient
4.使用SpringBoot的定时任务,定时爬取数据,将爬取到的结果打印到控制台。
如你有存入到数据库的需求,这个也是一样的。
代码实现
相关的代码已经放到码云,你也可以clone下来查看
创建一个maven工程,导入依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--阿里巴巴处理json工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--jsoup 处理爬虫文档-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
<!--lang3工具类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
准备实体对象用于接收结果
Arti
/**
* 临时存储第一部分爬虫的基础信息,非数据库实体表
*/
public class Arti implements Serializable {
private String title; //标题
private String src; //推文图片地址
private String href; //推文访问地址
private String article_id;//文章id
//get set ...
}
/**
* 文章实体,重要的实体部分,如有需要可以作为数据库部分
*/
public class Article implements Serializable {
private String id;
private String Channel_id;
private String User_id;//作者id
private String title;
private String firstPicture;//首图地址
private String isNotice;
private String isLocked;
private String isWeb;
private String replyCount; //回复数
private String replyTime;//回复时间
private String createTime;
private String updateTime;
private String clicks;//点击数
private String upCount;
private String status;
private String Sections_id;
private String UserGroup_id;
private String oldArtId;
private String oldAreaId;
private String content; //推文内容
//get set ...
}
/**
* 作者实体对象,重要的实体部分,如有需要可以作为数据库部分
*/
public class Author implements Serializable {
private String id;
private String isPublic;
private String isOrganization;
private String isDeveloper;
private String isSchoolVerify;
private String regTime;
private String kind;
private String nick;//作者昵称
private String name;//作者姓名
private String sex;
private String birth;
private String source;
private String major;
private String img;
private String userName;
private String area;
private String addr;
private String authority;
private String group_id;
private String qr_add;
private String qr_index;
private String token;
private String homeUrl;
private String iosHomeUrl;
private String androidHomeUrl;
private String authorname;
private String avatar;//头像地址
//get set ...
}
/**
* 异步返回的返回的Data数据
*/
public class Data implements Serializable {
private String id;
private String baseUri;
private String jsBaseUrl;
private String cssBaseUrl;
private String imgBaseUrl;
private String channel_id;
private String puid;
private String isManager;
private String jsVersion;
private String cssVersion;
private String isOrganization;
private String SectionFlag;
private Boolean editPermission;
private Boolean delPermission;
private String hotScore;
private String Sections_name;
//外键
private String articleId;
//非数据库字段
private Article article;
//非数据库字段
private UrlParams urlParams;
//get set ...
}
/**
* 异步请求返回的结果集
*/
public class Result implements Serializable {
private String id;
private String code; //返回的状态码
private String message;//提示消息
private Data data;
//get set ...
}
/**
* 文章的id信息
*/
public class UrlParams implements Serializable {
private String id; //id
private String channel_id;//频道id
private String puid;//作者id
//get set ...
}
准备工具类:
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
/**
* HttpClient的工具类,使用连接池的方式管理
*/
@Component
public class ClientUtils {
private PoolingHttpClientConnectionManager cm;
public ClientUtils() {
this.cm = new PoolingHttpClientConnectionManager();
// 设置最大连接数
cm.setMaxTotal(200);
// 设置每个主机的并发数
cm.setDefaultMaxPerRoute(20);
}
//获取内容
public String getHtml(String url) {
// 获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
// 声明httpGet请求对象
HttpGet httpGet = new HttpGet(url);
// 设置请求参数RequestConfig
httpGet.setConfig(this.getConfig());
//httpGet.addHeader("User-Agent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36");
CloseableHttpResponse response = null;
try {
// 使用HttpClient发起请求,返回response
response = httpClient.execute(httpGet);
// 解析response返回数据
if (response.getStatusLine().getStatusCode() == 200) {
String html = "";
// 如果response。getEntity获取的结果是空,在执行EntityUtils.toString会报错
// 需要对Entity进行非空的判断
if (response.getEntity() != null) {
html = EntityUtils.toString(response.getEntity(), "UTF-8");
}
return html;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
// 关闭连接
response.close();
}
// 不能关闭,现在使用的是连接管理器
// httpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
//获取请求参数对象
private RequestConfig getConfig() {
RequestConfig config = RequestConfig.custom().setConnectTimeout(2000)// 设置创建连接的超时时间
.setConnectionRequestTimeout(500) // 设置获取连接的超时时间
.setSocketTimeout(10000) // 设置连接的超时时间
.build();
return config;
}
}
设置定时任务:
/**
* 设置定时任务去爬取易班的内容
*/
@Component
@Configuration
@EnableScheduling
public class TaskArticle {
//工具类
@Autowired
private ClientUtils clientUtils;
/**
* 设置定时任务去爬虫
* 第一次执行前延时5秒启动
* 24*60*60*1000 每隔半天天去爬取数据
*/
@Scheduled(initialDelay = 1000, fixedDelay = 12*60*60*1000)
public void taskArticle0(){
System.out.println("执行了定时任务.");
String uri = "http://www.yiban.cn/school/index/puserid/13712362";
//开始爬虫0,爬取首页数据
getMap(clientUtils, uri);
}
/**
* 根据地址去获取http://www.yiban.cn/school/index/puserid/13712362
* 的文章,得到初步的信息
* System.out.println(src);//图片资源
* System.out.println(href);//a标签地址
* System.out.println(title);//标题
* System.out.println(article_id);//文章id
* @param clientUtils
* @param uri
* @return
*/
private void getMap(ClientUtils clientUtils, String uri){
//得到响应内容
String html = clientUtils.getHtml(uri);
//使用jsoup解析
Document parse = Jsoup.parse(html);
//解析div.section-body > ul > li路径下的内容
Elements clearfix = parse.select("div.section-body > ul > li");
//循环遍历
for (Element element : clearfix) {
//获取a标签下的内容
Elements a = element.select("a");
//获取图片
String src = a.select("img").attr("src");
//获取a标签下的跳转href
String href = a.attr("href");
//获取文章尾数编号
String article_id = "";
try {
article_id = href.substring(href.indexOf("article_id/")+11,href.indexOf("/channel_id"));
} catch (Exception e) {
// System.out.println("防止报错");
}
//获取标题
String title = a.select("span").select("b").text();
//封装结果
Arti arti = new Arti();
arti.setTitle(title);
arti.setHref(href);
arti.setArticle_id(article_id);
arti.setSrc(src);
//将结果加入集合
//调用方法继续执行
getElement(arti);
}
}
/**
* 根据爬取的内容来去进一步的加载
*/
private String getElement(Arti arti){
// System.out.println("arti:"+arti);
if(arti != null){
//如果文章id不为空,则说明无需重定向处理,否则就需要重定向处理
if (arti.getArticle_id() != null && !"".equals(arti.getArticle_id())){
//调用方法继续执行
redict(arti);
}else{
//调用方法继续执行,获取重定向后得到的链接
// System.out.println("执行了重定向方法");
redirect(arti);
}
}
return "";
}
/**
* 需要进一步重定向处理地址
* 获取重定向后得到的链接,然后继续调用处理
* @param arti
*/
private void redirect(Arti arti) {
//创建连接对象
CloseableHttpClient httpClient = HttpClients.createDefault();
List<URI> redirectLocations = null; //返回的重定向的地址集合
CloseableHttpResponse response = null;
try {
HttpClientContext context = HttpClientContext.create();
HttpGet httpGet = new HttpGet(arti.getHref()); //使用href去获取重定向地址
httpGet.addHeader("User-Agent","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36");
response = httpClient.execute(httpGet, context);
// 获取所有的重定向位置
redirectLocations = context.getRedirectLocations();
// System.out.println("redirectLocations:"+redirectLocations);
// System.out.println("getHref():"+arti.getHref());
String uri = redirectLocations.get(0).toString();//返回的重定向地址
String article_id = uri.substring(uri.lastIndexOf("/") + 1);//截取正确的文章id
// System.out.println("重定向后的文章id"+article_id);
//重新封装结果集
arti.setArticle_id(article_id);
arti.setHref(uri);
//调用方法继续执行
System.out.println(arti);
redict(arti);
} catch (IOException e) {
// System.out.println("redirect方法报错");
}
}
/**
* 无需重定向直接处理爬虫,获取响应id文章的内容
* @param arti
*/
private void redict(Arti arti) {
// System.out.println("非重定向:"+arti);
//封装结果
Map<String,String> map = new HashMap<>();
map.put("channel_id", "301700");
map.put("puid", "13712362");
map.put("article_id", arti.getArticle_id());
//发起HTTP请求
String s = HttpClientUtil.doPost("http://www.yiban.cn/forum/article/showAjax",map);
//将结果转换为JavaBean
JSONObject jsonObject = JSONObject.parseObject(s);
Result result = JSON.toJavaObject(jsonObject, Result.class);
//将结果存入数据库 Data
Data data = result.getData();
Article article = null;
Author author = null;
UrlParams urlParams = null;
String articleId = "";
try {
//文章
article = data.getArticle();
articleId = article.getId();
article.setId(arti.getArticle_id());
//修改时间格式
try {
if(article.getCreateTime().startsWith("2020") ||
article.getCreateTime().startsWith("2019")||
article.getCreateTime().startsWith("2018")||
article.getCreateTime().startsWith("2017")||
article.getCreateTime().startsWith("2016")||
article.getCreateTime().startsWith("2015")||
article.getCreateTime().startsWith("2021")){
//如果是以 以上前缀执行的,则不进行处理
}else{
//否则需要处理时间
//获取当前的年份
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy");
String year = simpleDateFormat.format(date);
String createTime = article.getCreateTime();
//赋值时间
article.setCreateTime(year+"-"+createTime);
}
} catch (Exception e) {
e.printStackTrace();
}
//作者信息
author = article.getAuthor();
//url信息
urlParams = data.getUrlParams();
/**
* 打印结果
*/
System.out.println("urlParams:"+urlParams);
System.out.println("作者信息:"+author);
System.out.println("文章信息:"+article);
} catch (Exception e) {
System.out.println("错误,跳过。");
}
}