项目详情:https://github.com/BlackerGod/Crawler
成品展示:点击访问
(ps:我在服务器上设置了定时任务每天五点自动爬取数据,但git不稳定,很有可能爬不到数据,可能表格为空)
一、项目需求
在我们Java开发过程中,需要引用一些项目,或者去学习别人项目中的技巧等。在Github里面有一个叫awesome-java的项目,他收录了Github中比较受欢迎的项目,但项目太多了,我们就需要一个工具就分析每个项目的受欢迎程度,我们从(StarCount、ForkCount、openIssueCount)等来衡量一个项目的受欢迎程度。
二、项目分析
我们需要工具先去获取每个项目的数据,然后保存在数据库中,写一个前台页面来解析数据库中的数据,最后以表格的形式显示在前台。
三、项目设计
前:建立Maven项目结构
- 抓取模块
- 分析模块
- 存储模块
- 展示模块
四、编码
1.抓取模块
我们需要一个httpClient来构造和发送请求、接收响应。我选择了okhttp的依赖来构造并获取响应。
public String getUrl(String url) throws IOException {
OkHttpClient okHttpClient = new OkHttpClient(); //创建一个客户端
Request request = new Request.Builder().url(url).build(); //根据url构造请求
Call call = okHttpClient.newCall(request);
Response response = call.execute(); //接收响应
if(!response.isSuccessful()){
System.out.println("请求失败");
return null;
}
return response.body().string(); //返回body部分
}
2.分析模块
当我们抓取到响应后,我们针对于页面进行分析。
可以看到每一个项目都是在<ul>标签下的<li>标签
可以看到<a>里面的内容为项目名称,然后href为各自项目的标签,那么我们可以用到 Jsoup工具来分析页面,同时
我们也需要一个名单来保存哪些不是github项目的链接
private HashSet<String> htmlBlackList = new HashSet<>();
{
htmlBlackList.add("https://github.com/events");
htmlBlackList.add("https://github.community");
htmlBlackList.add("https://github.com/about");
htmlBlackList.add("https://github.com/contact");
htmlBlackList.add("https://github.com/pricing");
}
public List<Project> parseProjectList(String html){
ArrayList<Project> result = new ArrayList<>(); //结果
Document document = Jsoup.parse(html);
Elements elements = document.getElementsByTag("li"); // 获取li标签
for (Element li : elements){ //遍历目标
Elements allLink = li.getElementsByTag("a");//获取a标签
if(allLink.size() == 0){
continue;
}
Element link = allLink.get(0);
String url = link.attr("href");
if(!url.startsWith("https://github.com")){
continue;
}
if(htmlBlackList.contains(url)){
continue;
}
Project project = new Project();
project.setName(link.text());
project.setUrl(link.attr("href"));
project.setDescription(li.text());
result.add(project);
}
return result;
}
到现在我们已经获取了每一个项目的名称和它的url,我们现在继续可以根据各自的url来获取他们的starCount等数量,但是这样比较麻烦,我们根据Github的官方API来获取每个项目的数据。
根据 https://api.github.com/repos/用户名/仓库名 就可以获取到仓库的详细信息
根据API返回的结果可以找到对应的数据,但返回的结果是Json格式的,我们就需要去获取Json里面的数据,这里我用的工具为Gson。(这里注意,Github在没登录情况下一小时只能调用60次)
public String getRepoName(String url){ //根据项目里面的url获取用户信息和仓库名
int lastOne = url.lastIndexOf("/");
int lastTwo = url.lastIndexOf("/",lastOne-1);
if(lastOne == -1 || lastTwo == -1){
System.out.println("url不是一个项目的url"+url);
return null;
}
return url.substring(lastTwo+1);
}
/**
*这里为了避免抓取频繁,我们要登录,先获取自己的Authorization,这是登录后的一个标志,我们设置到
*请求头里面,这样就可以一小时爬取5000次,这个函数返回的是json格式的字符串
*/
public String getRepoInfo(String respoName) throws IOException {
String username = "用户名";//自己的Git用户名
String password = "密码"; //自己的Git密码
//身份认证,把用户名密码加密后,得到一个字符串,把这个字符串放在HTTP header里面
String credential = Credentials.basic(username,password);
String url = "https://api.github.com/repos/"+respoName;
Request request = new Request.Builder().url(url).header("Authorization",credential).build();
Call call = okHttpClient.newCall(request);
Response response = call.execute();
if(!response.isSuccessful()){
System.out.println("获取项目失败");
return null;
}
return response.body().string();
}
然后根据设置每一个项目的属性值
public void parseRepoInfo(String jsonString,Project project){
//反射机制,从json里面获取到三个数据
Type type = new TypeToken<HashMap<String,Object>>(){}.getType();
HashMap<String,Object> hashMap = gson.fromJson(jsonString,type);
project.setStarCount(((Double) hashMap.get("stargazers_count")).intValue());
project.setForkCount(((Double) hashMap.get("forks_count")).intValue());
project.setOpenIssueCount(((Double) hashMap.get("open_issues_count")).intValue());
}
但我们知道,一个项目一个项目的爬取太慢了,网络都耗不起,所以我们使用多线程。
int THREADCOUNT = 10;
ExecutorService executorService = Executors.newFixedThreadPool(THREADCOUNT);
建立一个固定大小线程池
List<Future<?>> taskResults = new ArrayList<>();
for (Project project: projects){
Future<?> taskResult = executorService.submit(new CrawlerTask(project,crawler));
taskResults.add(taskResult);
}
//所有线程结束才可执行下一条
for (Future<?> taskResult : taskResults){
//调用Get方法就会阻塞,阻塞到改任务执行完毕,get才会返回
try {
taskResult.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//所有任务都执行完毕了,关闭线程池,回收资源
executorService.shutdown();
static class CrawlerTask implements Runnable{
private Project project;
private ThreadCrawler threadCrawler;
public CrawlerTask(Project project, ThreadCrawler threadCrawler) {
this.project = project;
this.threadCrawler = threadCrawler;
}
@Override
public void run() {
//1.调用API获取项目数据
//2.解析项目数据
try {
System.out.println("craw"+project.getName()+"...");
String repoName = threadCrawler.getRepoName(project.getUrl());
String jsonString = threadCrawler.getRepoInfo(repoName);
threadCrawler.parseRepoInfo(jsonString,project);
} catch (IOException e) {
throw new RuntimeException("插入失败"+project.getUrl());
}
}
}
这样相当于解析完毕,我们把每一个项目已经初始化结束
3.存储模块
先写一个DButil类用于获取和关闭连接。
package dao;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 获取数据库的连接,使用的单例模式
* */
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/gitproject?characterEncoding=utf8";
private static final String USER = "你的数据库用户名";
private static final String PASSWORD = "密码";
private static volatile DataSource dataSource = null;
private static DataSource getDataSource() {
if (dataSource == null) {
synchronized (DBUtil.class) {
dataSource = new MysqlDataSource();
MysqlDataSource mysqlDataSource = (MysqlDataSource) dataSource;
mysqlDataSource.setURL(URL);
mysqlDataSource.setUser(USER);
mysqlDataSource.setPassword(PASSWORD);
}
}
return dataSource;
}
public static Connection getConnection(){
try{
return getDataSource().getConnection();
} catch (SQLException e){
e.printStackTrace();
}
return null;
}
public static void close(Connection connection , PreparedStatement preparedStatement,ResultSet resultSet){
try {
if(resultSet != null) {
resultSet.close();
}
if(preparedStatement != null) {
preparedStatement.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
然后就是存储
public void save(Project project){
Connection connection = DBUtil.getConnection();
PreparedStatement preparedStatement = null;
String sql = "insert into project_table values(?,?,?,?,?,?,?);";
try {
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,project.getName());
preparedStatement.setString(2,project.getUrl());
preparedStatement.setString(3,project.getDescription());
preparedStatement.setInt(4,project.getStarCount());
preparedStatement.setInt(5,project.getForkCount());
preparedStatement.setInt(6,project.getOpenIssueCount());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
preparedStatement.setString(7,simpleDateFormat.format(System.currentTimeMillis()));
//new Date().getTime())
int ret = preparedStatement.executeUpdate();
if(ret != 1){
System.out.println("当前数据库执行插入数据出错");
return;
}
//System.out.println("数据插入成功");
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection,preparedStatement,null);
}
}
/**
*根据日期来获取数据库的数据
*/
public List<Project> selectProjectByDate(String date){
List<Project> projects = new ArrayList<>();
Connection connection = DBUtil.getConnection();
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
String sql = "select name,url,starCount,forkCount,openIssueCount " +
"from project_table where date = ? order by starCount desc;";
try {
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,date);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
Project project = new Project();
project.setName(resultSet.getString("name"));
project.setUrl(resultSet.getString("url"));
project.setStarCount(resultSet.getInt("starCount"));
project.setForkCount(resultSet.getInt("forkCount"));
project.setOpenIssueCount(resultSet.getInt("openIssueCount"));
projects.add(project);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection,preparedStatement,resultSet);
}
return projects;
}
4.展示模块
先写一个servlet来用于前端和后台的交互
package api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.Project;
import dao.ProjectDao;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet("/AllRankServlet")
public class AllRankServlet extends HttpServlet {
private Gson gson = new GsonBuilder().create();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=utf-8");
String date = req.getParameter("date");
if(date == null || date.equals("")){
resp.setStatus(404);
resp.getWriter().write("date参数错误");
return;
}
ProjectDao projectDao = new ProjectDao();
List<Project> projects = projectDao.selectProjectByDate(date);
String respString = gson.toJson(projects);//把数据转成json格式
resp.getWriter().write(respString);
return;
}
}
再写一个前端页面(前端页面写的比较菜,所以我从网站找了一个js制表网站)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我的Github趋势</title>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 100%;height:600px;"></div>
<!--从网络上下载 JQuery 这个库-->
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<!-- 引入 ECharts 文件 -->
<script src="js/echarts.min.js"></script>
<script>
function drawStars(projectNames,stars) { //根据echars提供的文档写
var myChart = echarts.init(document.getElementById('main'));
var option = {
title: {
text: 'Star 天榜'
},
tooltip: {},
legend: {
data:['Star']
},
xAxis: {
//data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
data:projectNames
},
yAxis: {},
series: [{
name: 'Star',
type: 'bar',//柱状图
//data: [5, 20, 36, 10, 10, 20]
data:stars
}],
dataZoom: [
{ // 这个dataZoom组件,默认控制x轴。
type: 'slider', // 这个 dataZoom 组件是 slider 型 dataZoom 组件
start: 0, // 左边在 10% 的位置。
end: 10 // 右边在 60% 的位置。
},
{ // 这个dataZoom组件,也控制x轴。
type: 'inside', // 这个 dataZoom 组件是 inside 型 dataZoom 组件
start: 0, // 左边在 10% 的位置。
end: 10 // 右边在 60% 的位置。
}
],
};
myChart.setOption(option);
}
Date.prototype.Format = function (formatStr) {
var str = formatStr;
var Week = ['日', '一', '二', '三', '四', '五', '六'];
str = str.replace(/yyyy|YYYY/, this.getFullYear());
str = str.replace(/yy|YY/, (this.getYear() % 100) > 9 ? (this.getYear() % 100).toString() : '0' + (this.getYear() % 100));
str = str.replace(/MM/, this.getMonth() > 9 ? this.getMonth().toString() + 1 : '0' + (this.getMonth() + 1));
str = str.replace(/M/g, this.getMonth());
str = str.replace(/w|W/g, Week[this.getDay()]);
str = str.replace(/dd|DD/, this.getDate() > 9 ? this.getDate().toString() : '0' + this.getDate());
str = str.replace(/d|D/g, this.getDate());
str = str.replace(/hh|HH/, this.getHours() > 9 ? this.getHours().toString() : '0' + this.getHours());
str = str.replace(/h|H/g, this.getHours());
str = str.replace(/mm/, this.getMinutes() > 9 ? this.getMinutes().toString() : '0' + this.getMinutes());
str = str.replace(/m/g, this.getMinutes());
str = str.replace(/ss|SS/, this.getSeconds() > 9 ? this.getSeconds().toString() : '0' + this.getSeconds());
str = str.replace(/s|S/g, this.getSeconds());
return str;
}
var date = new Date().Format("yyyyMMdd");
// 这是 JS 常用调试手段, 可以把一段内容打印到浏览器的控制台上.
console.log(date);
$.ajax({
url:"AllRankServlet?date=" + date,
type:'get',
success:function (date,status) {
var projectNames=[];
var stars = [];
for(var index in date){
var project = date[index];
projectNames.push(project.name);
stars.push(project.starCount);
}
drawStars(projectNames,stars);
}
})
</script>
</body>
</html>
到此就结束了,然后就是打成war包部署。然后把抓取的那个类打成jar包,在服务器上设只一个定时任务,每天固定时间抓取即可。