項目詳情: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包,在服務器上設只一個定時任務,每天固定時間抓取即可。